• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ArkScript-lang / Ark / 22281229261

22 Feb 2026 04:49PM UTC coverage: 93.5% (+0.04%) from 93.464%
22281229261

push

github

SuperFola
feat(cli): ARK-336, accept '-' as a filename to mean 'read code from stdin'

9250 of 9893 relevant lines covered (93.5%)

267283.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.13
/src/arkreactor/Compiler/AST/Parser.cpp
1
#include <Ark/Compiler/AST/Parser.hpp>
2

3
#include <fmt/core.h>
4

5
namespace Ark::internal
6
{
7
    Parser::Parser(const unsigned debug, const ParserMode mode) :
1,386✔
8
        BaseParser(), m_mode(mode), m_logger("Parser", debug),
693✔
9
        m_ast(NodeType::List), m_imports({}), m_allow_macro_behavior(0),
693✔
10
        m_nested_nodes(0)
693✔
11
    {
693✔
12
        m_ast.push_back(Node(Keyword::Begin));
693✔
13

14
        m_parsers = {
1,386✔
15
            [this](FilePosition) {
73,827✔
16
                return wrapped(&Parser::letMutSet, "variable assignment or declaration");
73,134✔
17
            },
9✔
18
            [this](FilePosition) {
63,423✔
19
                return wrapped(&Parser::function, "function");
62,730✔
20
            },
8✔
21
            [this](FilePosition) {
59,532✔
22
                return wrapped(&Parser::condition, "condition");
58,839✔
23
            },
3✔
24
            [this](FilePosition) {
57,650✔
25
                return wrapped(&Parser::loop, "loop");
56,957✔
26
            },
2✔
27
            [this](const FilePosition filepos) {
56,390✔
28
                return import_(filepos);
55,697✔
29
            },
30
            [this](const FilePosition filepos) {
56,157✔
31
                return block(filepos);
55,464✔
32
            },
33
            [this](FilePosition) {
52,732✔
34
                return wrapped(&Parser::macroCondition, "$if");
52,039✔
35
            },
2✔
36
            [this](const FilePosition filepos) {
52,677✔
37
                return macro(filepos);
51,984✔
38
            },
39
            [this](FilePosition) {
52,508✔
40
                return wrapped(&Parser::del, "del");
51,815✔
41
            },
1✔
42
            [this](const FilePosition filepos) {
52,496✔
43
                return functionCall(filepos);
51,803✔
44
            },
45
            [this](const FilePosition filepos) {
29,858✔
46
                return list(filepos);
29,165✔
47
            }
48
        };
49
    }
693✔
50

51
    void Parser::process(const std::string& filename, const std::string& code)
693✔
52
    {
693✔
53
        m_logger.traceStart("process");
749✔
54
        initParser(filename, code);
693✔
55

56
        while (!isEOF())
5,707✔
57
        {
58
            std::string comment = newlineOrComment();
5,650✔
59
            if (isEOF())
5,650✔
60
            {
61
                if (!comment.empty())
580✔
62
                    m_ast.list().back().attachCommentAfter(comment);
2✔
63
                break;
580✔
64
            }
65

66
            const auto pos = getCount();
5,070✔
67
            if (auto n = node())
10,140✔
68
            {
69
                m_ast.push_back(n->attachNearestCommentBefore(n->comment() + comment));
5,014✔
70
                m_ast.list().back().attachCommentAfter(spaceComment());
5,014✔
71
            }
5,014✔
72
            else
73
            {
74
                backtrack(pos);
6✔
75
                std::string out = peek();
6✔
76
                std::string message;
6✔
77
                if (out == ")")
6✔
78
                    message = "Unexpected closing paren";
1✔
79
                else if (out == "}")
5✔
80
                    message = "Unexpected closing bracket";
1✔
81
                else if (out == "]")
4✔
82
                    message = "Unexpected closing square bracket";
1✔
83
                else
84
                    errorWithNextToken("invalid syntax, expected node");
3✔
85
                errorWithNextToken(message);
3✔
86
            }
6✔
87
        }
5,650✔
88

89
        m_logger.traceEnd();
637✔
90
    }
693✔
91

92
    const Node& Parser::ast() const noexcept
652✔
93
    {
652✔
94
        return m_ast;
652✔
95
    }
96

97
    const std::vector<Import>& Parser::imports() const
563✔
98
    {
563✔
99
        return m_imports;
563✔
100
    }
101

102
    Node Parser::positioned(Node node, const FilePosition cursor) const
165,313✔
103
    {
165,313✔
104
        const auto [row, col] = cursor;
495,939✔
105
        const auto [end_row, end_col] = getCursor();
495,939✔
106

107
        node.m_filename = m_filename;
165,313✔
108
        node.m_pos = FileSpan {
330,626✔
109
            .start = FilePos { .line = row, .column = col },
495,939✔
110
            .end = FilePos { .line = end_row, .column = end_col }
495,939✔
111
        };
112
        return node;
165,313✔
113
    }
165,313✔
114

115
    std::optional<Node>& Parser::positioned(std::optional<Node>& node, const FilePosition cursor) const
49,507✔
116
    {
49,507✔
117
        if (!node)
49,507✔
118
            return node;
×
119

120
        const auto [row, col] = cursor;
148,521✔
121
        const auto [end_row, end_col] = getCursor();
148,521✔
122

123
        node->m_filename = m_filename;
49,507✔
124
        node->m_pos = FileSpan {
99,014✔
125
            .start = FilePos { .line = row, .column = col },
148,521✔
126
            .end = FilePos { .line = end_row, .column = end_col }
148,521✔
127
        };
128
        return node;
49,507✔
129
    }
49,507✔
130

131
    std::optional<Node> Parser::node()
73,135✔
132
    {
73,135✔
133
        ++m_nested_nodes;
73,135✔
134

135
        if (m_nested_nodes > MaxNestedNodes)
73,135✔
136
            errorWithNextToken(fmt::format("Too many nested node while parsing, exceeds limit of {}. Consider rewriting your code by breaking it in functions and macros.", MaxNestedNodes));
1,084✔
137

138
        // save current position in buffer to be able to go back if needed
139
        const auto position = getCount();
73,134✔
140
        const auto filepos = getCursor();
73,134✔
141
        std::optional<Node> result = std::nullopt;
73,134✔
142

143
        for (auto&& parser : m_parsers)
672,761✔
144
        {
145
            result = parser(filepos);
599,627✔
146

147
            if (result)
598,544✔
148
                break;
44,568✔
149
            backtrack(position);
553,976✔
150
        }
599,627✔
151

152
        // return std::nullopt only on parsing error, nothing matched, the user provided terrible code
153
        --m_nested_nodes;
72,051✔
154
        return result;
72,051✔
155
    }
73,135✔
156

157
    std::optional<Node> Parser::letMutSet(const FilePosition filepos)
40,554✔
158
    {
40,554✔
159
        std::optional<Node> leaf { NodeType::List };
40,554✔
160

161
        std::string token;
40,554✔
162
        if (!oneOf({ "let", "mut", "set" }, &token))
40,554✔
163
            return std::nullopt;
30,150✔
164

165
        std::string comment = newlineOrComment();
10,404✔
166
        leaf->attachNearestCommentBefore(comment);
10,404✔
167

168
        if (token == "let")
10,404✔
169
            leaf->push_back(Node(Keyword::Let));
5,194✔
170
        else if (token == "mut")
5,210✔
171
            leaf->push_back(Node(Keyword::Mut));
2,823✔
172
        else  // "set"
173
            leaf->push_back(Node(Keyword::Set));
2,387✔
174

175
        if (m_allow_macro_behavior > 0)
10,404✔
176
        {
177
            const auto position = getCount();
55✔
178
            const auto value_pos = getCursor();
55✔
179
            if (const auto value = nodeOrValue(); value.has_value())
110✔
180
            {
181
                const Node& sym = value.value();
55✔
182
                if (sym.nodeType() == NodeType::List || sym.nodeType() == NodeType::Symbol || sym.nodeType() == NodeType::Macro || sym.nodeType() == NodeType::Spread)
55✔
183
                    leaf->push_back(sym);
54✔
184
                else
185
                    error(fmt::format("Can not use a {} as a symbol name, even in a macro", nodeTypes[static_cast<std::size_t>(sym.nodeType())]), value_pos);
1✔
186
            }
55✔
187
            else
188
                backtrack(position);
×
189
        }
55✔
190

191
        if (leaf->constList().size() == 1)
10,403✔
192
        {
193
            // we haven't parsed anything while in "macro state"
194
            std::string symbol_name;
10,349✔
195
            const auto value_pos = getCursor();
10,349✔
196
            if (!name(&symbol_name))
10,349✔
197
                errorWithNextToken(token + " needs a symbol");
2✔
198

199
            leaf->push_back(positioned(Node(NodeType::Symbol, symbol_name), value_pos));
10,347✔
200
        }
10,349✔
201

202
        comment = newlineOrComment();
10,401✔
203
        if (auto value = nodeOrValue(); value.has_value())
20,802✔
204
            leaf->push_back(value.value().attachNearestCommentBefore(comment));
10,395✔
205
        else
206
            errorWithNextToken("Expected a value");
×
207

208
        return positioned(leaf, filepos);
10,395✔
209
    }
40,563✔
210

211
    std::optional<Node> Parser::del(const FilePosition filepos)
22,651✔
212
    {
22,651✔
213
        std::optional<Node> leaf { NodeType::List };
22,651✔
214

215
        if (!oneOf({ "del" }))
22,651✔
216
            return std::nullopt;
22,639✔
217
        leaf->push_back(Node(Keyword::Del));
12✔
218

219
        const std::string comment = newlineOrComment();
12✔
220

221
        std::string symbol_name;
12✔
222
        if (!name(&symbol_name))
12✔
223
            errorWithNextToken("del needs a symbol");
1✔
224

225
        leaf->push_back(Node(NodeType::Symbol, symbol_name));
11✔
226
        leaf->list().back().attachNearestCommentBefore(comment);
11✔
227

228
        return positioned(leaf, filepos);
11✔
229
    }
22,652✔
230

231
    std::optional<Node> Parser::condition(const FilePosition filepos)
26,259✔
232
    {
26,259✔
233
        std::optional<Node> leaf { NodeType::List };
26,259✔
234

235
        if (!oneOf({ "if" }))
26,259✔
236
            return std::nullopt;
24,377✔
237

238
        std::string comment = newlineOrComment();
1,882✔
239

240
        leaf->push_back(Node(Keyword::If));
1,882✔
241

242
        if (auto cond_expr = nodeOrValue(); cond_expr.has_value())
3,764✔
243
            leaf->push_back(cond_expr.value().attachNearestCommentBefore(comment));
1,881✔
244
        else
245
            errorWithNextToken("`if' needs a valid condition");
1✔
246

247
        comment = newlineOrComment();
1,881✔
248
        if (auto value_if_true = nodeOrValue(); value_if_true.has_value())
3,762✔
249
            leaf->push_back(value_if_true.value().attachNearestCommentBefore(comment));
1,880✔
250
        else
251
            errorWithNextToken("Expected a node or value after condition");
1✔
252

253
        comment = newlineOrComment();
1,880✔
254
        if (auto value_if_false = nodeOrValue(); value_if_false.has_value())
3,760✔
255
        {
256
            leaf->push_back(value_if_false.value().attachNearestCommentBefore(comment));
1,138✔
257
            leaf->list().back().attachCommentAfter(newlineOrComment());
1,138✔
258
        }
1,138✔
259
        else if (!comment.empty())
742✔
260
            leaf->attachCommentAfter(comment);
2✔
261

262
        return positioned(leaf, filepos);
1,880✔
263
    }
26,261✔
264

265
    std::optional<Node> Parser::loop(const FilePosition filepos)
24,377✔
266
    {
24,377✔
267
        std::optional<Node> leaf { NodeType::List };
24,377✔
268

269
        if (!oneOf({ "while" }))
24,377✔
270
            return std::nullopt;
23,117✔
271

272
        std::string comment = newlineOrComment();
1,260✔
273
        leaf->push_back(Node(Keyword::While));
1,260✔
274

275
        if (auto cond_expr = nodeOrValue(); cond_expr.has_value())
2,520✔
276
            leaf->push_back(cond_expr.value().attachNearestCommentBefore(comment));
1,259✔
277
        else
278
            errorWithNextToken("`while' needs a valid condition");
1✔
279

280
        comment = newlineOrComment();
1,259✔
281
        if (auto body = nodeOrValue(); body.has_value())
2,518✔
282
            leaf->push_back(body.value().attachNearestCommentBefore(comment));
1,258✔
283
        else
284
            errorWithNextToken("Expected a node or value after loop condition");
1✔
285

286
        return positioned(leaf, filepos);
1,258✔
287
    }
24,379✔
288

289
    std::optional<Node> Parser::import_(const FilePosition filepos)
55,697✔
290
    {
55,697✔
291
        std::optional<Node> leaf { NodeType::List };
55,697✔
292

293
        auto context = generateErrorContextAtCurrentPosition();
55,697✔
294
        if (!accept(IsChar('(')))
55,697✔
295
            return std::nullopt;
32,580✔
296

297
        std::string comment = newlineOrComment();
23,117✔
298
        leaf->attachNearestCommentBefore(comment);
23,117✔
299

300
        if (!oneOf({ "import" }))
23,117✔
301
            return std::nullopt;
22,884✔
302

303
        comment = newlineOrComment();
233✔
304
        leaf->push_back(Node(Keyword::Import));
233✔
305

306
        Import import_data;
233✔
307
        import_data.col = filepos.col;
233✔
308
        import_data.line = filepos.row;
233✔
309

310
        const auto pos = getCount();
233✔
311
        if (!packageName(&import_data.prefix))
233✔
312
            errorWithNextToken("Import expected a package name");
1✔
313

314
        if (import_data.prefix.size() > 255)
232✔
315
        {
316
            backtrack(pos);
1✔
317
            errorWithNextToken(fmt::format("Import name too long, expected at most 255 characters, got {}", import_data.prefix.size()));
1✔
318
        }
×
319
        import_data.package.push_back(import_data.prefix);
231✔
320

321
        Node packageNode = positioned(Node(NodeType::List), getCursor()).attachNearestCommentBefore(comment);
231✔
322
        packageNode.push_back(Node(NodeType::Symbol, import_data.prefix));
231✔
323

324
        // first, parse the package name
325
        while (!isEOF())
405✔
326
        {
327
            const auto item_pos = getCursor();
405✔
328

329
            // parsing package folder.foo.bar.yes
330
            if (accept(IsChar('.')))
405✔
331
            {
332
                const auto package_pos = getCursor();
177✔
333
                std::string path;
177✔
334
                if (!packageName(&path))
177✔
335
                    errorWithNextToken("Package name expected after '.'");
2✔
336
                else
337
                {
338
                    packageNode.push_back(positioned(Node(NodeType::Symbol, path), package_pos));
175✔
339

340
                    import_data.package.push_back(path);
175✔
341
                    import_data.prefix = path;  // in the end we will store the last element of the package, which is what we want
175✔
342

343
                    if (path.size() > 255)
175✔
344
                    {
345
                        backtrack(pos);
1✔
346
                        errorWithNextToken(fmt::format("Import name too long, expected at most 255 characters, got {}", path.size()));
1✔
347
                    }
×
348
                }
349
            }
177✔
350
            else if (accept(IsChar(':')) && accept(IsChar('*')))  // parsing :*, terminal in imports
228✔
351
            {
352
                leaf->push_back(packageNode);
11✔
353
                leaf->push_back(positioned(Node(NodeType::Symbol, "*"), item_pos));
11✔
354

355
                space();
11✔
356
                expectSuffixOrError(')', fmt::format("in import `{}'", import_data.toPackageString()), context);
11✔
357

358
                // save the import data structure to know we encounter an import node, and retrieve its data more easily later on
359
                import_data.with_prefix = false;
11✔
360
                import_data.is_glob = true;
11✔
361
                m_imports.push_back(import_data);
11✔
362

363
                return positioned(leaf, filepos);
11✔
364
            }
365
            else
366
                break;
217✔
367
        }
405✔
368

369
        Node symbols = positioned(Node(NodeType::List), getCursor());
217✔
370
        // then parse the symbols to import, if any
371
        if (space())
217✔
372
        {
373
            comment = newlineOrComment();
83✔
374

375
            while (!isEOF())
151✔
376
            {
377
                if (accept(IsChar(':')))  // parsing potential :a :b :c
151✔
378
                {
379
                    const auto symbol_pos = getCursor();
148✔
380
                    std::string symbol_name;
148✔
381
                    if (!name(&symbol_name))
148✔
382
                        errorWithNextToken("Expected a valid symbol to import");
1✔
383
                    if (symbol_name == "*")
147✔
384
                        error(fmt::format("Glob patterns can not be separated from the package, use (import {}:*) instead", import_data.toPackageString()), symbol_pos);
1✔
385

386
                    if (symbol_name.size() >= 2 && symbol_name[symbol_name.size() - 2] == ':' && symbol_name.back() == '*')
146✔
387
                        error("Glob pattern can not follow a symbol to import", FilePosition { .row = symbol_pos.row, .col = symbol_pos.col + symbol_name.size() - 2 });
1✔
388

389
                    symbols.push_back(positioned(Node(NodeType::Symbol, symbol_name).attachNearestCommentBefore(comment), symbol_pos));
145✔
390
                    comment.clear();
145✔
391

392
                    import_data.symbols.push_back(symbol_name);
145✔
393
                    // we do not need the prefix when importing specific symbols
394
                    import_data.with_prefix = false;
145✔
395
                }
148✔
396

397
                if (!space())
148✔
398
                    break;
80✔
399
                comment = newlineOrComment();
68✔
400
            }
401

402
            if (!comment.empty() && !symbols.list().empty())
80✔
403
                symbols.list().back().attachCommentAfter(comment);
2✔
404
        }
80✔
405

406
        leaf->push_back(packageNode);
214✔
407
        leaf->push_back(symbols);
214✔
408
        // save the import data
409
        m_imports.push_back(import_data);
214✔
410

411
        comment = newlineOrComment();
214✔
412
        if (!comment.empty())
214✔
413
            leaf->list().back().attachCommentAfter(comment);
1✔
414

415
        expectSuffixOrError(')', fmt::format("in import `{}'", import_data.toPackageString()), context);
214✔
416
        return positioned(leaf, filepos);
214✔
417
    }
55,705✔
418

419
    std::optional<Node> Parser::block(const FilePosition filepos)
55,464✔
420
    {
55,464✔
421
        std::optional<Node> leaf { NodeType::List };
55,464✔
422

423
        auto context = generateErrorContextAtCurrentPosition();
55,464✔
424
        bool alt_syntax = false;
55,464✔
425
        std::string comment;
55,464✔
426
        if (accept(IsChar('(')))
55,464✔
427
        {
428
            comment = newlineOrComment();
22,884✔
429
            if (!oneOf({ "begin" }))
22,884✔
430
                return std::nullopt;
22,875✔
431
        }
9✔
432
        else if (accept(IsChar('{')))
32,580✔
433
            alt_syntax = true;
3,416✔
434
        else
435
            return std::nullopt;
29,164✔
436

437
        leaf->setAltSyntax(alt_syntax);
3,425✔
438
        leaf->push_back(Node(Keyword::Begin).attachNearestCommentBefore(comment));
3,425✔
439

440
        comment = newlineOrComment();
3,425✔
441

442
        while (!isEOF())
15,305✔
443
        {
444
            if (auto value = nodeOrValue(); value.has_value())
30,608✔
445
            {
446
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
11,880✔
447
                comment = newlineOrComment();
11,880✔
448
            }
11,880✔
449
            else
450
                break;
3,423✔
451
        }
452

453
        comment += newlineOrComment();
3,424✔
454
        expectSuffixOrError(alt_syntax ? '}' : ')', "to close block", context);
3,424✔
455
        leaf->list().back().attachCommentAfter(comment);
3,423✔
456
        return positioned(leaf, filepos);
3,423✔
457
    }
55,466✔
458

459
    std::optional<Node> Parser::functionArgs(const FilePosition filepos)
3,867✔
460
    {
3,867✔
461
        expect(IsChar('('));
3,870✔
462
        std::optional<Node> args { NodeType::List };
3,867✔
463

464
        std::string comment = newlineOrComment();
3,867✔
465
        args->attachNearestCommentBefore(comment);
3,867✔
466

467
        bool has_captures = false;
3,867✔
468

469
        while (!isEOF())
10,109✔
470
        {
471
            const auto pos = getCursor();
10,108✔
472
            if (accept(IsChar('&')))  // captures
10,108✔
473
            {
474
                has_captures = true;
293✔
475
                std::string capture;
293✔
476
                if (!name(&capture))
293✔
477
                    error("No symbol provided to capture", pos);
1✔
478

479
                args->push_back(positioned(Node(NodeType::Capture, capture), pos));
292✔
480
            }
293✔
481
            else if (accept(IsChar('(')))
9,815✔
482
            {
483
                // attribute modifiers: mut, ref
484
                std::string modifier;
975✔
485
                std::ignore = newlineOrComment();
975✔
486
                if (!oneOf({ "mut", "ref" }, &modifier))
975✔
487
                    // We cannot return an error like this:
488
                    //   error("Expected an attribute modifier, either `mut' or `ref'", pos);
489
                    // Because it would break on macro instantiations like (fun ((suffix-dup a 3)) ())
490
                    return std::nullopt;
1✔
491

492
                NodeType type = NodeType::Unused;
974✔
493
                if (modifier == "mut")
974✔
494
                    type = NodeType::MutArg;
104✔
495
                else if (modifier == "ref")
870✔
496
                    type = NodeType::RefArg;
870✔
497

498
                Node arg_with_attr = Node(type);
974✔
499
                std::string comment2 = newlineOrComment();
974✔
500
                arg_with_attr.attachCommentAfter(comment2);
974✔
501

502
                std::string symbol_name;
974✔
503
                if (!name(&symbol_name))
974✔
504
                    error(fmt::format("Expected a symbol name for the attribute with modifier `{}'", modifier), pos);
1✔
505
                arg_with_attr.setString(symbol_name);
973✔
506

507
                args->push_back(positioned(arg_with_attr, pos));
973✔
508
                std::ignore = newlineOrComment();
973✔
509
                expect(IsChar(')'));
973✔
510
            }
975✔
511
            else
512
            {
513
                std::string symbol_name;
8,840✔
514
                if (!name(&symbol_name))
8,840✔
515
                    break;
3,862✔
516
                if (has_captures)
4,978✔
517
                    error("Captured variables should be at the end of the argument list", pos);
1✔
518

519
                args->push_back(positioned(Node(NodeType::Symbol, symbol_name), pos));
4,977✔
520
            }
8,840✔
521

522
            if (!comment.empty())
6,242✔
523
                args->list().back().attachNearestCommentBefore(comment);
12✔
524
            comment = newlineOrComment();
6,242✔
525
        }
10,108✔
526

527
        if (accept(IsChar(')')))
3,863✔
528
            return positioned(args, filepos);
3,862✔
529
        return std::nullopt;
1✔
530
    }
3,870✔
531

532
    std::optional<Node> Parser::function(const FilePosition filepos)
30,150✔
533
    {
30,150✔
534
        std::optional<Node> leaf { NodeType::List };
30,150✔
535

536
        if (!oneOf({ "fun" }))
30,150✔
537
            return std::nullopt;
26,259✔
538
        leaf->push_back(Node(Keyword::Fun));
3,891✔
539

540
        const std::string comment_before_args = newlineOrComment();
3,891✔
541

542
        while (m_allow_macro_behavior > 0)
3,891✔
543
        {
544
            const auto position = getCount();
24✔
545

546
            // args
547
            if (const auto value = nodeOrValue(); value.has_value())
48✔
548
            {
549
                // if value is nil, just add an empty argument bloc to prevent bugs when
550
                // declaring functions inside macros
551
                const Node& args = value.value();
24✔
552
                if (args.nodeType() == NodeType::Symbol && args.string() == "nil")
24✔
553
                    leaf->push_back(Node(NodeType::List));
6✔
554
                else
555
                    leaf->push_back(args);
18✔
556
            }
24✔
557
            else
558
            {
559
                backtrack(position);
×
560
                break;
×
561
            }
562

563
            const std::string comment = newlineOrComment();
24✔
564
            // body
565
            if (auto value = nodeOrValue(); value.has_value())
48✔
566
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
23✔
567
            else
568
                errorWithNextToken("Expected a body for the function");
1✔
569
            return positioned(leaf, filepos);
23✔
570
        }
24✔
571

572
        const auto position = getCount();
3,867✔
573
        const auto args_file_pos = getCursor();
3,867✔
574
        if (auto args = functionArgs(args_file_pos); args.has_value())
7,734✔
575
            leaf->push_back(args.value().attachNearestCommentBefore(comment_before_args));
3,862✔
576
        else
577
        {
578
            backtrack(position);
2✔
579

580
            if (auto value = nodeOrValue(); value.has_value())
4✔
581
                leaf->push_back(value.value().attachNearestCommentBefore(comment_before_args));
1✔
582
            else
583
                errorWithNextToken("Expected an argument list");
×
584
        }
585

586
        const std::string comment = newlineOrComment();
3,863✔
587

588
        if (auto value = nodeOrValue(); value.has_value())
7,726✔
589
            leaf->push_back(value.value().attachNearestCommentBefore(comment));
3,861✔
590
        else
591
            errorWithNextToken("Expected a body for the function");
2✔
592

593
        return positioned(leaf, filepos);
3,861✔
594
    }
30,157✔
595

596
    std::optional<Node> Parser::macroCondition(const FilePosition filepos)
22,875✔
597
    {
22,875✔
598
        std::optional<Node> leaf { NodeType::Macro };
22,875✔
599

600
        if (!oneOf({ "$if" }))
22,875✔
601
            return std::nullopt;
22,820✔
602
        leaf->push_back(Node(Keyword::If));
55✔
603

604
        std::string comment = newlineOrComment();
55✔
605
        leaf->attachNearestCommentBefore(comment);
55✔
606

607
        if (const auto cond_expr = nodeOrValue(); cond_expr.has_value())
110✔
608
            leaf->push_back(cond_expr.value());
54✔
609
        else
610
            errorWithNextToken("$if need a valid condition");
1✔
611

612
        comment = newlineOrComment();
54✔
613
        if (auto value_if_true = nodeOrValue(); value_if_true.has_value())
108✔
614
            leaf->push_back(value_if_true.value().attachNearestCommentBefore(comment));
53✔
615
        else
616
            errorWithNextToken("Expected a node or value after condition");
1✔
617

618
        comment = newlineOrComment();
53✔
619
        if (auto value_if_false = nodeOrValue(); value_if_false.has_value())
92✔
620
        {
621
            leaf->push_back(value_if_false.value().attachNearestCommentBefore(comment));
39✔
622
            comment = newlineOrComment();
39✔
623
            leaf->list().back().attachCommentAfter(comment);
39✔
624
        }
39✔
625

626
        return positioned(leaf, filepos);
53✔
627
    }
22,877✔
628

629
    std::optional<Node> Parser::macroArgs(const FilePosition filepos)
168✔
630
    {
168✔
631
        if (!accept(IsChar('(')))
172✔
632
            return std::nullopt;
25✔
633

634
        std::optional<Node> args { NodeType::List };
143✔
635

636
        std::string comment = newlineOrComment();
143✔
637
        args->attachNearestCommentBefore(comment);
143✔
638

639
        std::vector<std::string> names;
143✔
640
        while (!isEOF())
343✔
641
        {
642
            const auto pos = getCount();
342✔
643

644
            std::string arg_name;
342✔
645
            if (!name(&arg_name))
342✔
646
                break;
141✔
647

648
            comment = newlineOrComment();
201✔
649
            args->push_back(Node(NodeType::Symbol, arg_name).attachNearestCommentBefore(comment));
201✔
650

651
            if (std::ranges::find(names, arg_name) != names.end())
201✔
652
            {
653
                backtrack(pos);
1✔
654
                errorWithNextToken(fmt::format("Argument names must be unique, can not reuse `{}'", arg_name));
1✔
655
            }
×
656
            names.push_back(arg_name);
200✔
657
        }
342✔
658

659
        const auto pos = getCount();
142✔
660
        if (sequence("..."))
142✔
661
        {
662
            std::string spread_name;
48✔
663
            if (!name(&spread_name))
48✔
664
                errorWithNextToken("Expected a name for the variadic arguments list");
2✔
665

666
            args->push_back(Node(NodeType::Spread, spread_name));
46✔
667
            args->list().back().attachCommentAfter(newlineOrComment());
46✔
668

669
            if (std::ranges::find(names, spread_name) != names.end())
46✔
670
            {
671
                backtrack(pos);
1✔
672
                errorWithNextToken(fmt::format("Argument names must be unique, can not reuse `{}'", spread_name));
1✔
673
            }
×
674
        }
48✔
675

676
        if (!accept(IsChar(')')))
139✔
677
            return std::nullopt;
34✔
678

679
        comment = newlineOrComment();
105✔
680
        if (!comment.empty())
105✔
681
            args->attachCommentAfter(comment);
4✔
682

683
        return positioned(args, filepos);
105✔
684
    }
172✔
685

686
    std::optional<Node> Parser::macro(const FilePosition filepos)
51,984✔
687
    {
51,984✔
688
        std::optional<Node> leaf { NodeType::Macro };
51,984✔
689

690
        auto context = generateErrorContextAtCurrentPosition();
51,984✔
691
        if (!accept(IsChar('(')))
51,984✔
692
            return std::nullopt;
29,164✔
693

694
        if (!oneOf({ "macro" }))
22,820✔
695
            return std::nullopt;
22,651✔
696
        std::string comment = newlineOrComment();
169✔
697
        leaf->attachNearestCommentBefore(comment);
169✔
698

699
        std::string symbol_name;
169✔
700
        if (!name(&symbol_name))
169✔
701
            errorWithNextToken("Expected a symbol to declare a macro");
1✔
702

703
        comment = newlineOrComment();
168✔
704
        leaf->push_back(Node(NodeType::Symbol, symbol_name).attachNearestCommentBefore(comment));
168✔
705

706
        const auto position = getCount();
168✔
707
        const auto args_file_pos = getCursor();
168✔
708
        if (const auto args = macroArgs(args_file_pos); args.has_value())
336✔
709
            leaf->push_back(args.value());
105✔
710
        else
711
        {
712
            // if we couldn't parse arguments, then we have a value
713
            backtrack(position);
59✔
714

715
            ++m_allow_macro_behavior;
59✔
716
            const auto value = nodeOrValue();
59✔
717
            --m_allow_macro_behavior;
57✔
718

719
            if (value.has_value())
57✔
720
                leaf->push_back(value.value());
56✔
721
            else
722
                errorWithNextToken(fmt::format("Expected an argument list, atom or node while defining macro `{}'", symbol_name));
1✔
723

724
            leaf->list().back().attachCommentAfter(newlineOrComment());
56✔
725
            expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
56✔
726
            return positioned(leaf, filepos);
55✔
727
        }
59✔
728

729
        ++m_allow_macro_behavior;
105✔
730
        const auto value = nodeOrValue();
105✔
731
        --m_allow_macro_behavior;
103✔
732

733
        if (value.has_value())
103✔
734
            leaf->push_back(value.value());
99✔
735
        else if (leaf->list().size() == 2)  // the argument list is actually a function call and it's okay
4✔
736
        {
737
            leaf->list().back().attachCommentAfter(newlineOrComment());
4✔
738

739
            expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
4✔
740
            return positioned(leaf, filepos);
4✔
741
        }
742
        else
743
        {
744
            backtrack(position);
×
745
            errorWithNextToken(fmt::format("Expected a value while defining macro `{}'", symbol_name), context);
×
746
        }
747

748
        leaf->list().back().attachCommentAfter(newlineOrComment());
99✔
749

750
        expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
99✔
751
        return positioned(leaf, filepos);
99✔
752
    }
51,993✔
753

754
    std::optional<Node> Parser::functionCall(const FilePosition filepos)
51,803✔
755
    {
51,803✔
756
        auto context = generateErrorContextAtCurrentPosition();
51,803✔
757
        if (!accept(IsChar('(')))
51,803✔
758
            return std::nullopt;
29,164✔
759
        std::string comment = newlineOrComment();
22,639✔
760

761
        const auto func_name_pos = getCursor();
22,639✔
762
        std::optional<Node> func;
22,639✔
763
        if (auto sym_or_field = anyAtomOf({ NodeType::Symbol, NodeType::Field }); sym_or_field.has_value())
45,278✔
764
            func = sym_or_field->attachNearestCommentBefore(comment);
21,571✔
765
        else if (auto nested = node(); nested.has_value())
2,136✔
766
            func = nested->attachNearestCommentBefore(comment);
43✔
767
        else
768
            return std::nullopt;
1✔
769

770
        if (func.value().nodeType() == NodeType::Symbol && func.value().string() == "ref")
21,614✔
771
            error("`ref' can not be used outside a function's arguments list.", func_name_pos);
1✔
772

773
        std::optional<Node> leaf { NodeType::List };
21,613✔
774
        leaf->push_back(positioned(func.value(), func_name_pos));
21,613✔
775

776
        comment = newlineOrComment();
21,613✔
777

778
        while (!isEOF())
129,526✔
779
        {
780
            if (auto arg = nodeOrValue(); arg.has_value())
259,044✔
781
            {
782
                leaf->push_back(arg.value().attachNearestCommentBefore(comment));
107,913✔
783
                comment = newlineOrComment();
107,913✔
784
            }
107,913✔
785
            else
786
                break;
21,602✔
787
        }
788

789
        leaf->list().back().attachCommentAfter(comment);
21,606✔
790
        comment = newlineOrComment();
21,606✔
791
        if (!comment.empty())
21,606✔
792
            leaf->list().back().attachCommentAfter(comment);
×
793

794
        expectSuffixOrError(')', fmt::format("in function call to `{}'", func.value().repr()), context);
21,606✔
795
        return positioned(leaf, filepos);
21,602✔
796
    }
52,839✔
797

798
    std::optional<Node> Parser::list(const FilePosition filepos)
29,165✔
799
    {
29,165✔
800
        std::optional<Node> leaf { NodeType::List };
29,165✔
801

802
        auto context = generateErrorContextAtCurrentPosition();
29,165✔
803
        if (!accept(IsChar('[')))
29,165✔
804
            return std::nullopt;
27,483✔
805
        leaf->setAltSyntax(true);
1,682✔
806
        leaf->push_back(Node(NodeType::Symbol, "list"));
1,682✔
807

808
        std::string comment = newlineOrComment();
1,682✔
809
        leaf->attachNearestCommentBefore(comment);
1,682✔
810

811
        while (!isEOF())
4,055✔
812
        {
813
            if (auto value = nodeOrValue(); value.has_value())
8,108✔
814
            {
815
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
2,373✔
816
                comment = newlineOrComment();
2,373✔
817
            }
2,373✔
818
            else
819
                break;
1,681✔
820
        }
821
        leaf->list().back().attachCommentAfter(comment);
1,682✔
822

823
        expectSuffixOrError(']', "to end list definition", context);
1,682✔
824
        return positioned(leaf, filepos);
1,681✔
825
    }
29,166✔
826

827
    std::optional<Node> Parser::number(const FilePosition filepos)
194,376✔
828
    {
194,376✔
829
        std::string res;
194,376✔
830
        if (signedNumber(&res))
194,376✔
831
        {
832
            double output;
78,317✔
833
            if (Utils::isDouble(res, &output))
78,317✔
834
                return positioned(Node(output), filepos);
78,316✔
835

836
            error("Is not a valid number", filepos);
1✔
837
        }
78,317✔
838
        return std::nullopt;
116,059✔
839
    }
194,377✔
840

841
    std::optional<Node> Parser::string(const FilePosition filepos)
116,059✔
842
    {
116,059✔
843
        std::string res;
116,059✔
844
        if (accept(IsChar('"')))
116,059✔
845
        {
846
            while (true)
30,381✔
847
            {
848
                const auto pos = getCursor();
30,381✔
849

850
                if (accept(IsChar('\\')))
30,381✔
851
                {
852
                    if (m_mode != ParserMode::Interpret)
438✔
853
                        res += '\\';
28✔
854

855
                    if (accept(IsChar('"')))
438✔
856
                        res += '"';
61✔
857
                    else if (accept(IsChar('\\')))
377✔
858
                        res += '\\';
35✔
859
                    else if (accept(IsChar('n')))
342✔
860
                        res += m_mode == ParserMode::Interpret ? '\n' : 'n';
145✔
861
                    else if (accept(IsChar('t')))
197✔
862
                        res += m_mode == ParserMode::Interpret ? '\t' : 't';
94✔
863
                    else if (accept(IsChar('v')))
103✔
864
                        res += m_mode == ParserMode::Interpret ? '\v' : 'v';
2✔
865
                    else if (accept(IsChar('r')))
101✔
866
                        res += m_mode == ParserMode::Interpret ? '\r' : 'r';
82✔
867
                    else if (accept(IsChar('a')))
19✔
868
                        res += m_mode == ParserMode::Interpret ? '\a' : 'a';
2✔
869
                    else if (accept(IsChar('b')))
17✔
870
                        res += m_mode == ParserMode::Interpret ? '\b' : 'b';
2✔
871
                    else if (accept(IsChar('f')))
15✔
872
                        res += m_mode == ParserMode::Interpret ? '\f' : 'f';
2✔
873
                    else if (accept(IsChar('u')))
13✔
874
                    {
875
                        std::string seq;
5✔
876
                        if (hexNumber(4, &seq))
5✔
877
                        {
878
                            if (m_mode == ParserMode::Interpret)
4✔
879
                            {
880
                                char utf8_str[5];
2✔
881
                                utf8::decode(seq.c_str(), utf8_str);
2✔
882
                                if (*utf8_str == '\0')
2✔
883
                                    error("Invalid escape sequence", pos);
×
884
                                res += utf8_str;
2✔
885
                            }
2✔
886
                            else
887
                                res += "u" + seq;
2✔
888
                        }
4✔
889
                        else
890
                            error("Invalid escape sequence, expected 4 hex digits: \\uabcd", pos);
1✔
891
                    }
5✔
892
                    else if (accept(IsChar('U')))
8✔
893
                    {
894
                        std::string seq;
6✔
895
                        if (hexNumber(8, &seq))
6✔
896
                        {
897
                            if (m_mode == ParserMode::Interpret)
5✔
898
                            {
899
                                std::size_t begin = 0;
3✔
900
                                for (; seq[begin] == '0'; ++begin)
9✔
901
                                    ;
902
                                char utf8_str[5];
3✔
903
                                utf8::decode(seq.c_str() + begin, utf8_str);
3✔
904
                                if (*utf8_str == '\0')
3✔
905
                                    error("Invalid escape sequence", pos);
1✔
906
                                res += utf8_str;
2✔
907
                            }
3✔
908
                            else
909
                                res += "U" + seq;
2✔
910
                        }
4✔
911
                        else
912
                            error("Invalid escape sequence, expected 8 hex digits: \\UABCDEF78", pos);
1✔
913
                    }
6✔
914
                    else
915
                    {
916
                        backtrack(getCount() - 1);
2✔
917
                        error("Unknown escape sequence", pos);
2✔
918
                    }
919
                }
433✔
920
                else
921
                    accept(IsNot(IsEither(IsChar('\\'), IsChar('"'))), &res);
29,943✔
922

923
                if (accept(IsChar('"')))
30,376✔
924
                    break;
2,637✔
925
                if (isEOF())
27,739✔
926
                    expectSuffixOrError('"', "after string");
1✔
927
            }
30,381✔
928

929
            return positioned(Node(NodeType::String, res), filepos);
2,637✔
930
        }
931
        return std::nullopt;
113,416✔
932
    }
116,065✔
933

934
    std::optional<Node> Parser::field(const FilePosition filepos)
113,387✔
935
    {
113,387✔
936
        std::string sym;
113,387✔
937
        if (!name(&sym))
113,387✔
938
            return std::nullopt;
68,166✔
939

940
        std::optional<Node> leaf { Node(NodeType::Field) };
45,221✔
941
        leaf->push_back(Node(NodeType::Symbol, sym));
45,221✔
942

943
        while (true)
46,220✔
944
        {
945
            if (leaf->list().size() == 1 && !accept(IsChar('.')))  // Symbol:abc
46,220✔
946
                return std::nullopt;
44,250✔
947

948
            if (leaf->list().size() > 1 && !accept(IsChar('.')))
1,970✔
949
                break;
970✔
950

951
            const auto filepos_inner = getCursor();
1,000✔
952
            std::string res;
1,000✔
953
            if (!name(&res))
1,000✔
954
                errorWithNextToken("Expected a field name: <symbol>.<field>");
1✔
955
            leaf->push_back(positioned(Node(NodeType::Symbol, res), filepos_inner));
999✔
956
        }
1,000✔
957

958
        return positioned(leaf, filepos);
970✔
959
    }
113,388✔
960

961
    std::optional<Node> Parser::symbol(const FilePosition filepos)
112,416✔
962
    {
112,416✔
963
        std::string res;
112,416✔
964
        if (!name(&res))
112,416✔
965
            return std::nullopt;
68,166✔
966
        return positioned(Node(NodeType::Symbol, res), filepos);
44,250✔
967
    }
112,416✔
968

969
    std::optional<Node> Parser::spread(const FilePosition filepos)
113,416✔
970
    {
113,416✔
971
        std::string res;
113,416✔
972
        if (sequence("..."))
113,416✔
973
        {
974
            if (!name(&res))
29✔
975
                errorWithNextToken("Expected a name for the variadic");
1✔
976
            return positioned(Node(NodeType::Spread, res), filepos);
28✔
977
        }
978
        return std::nullopt;
113,387✔
979
    }
113,417✔
980

981
    std::optional<Node> Parser::nil(const FilePosition filepos)
68,166✔
982
    {
68,166✔
983
        if (!accept(IsChar('(')))
68,166✔
984
            return std::nullopt;
32,553✔
985

986
        const std::string comment = newlineOrComment();
35,613✔
987
        if (!accept(IsChar(')')))
35,613✔
988
            return std::nullopt;
35,511✔
989

990
        if (m_mode == ParserMode::Interpret)
102✔
991
            return positioned(Node(NodeType::Symbol, "nil").attachNearestCommentBefore(comment), filepos);
92✔
992
        return positioned(Node(NodeType::List).attachNearestCommentBefore(comment), filepos);
10✔
993
    }
68,166✔
994

995
    std::optional<Node> Parser::atom()
194,375✔
996
    {
194,375✔
997
        const auto pos = getCount();
194,375✔
998
        const auto filepos = getCursor();
194,375✔
999

1000
        if (auto res = Parser::number(filepos); res.has_value())
194,375✔
1001
            return res;
78,316✔
1002
        backtrack(pos);
116,053✔
1003

1004
        if (auto res = Parser::string(filepos); res.has_value())
116,053✔
1005
            return res;
2,637✔
1006
        backtrack(pos);
113,415✔
1007

1008
        if (auto res = Parser::spread(filepos); m_allow_macro_behavior > 0 && res.has_value())
113,415✔
1009
            return res;
28✔
1010
        backtrack(pos);
113,386✔
1011

1012
        if (auto res = Parser::field(filepos); res.has_value())
113,386✔
1013
            return res;
970✔
1014
        backtrack(pos);
112,416✔
1015

1016
        if (auto res = Parser::symbol(filepos); res.has_value())
112,416✔
1017
            return res;
44,250✔
1018
        backtrack(pos);
68,166✔
1019

1020
        if (auto res = Parser::nil(filepos); res.has_value())
68,166✔
1021
            return res;
102✔
1022
        backtrack(pos);
68,064✔
1023

1024
        return std::nullopt;
68,064✔
1025
    }
194,375✔
1026

1027
    std::optional<Node> Parser::anyAtomOf(const std::initializer_list<NodeType> types)
22,639✔
1028
    {
22,639✔
1029
        if (auto value = atom(); value.has_value())
44,211✔
1030
        {
1031
            for (const auto type : types)
43,239✔
1032
            {
1033
                if (value->nodeType() == type)
21,667✔
1034
                    return value;
21,571✔
1035
            }
21,667✔
1036
        }
1✔
1037
        return std::nullopt;
1,068✔
1038
    }
22,639✔
1039

1040
    std::optional<Node> Parser::nodeOrValue()
171,728✔
1041
    {
171,728✔
1042
        if (auto value = atom(); value.has_value())
171,728✔
1043
            return value;
104,731✔
1044
        if (auto sub_node = node(); sub_node.has_value())
66,987✔
1045
            return sub_node;
39,511✔
1046

1047
        return std::nullopt;
27,476✔
1048
    }
171,728✔
1049

1050
    std::optional<Node> Parser::wrapped(std::optional<Node> (Parser::*parser)(FilePosition), const std::string& name)
355,514✔
1051
    {
355,514✔
1052
        const auto cursor = getCursor();
355,514✔
1053
        auto context = generateErrorContextAtCurrentPosition();
355,514✔
1054
        if (!prefix('('))
355,514✔
1055
            return std::nullopt;
188,648✔
1056

1057
        const std::string comment = newlineOrComment();
166,866✔
1058

1059
        if (auto result = (this->*parser)(cursor); result.has_value())
184,347✔
1060
        {
1061
            result->attachNearestCommentBefore(result->comment() + comment);
17,481✔
1062
            result.value().attachCommentAfter(newlineOrComment());
17,481✔
1063

1064
            if (name == "function")
17,481✔
1065
                expectSuffixOrError(')', "after function body. Did you forget to wrap the body with `{}'?", context);
3,884✔
1066
            else
1067
                expectSuffixOrError(')', "after " + name, context);
13,597✔
1068

1069
            result.value().attachCommentAfter(spaceComment());
17,479✔
1070
            return result;
17,479✔
1071
        }
1072

1073
        return std::nullopt;
149,362✔
1074
    }
355,516✔
1075
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc