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

ArkScript-lang / Ark / 15272867184

27 May 2025 10:20AM UTC coverage: 86.626% (-0.1%) from 86.737%
15272867184

push

github

SuperFola
fix(vm): check for negative index when accessing elements in lists

7 of 7 new or added lines in 1 file covered. (100.0%)

20 existing lines in 2 files now uncovered.

7196 of 8307 relevant lines covered (86.63%)

91500.31 hits per line

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

96.87
/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 bool interpret) :
922✔
8
        BaseParser(), m_interpret(interpret), m_logger("Parser", debug),
461✔
9
        m_ast(NodeType::List), m_imports({}), m_allow_macro_behavior(0),
461✔
10
        m_nested_nodes(0)
461✔
11
    {
461✔
12
        m_ast.push_back(Node(Keyword::Begin));
461✔
13

14
        m_parsers = {
922✔
15
            [this]() {
31,983✔
16
                return wrapped(&Parser::letMutSet, "variable assignment or declaration");
31,522✔
17
            },
5✔
18
            [this]() {
27,709✔
19
                return wrapped(&Parser::function, "function");
27,248✔
20
            },
4✔
21
            [this]() {
26,242✔
22
                return wrapped(&Parser::condition, "condition");
25,781✔
23
            },
3✔
24
            [this]() {
25,496✔
25
                return wrapped(&Parser::loop, "loop");
25,035✔
26
            },
2✔
27
            [this]() {
24,896✔
28
                return import_();
24,435✔
29
            },
30
            [this]() {
24,731✔
31
                return block();
24,270✔
32
            },
33
            [this]() {
23,217✔
34
                return wrapped(&Parser::macroCondition, "$if");
22,756✔
35
            },
2✔
36
            [this]() {
23,166✔
37
                return macro();
22,705✔
38
            },
39
            [this]() {
23,017✔
40
                return wrapped(&Parser::del, "del");
22,556✔
41
            },
1✔
42
            [this]() {
23,006✔
43
                return functionCall();
22,545✔
44
            },
45
            [this]() {
12,987✔
46
                return list();
12,526✔
47
            }
48
        };
49
    }
461✔
50

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

56
        while (!isEOF())
2,597✔
57
        {
58
            std::string comment;
2,558✔
59
            newlineOrComment(&comment);
2,558✔
60
            if (isEOF())
2,558✔
61
            {
62
                if (!comment.empty())
379✔
63
                    m_ast.list().back().attachCommentAfter(comment);
2✔
64
                break;
379✔
65
            }
66

67
            const auto pos = getCount();
2,179✔
68
            if (auto n = node())
4,358✔
69
            {
70
                m_ast.push_back(n->attachNearestCommentBefore(n->comment() + comment));
2,136✔
71
                comment.clear();
2,136✔
72
                if (spaceComment(&comment))
2,136✔
73
                    m_ast.list().back().attachCommentAfter(comment);
21✔
74
            }
2,136✔
75
            else
76
            {
77
                backtrack(pos);
4✔
78
                std::string out = peek();
4✔
79
                std::string message;
4✔
80
                if (out == ")")
4✔
81
                    message = "Unexpected closing paren";
1✔
82
                else if (out == "}")
3✔
83
                    message = "Unexpected closing bracket";
1✔
84
                else if (out == "]")
2✔
85
                    message = "Unexpected closing square bracket";
1✔
86
                else
87
                    errorWithNextToken("invalid syntax, expected node");
1✔
88
                errorWithNextToken(message);
3✔
89
            }
4✔
90
        }
2,558✔
91

92
        m_logger.traceEnd();
418✔
93
    }
461✔
94

95
    const Node& Parser::ast() const noexcept
432✔
96
    {
432✔
97
        return m_ast;
432✔
98
    }
99

100
    const std::vector<Import>& Parser::imports() const
364✔
101
    {
364✔
102
        return m_imports;
364✔
103
    }
104

105
    Node& Parser::setNodePosAndFilename(Node& node, const std::optional<FilePosition>& cursor) const
323,769✔
106
    {
323,769✔
107
        if (node.line() != 0 || node.col() != 0)
323,769✔
108
            return node;
45,184✔
109

110
        const auto [row, col] = cursor.value_or(getCursor());
278,585✔
111
        node.setPos(row, col);
278,585✔
112
        node.setFilename(m_filename);
278,585✔
113
        return node;
278,585✔
114
    }
323,769✔
115

116
    std::optional<Node> Parser::node()
31,523✔
117
    {
31,523✔
118
        ++m_nested_nodes;
31,523✔
119

120
        if (m_nested_nodes > MaxNestedNodes)
31,523✔
121
            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,066✔
122

123
        // save current position in buffer to be able to go back if needed
124
        const auto position = getCount();
31,522✔
125
        std::optional<Node> result = std::nullopt;
31,522✔
126

127
        for (auto&& parser : m_parsers)
292,901✔
128
        {
129
            result = parser();
261,379✔
130

131
            if (result)
260,314✔
132
                break;
18,808✔
133
            backtrack(position);
241,506✔
134
        }
261,379✔
135

136
        // return std::nullopt only on parsing error, nothing matched, the user provided terrible code
137
        --m_nested_nodes;
30,457✔
138
        return result;
30,457✔
139
    }
31,523✔
140

141
    std::optional<Node> Parser::letMutSet()
17,490✔
142
    {
17,490✔
143
        std::optional<Node> leaf { NodeType::List };
17,490✔
144
        setNodePosAndFilename(leaf.value());
17,490✔
145

146
        std::string token;
17,490✔
147
        if (!oneOf({ "let", "mut", "set" }, &token))
17,490✔
148
            return std::nullopt;
13,216✔
149
        std::string comment;
4,274✔
150
        newlineOrComment(&comment);
4,274✔
151
        leaf->attachNearestCommentBefore(comment);
4,274✔
152

153
        if (token == "let")
4,274✔
154
            leaf->push_back(Node(Keyword::Let));
1,826✔
155
        else if (token == "mut")
2,448✔
156
            leaf->push_back(Node(Keyword::Mut));
1,373✔
157
        else  // "set"
158
            leaf->push_back(Node(Keyword::Set));
1,075✔
159

160
        if (m_allow_macro_behavior > 0)
4,274✔
161
        {
162
            const auto position = getCount();
22✔
163
            if (const auto value = nodeOrValue(); value.has_value())
44✔
164
            {
165
                const auto sym = value.value();
22✔
166
                if (sym.nodeType() == NodeType::List || sym.nodeType() == NodeType::Symbol || sym.nodeType() == NodeType::Macro || sym.nodeType() == NodeType::Spread)
22✔
167
                    leaf->push_back(sym);
21✔
168
                else
169
                    error(fmt::format("Can not use a {} as a symbol name, even in a macro", nodeTypes[static_cast<std::size_t>(sym.nodeType())]), sym.repr());
1✔
170
            }
22✔
171
            else
172
                backtrack(position);
×
173
        }
22✔
174

175
        if (leaf->constList().size() == 1)
4,273✔
176
        {
177
            // we haven't parsed anything while in "macro state"
178
            std::string symbol_name;
4,252✔
179
            if (!name(&symbol_name))
4,252✔
180
                errorWithNextToken(token + " needs a symbol");
2✔
181

182
            leaf->push_back(Node(NodeType::Symbol, symbol_name));
4,250✔
183
        }
4,252✔
184

185
        comment.clear();
4,271✔
186
        newlineOrComment(&comment);
4,271✔
187

188
        if (auto value = nodeOrValue(); value.has_value())
8,542✔
189
            leaf->push_back(value.value().attachNearestCommentBefore(comment));
4,269✔
190
        else
191
            errorWithNextToken("Expected a value");
×
192

193
        return leaf;
4,269✔
194
    }
17,495✔
195

196
    std::optional<Node> Parser::del()
10,030✔
197
    {
10,030✔
198
        std::optional<Node> leaf { NodeType::List };
10,030✔
199
        setNodePosAndFilename(leaf.value());
10,030✔
200

201
        if (!oneOf({ "del" }))
10,030✔
202
            return std::nullopt;
10,019✔
203
        leaf->push_back(Node(Keyword::Del));
11✔
204

205
        std::string comment;
11✔
206
        newlineOrComment(&comment);
11✔
207

208
        std::string symbol_name;
11✔
209
        if (!name(&symbol_name))
11✔
210
            errorWithNextToken("del needs a symbol");
1✔
211

212
        leaf->push_back(Node(NodeType::Symbol, symbol_name));
10✔
213
        leaf->list().back().attachNearestCommentBefore(comment);
10✔
214
        setNodePosAndFilename(leaf->list().back());
10✔
215

216
        return leaf;
10✔
217
    }
10,031✔
218

219
    std::optional<Node> Parser::condition()
11,749✔
220
    {
11,749✔
221
        std::optional<Node> leaf { NodeType::List };
11,749✔
222
        setNodePosAndFilename(leaf.value());
11,749✔
223

224
        if (!oneOf({ "if" }))
11,749✔
225
            return std::nullopt;
11,003✔
226

227
        std::string comment;
746✔
228
        newlineOrComment(&comment);
746✔
229

230
        leaf->push_back(Node(Keyword::If));
746✔
231

232
        if (auto cond_expr = nodeOrValue(); cond_expr.has_value())
1,492✔
233
            leaf->push_back(cond_expr.value().attachNearestCommentBefore(comment));
745✔
234
        else
235
            errorWithNextToken("`if' needs a valid condition");
1✔
236

237
        comment.clear();
745✔
238
        newlineOrComment(&comment);
745✔
239

240
        if (auto value_if_true = nodeOrValue(); value_if_true.has_value())
1,490✔
241
            leaf->push_back(value_if_true.value().attachNearestCommentBefore(comment));
744✔
242
        else
243
            errorWithNextToken("Expected a node or value after condition");
1✔
244

245
        comment.clear();
744✔
246
        newlineOrComment(&comment);
744✔
247

248
        if (auto value_if_false = nodeOrValue(); value_if_false.has_value())
1,488✔
249
        {
250
            leaf->push_back(value_if_false.value().attachNearestCommentBefore(comment));
498✔
251
            comment.clear();
498✔
252
            if (newlineOrComment(&comment))
498✔
253
                leaf->list().back().attachCommentAfter(comment);
2✔
254
        }
498✔
255
        else if (!comment.empty())
246✔
256
            leaf->attachCommentAfter(comment);
2✔
257

258
        setNodePosAndFilename(leaf->list().back());
744✔
259
        return leaf;
744✔
260
    }
11,751✔
261

262
    std::optional<Node> Parser::loop()
11,003✔
263
    {
11,003✔
264
        std::optional<Node> leaf { NodeType::List };
11,003✔
265
        setNodePosAndFilename(leaf.value());
11,003✔
266

267
        if (!oneOf({ "while" }))
11,003✔
268
            return std::nullopt;
10,403✔
269

270
        std::string comment;
600✔
271
        newlineOrComment(&comment);
600✔
272

273
        leaf->push_back(Node(Keyword::While));
600✔
274

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

280
        comment.clear();
599✔
281
        newlineOrComment(&comment);
599✔
282

283
        if (auto body = nodeOrValue(); body.has_value())
1,198✔
284
            leaf->push_back(body.value().attachNearestCommentBefore(comment));
598✔
285
        else
286
            errorWithNextToken("Expected a node or value after loop condition");
1✔
287

288
        setNodePosAndFilename(leaf->list().back());
598✔
289
        return leaf;
598✔
290
    }
11,005✔
291

292
    std::optional<Node> Parser::import_()
24,435✔
293
    {
24,435✔
294
        std::optional<Node> leaf { NodeType::List };
24,435✔
295
        setNodePosAndFilename(leaf.value());
24,435✔
296

297
        auto context = generateErrorContext("(");
24,435✔
298
        if (!accept(IsChar('(')))
24,435✔
299
            return std::nullopt;
14,032✔
300

301
        std::string comment;
10,403✔
302
        newlineOrComment(&comment);
10,403✔
303
        leaf->attachNearestCommentBefore(comment);
10,403✔
304

305
        if (!oneOf({ "import" }))
10,403✔
306
            return std::nullopt;
10,238✔
307
        comment.clear();
165✔
308
        newlineOrComment(&comment);
165✔
309
        leaf->push_back(Node(Keyword::Import));
165✔
310

311
        Import import_data;
165✔
312

313
        const auto pos = getCount();
165✔
314
        if (!packageName(&import_data.prefix))
165✔
315
            errorWithNextToken("Import expected a package name");
1✔
316

317
        if (import_data.prefix.size() > 255)
164✔
318
        {
319
            backtrack(pos);
1✔
320
            errorWithNextToken(fmt::format("Import name too long, expected at most 255 characters, got {}", import_data.prefix.size()));
1✔
321
        }
×
322
        import_data.package.push_back(import_data.prefix);
163✔
323

324
        const auto [row, col] = getCursor();
163✔
325
        import_data.col = col;
163✔
326
        import_data.line = row;
163✔
327

328
        Node packageNode(NodeType::List);
163✔
329
        setNodePosAndFilename(packageNode.attachNearestCommentBefore(comment));
163✔
330
        packageNode.push_back(Node(NodeType::Symbol, import_data.prefix));
163✔
331

332
        // first, parse the package name
333
        while (!isEOF())
285✔
334
        {
335
            // parsing package folder.foo.bar.yes
336
            if (accept(IsChar('.')))
285✔
337
            {
338
                std::string path;
125✔
339
                if (!packageName(&path))
125✔
340
                    errorWithNextToken("Package name expected after '.'");
2✔
341
                else
342
                {
343
                    packageNode.push_back(Node(NodeType::Symbol, path));
123✔
344
                    setNodePosAndFilename(packageNode.list().back());
123✔
345
                    import_data.package.push_back(path);
123✔
346
                    import_data.prefix = path;  // in the end we will store the last element of the package, which is what we want
123✔
347

348
                    if (path.size() > 255)
123✔
349
                    {
350
                        backtrack(pos);
1✔
351
                        errorWithNextToken(fmt::format("Import name too long, expected at most 255 characters, got {}", path.size()));
1✔
352
                    }
×
353
                }
354
            }
125✔
355
            else if (accept(IsChar(':')) && accept(IsChar('*')))  // parsing :*
160✔
356
            {
357
                leaf->push_back(packageNode);
10✔
358
                leaf->push_back(Node(NodeType::Symbol, "*"));
10✔
359
                setNodePosAndFilename(leaf->list().back());
10✔
360

361
                space();
10✔
362
                expectSuffixOrError(')', fmt::format("in import `{}'", import_data.toPackageString()), context);
10✔
363

364
                // save the import data structure to know we encounter an import node, and retrieve its data more easily later on
365
                import_data.with_prefix = false;
10✔
366
                import_data.is_glob = true;
10✔
367
                m_imports.push_back(import_data);
10✔
368

369
                return leaf;
10✔
370
            }
371
            else
372
                break;
150✔
373
        }
374

375
        Node symbols(NodeType::List);
150✔
376
        setNodePosAndFilename(symbols);
150✔
377
        // then parse the symbols to import, if any
378
        if (space())
150✔
379
        {
380
            comment.clear();
68✔
381
            newlineOrComment(&comment);
68✔
382

383
            while (!isEOF())
106✔
384
            {
385
                if (accept(IsChar(':')))  // parsing potential :a :b :c
106✔
386
                {
387
                    std::string symbol_name;
103✔
388
                    if (!name(&symbol_name))
103✔
389
                        errorWithNextToken("Expected a valid symbol to import");
1✔
390
                    if (symbol_name == "*")
102✔
391
                        error(fmt::format("Glob patterns can not be separated from the package, use (import {}:*) instead", import_data.toPackageString()), symbol_name);
1✔
392

393
                    if (symbol_name.size() >= 2 && symbol_name[symbol_name.size() - 2] == ':' && symbol_name.back() == '*')
101✔
394
                    {
395
                        backtrack(getCount() - 2);  // we can backtrack n-2 safely here because we know the previous chars were ":*"
1✔
396
                        error("Glob pattern can not follow a symbol to import", ":*");
1✔
397
                    }
×
398

399
                    symbols.push_back(Node(NodeType::Symbol, symbol_name).attachNearestCommentBefore(comment));
100✔
400
                    comment.clear();
100✔
401
                    setNodePosAndFilename(symbols.list().back());
100✔
402
                    import_data.symbols.push_back(symbol_name);
100✔
403
                    // we do not need the prefix when importing specific symbols
404
                    import_data.with_prefix = false;
100✔
405
                }
103✔
406

407
                if (!space())
103✔
408
                    break;
65✔
409
                comment.clear();
38✔
410
                newlineOrComment(&comment);
38✔
411
            }
412

413
            if (!comment.empty() && !symbols.list().empty())
65✔
414
                symbols.list().back().attachCommentAfter(comment);
2✔
415
        }
65✔
416

417
        leaf->push_back(packageNode);
147✔
418
        leaf->push_back(symbols);
147✔
419
        // save the import data
420
        m_imports.push_back(import_data);
147✔
421

422
        comment.clear();
147✔
423
        if (newlineOrComment(&comment))
147✔
424
            leaf->list().back().attachCommentAfter(comment);
1✔
425

426
        expectSuffixOrError(')', fmt::format("in import `{}'", import_data.toPackageString()), context);
147✔
427
        return leaf;
147✔
428
    }
24,443✔
429

430
    std::optional<Node> Parser::block()
24,270✔
431
    {
24,270✔
432
        std::optional<Node> leaf { NodeType::List };
24,270✔
433
        setNodePosAndFilename(leaf.value());
24,270✔
434

435
        auto context = generateErrorContext("(");
24,270✔
436
        bool alt_syntax = false;
24,270✔
437
        std::string comment;
24,270✔
438
        if (accept(IsChar('(')))
24,270✔
439
        {
440
            newlineOrComment(&comment);
10,238✔
441
            if (!oneOf({ "begin" }))
10,238✔
442
                return std::nullopt;
10,230✔
443
        }
8✔
444
        else if (accept(IsChar('{')))
14,032✔
445
            alt_syntax = true;
1,506✔
446
        else
447
            return std::nullopt;
12,526✔
448

449
        leaf->push_back(Node(Keyword::Begin).attachNearestCommentBefore(comment));
1,514✔
450

451
        comment.clear();
1,514✔
452
        newlineOrComment(&comment);
1,514✔
453

454
        while (!isEOF())
6,969✔
455
        {
456
            if (auto value = nodeOrValue(); value.has_value())
13,936✔
457
            {
458
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
5,455✔
459
                comment.clear();
5,455✔
460
                newlineOrComment(&comment);
5,455✔
461
            }
5,455✔
462
            else
463
                break;
1,513✔
464
        }
465

466
        newlineOrComment(&comment);
1,514✔
467
        expectSuffixOrError(alt_syntax ? '}' : ')', "to close block", context);
1,514✔
468
        setNodePosAndFilename(leaf->list().back());
1,513✔
469
        leaf->list().back().attachCommentAfter(comment);
1,513✔
470
        return leaf;
1,513✔
471
    }
24,271✔
472

473
    std::optional<Node> Parser::functionArgs()
1,446✔
474
    {
1,446✔
475
        expect(IsChar('('));
1,447✔
476
        std::optional<Node> args { NodeType::List };
1,446✔
477
        setNodePosAndFilename(args.value());
1,446✔
478

479
        std::string comment;
1,446✔
480
        newlineOrComment(&comment);
1,446✔
481
        args->attachNearestCommentBefore(comment);
1,446✔
482

483
        bool has_captures = false;
1,446✔
484

485
        while (!isEOF())
3,917✔
486
        {
487
            const auto pos = getCursor();
3,916✔
488
            if (accept(IsChar('&')))  // captures
3,916✔
489
            {
490
                has_captures = true;
214✔
491
                std::string capture;
214✔
492
                if (!name(&capture))
214✔
493
                    break;
×
494
                Node capture_node = Node(NodeType::Capture, capture).attachNearestCommentBefore(comment);
214✔
495
                setNodePosAndFilename(capture_node, pos);
214✔
496
                args->push_back(capture_node);
214✔
497
            }
214✔
498
            else
499
            {
500
                const auto count = getCount();
3,702✔
501
                std::string symbol_name;
3,702✔
502
                if (!name(&symbol_name))
3,702✔
503
                    break;
1,444✔
504
                if (has_captures)
2,258✔
505
                {
506
                    backtrack(count);
1✔
507
                    error("Captured variables should be at the end of the argument list", symbol_name);
1✔
508
                }
×
509

510
                Node arg_node = Node(NodeType::Symbol, symbol_name).attachNearestCommentBefore(comment);
2,257✔
511
                setNodePosAndFilename(arg_node, pos);
2,257✔
512
                args->push_back(arg_node);
2,257✔
513
            }
3,702✔
514

515
            comment.clear();
2,471✔
516
            newlineOrComment(&comment);
2,471✔
517
        }
3,916✔
518

519
        if (accept(IsChar(')')))
1,445✔
520
            return args;
1,443✔
521
        return std::nullopt;
2✔
522
    }
1,447✔
523

524
    std::optional<Node> Parser::function()
13,216✔
525
    {
13,216✔
526
        std::optional<Node> leaf { NodeType::List };
13,216✔
527
        setNodePosAndFilename(leaf.value());
13,216✔
528

529
        if (!oneOf({ "fun" }))
13,216✔
530
            return std::nullopt;
11,749✔
531
        leaf->push_back(Node(Keyword::Fun));
1,467✔
532

533
        std::string comment_before_args;
1,467✔
534
        newlineOrComment(&comment_before_args);
1,467✔
535

536
        while (m_allow_macro_behavior > 0)
1,467✔
537
        {
538
            const auto position = getCount();
21✔
539

540
            // args
541
            if (const auto value = nodeOrValue(); value.has_value())
42✔
542
            {
543
                // if value is nil, just add an empty argument bloc to prevent bugs when
544
                // declaring functions inside macros
545
                Node args = value.value();
21✔
546
                setNodePosAndFilename(args);
21✔
547
                if (args.nodeType() == NodeType::Symbol && args.string() == "nil")
21✔
548
                    leaf->push_back(Node(NodeType::List));
5✔
549
                else
550
                    leaf->push_back(args);
16✔
551
            }
21✔
552
            else
553
            {
554
                backtrack(position);
×
555
                break;
×
556
            }
557

558
            std::string comment;
21✔
559
            newlineOrComment(&comment);
21✔
560
            // body
561
            if (auto value = nodeOrValue(); value.has_value())
42✔
562
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
21✔
563
            else
564
                errorWithNextToken("Expected a body for the function");
×
565
            setNodePosAndFilename(leaf->list().back());
21✔
566
            return leaf;
21✔
567
        }
21✔
568

569
        const auto position = getCount();
1,446✔
570
        if (auto args = functionArgs(); args.has_value())
2,892✔
571
            leaf->push_back(args.value().attachNearestCommentBefore(comment_before_args));
1,443✔
572
        else
573
        {
574
            backtrack(position);
2✔
575

576
            if (auto value = nodeOrValue(); value.has_value())
4✔
577
                leaf->push_back(value.value().attachNearestCommentBefore(comment_before_args));
1✔
578
            else
579
                errorWithNextToken("Expected an argument list");
×
580
        }
581

582
        std::string comment;
1,444✔
583
        newlineOrComment(&comment);
1,444✔
584

585
        if (auto value = nodeOrValue(); value.has_value())
2,888✔
586
            leaf->push_back(value.value().attachNearestCommentBefore(comment));
1,442✔
587
        else
588
            errorWithNextToken("Expected a body for the function");
2✔
589

590
        setNodePosAndFilename(leaf->list().back());
1,442✔
591
        return leaf;
1,442✔
592
    }
13,220✔
593

594
    std::optional<Node> Parser::macroCondition()
10,230✔
595
    {
10,230✔
596
        std::optional<Node> leaf { NodeType::Macro };
10,230✔
597
        setNodePosAndFilename(leaf.value());
10,230✔
598

599
        if (!oneOf({ "$if" }))
10,230✔
600
            return std::nullopt;
10,179✔
601
        leaf->push_back(Node(Keyword::If));
51✔
602

603
        std::string comment;
51✔
604
        newlineOrComment(&comment);
51✔
605
        leaf->attachNearestCommentBefore(comment);
51✔
606

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

612
        comment.clear();
50✔
613
        newlineOrComment(&comment);
50✔
614

615
        if (auto value_if_true = nodeOrValue(); value_if_true.has_value())
100✔
616
            leaf->push_back(value_if_true.value().attachNearestCommentBefore(comment));
49✔
617
        else
618
            errorWithNextToken("Expected a node or value after condition");
1✔
619

620
        comment.clear();
49✔
621
        newlineOrComment(&comment);
49✔
622

623
        if (auto value_if_false = nodeOrValue(); value_if_false.has_value())
88✔
624
        {
625
            leaf->push_back(value_if_false.value().attachNearestCommentBefore(comment));
39✔
626
            comment.clear();
39✔
627
            newlineOrComment(&comment);
39✔
628
            leaf->list().back().attachCommentAfter(comment);
39✔
629
        }
39✔
630

631
        setNodePosAndFilename(leaf->list().back());
49✔
632
        return leaf;
49✔
633
    }
10,232✔
634

635
    std::optional<Node> Parser::macroArgs()
148✔
636
    {
148✔
637
        if (!accept(IsChar('(')))
151✔
638
            return std::nullopt;
23✔
639

640
        std::optional<Node> args { NodeType::List };
125✔
641
        setNodePosAndFilename(args.value());
125✔
642

643
        std::string comment;
125✔
644
        newlineOrComment(&comment);
125✔
645
        args->attachNearestCommentBefore(comment);
125✔
646

647
        std::vector<std::string> names;
125✔
648
        while (!isEOF())
304✔
649
        {
650
            const auto pos = getCount();
303✔
651

652
            std::string arg_name;
303✔
653
            if (!name(&arg_name))
303✔
654
                break;
123✔
655
            comment.clear();
180✔
656
            newlineOrComment(&comment);
180✔
657
            args->push_back(Node(NodeType::Symbol, arg_name).attachNearestCommentBefore(comment));
180✔
658

659
            if (std::ranges::find(names, arg_name) != names.end())
180✔
660
            {
661
                backtrack(pos);
1✔
662
                errorWithNextToken(fmt::format("Argument names must be unique, can not reuse `{}'", arg_name));
1✔
663
            }
×
664
            names.push_back(arg_name);
179✔
665
        }
303✔
666

667
        const auto pos = getCount();
124✔
668
        if (sequence("..."))
124✔
669
        {
670
            std::string spread_name;
45✔
671
            if (!name(&spread_name))
45✔
672
                errorWithNextToken("Expected a name for the variadic arguments list");
1✔
673
            args->push_back(Node(NodeType::Spread, spread_name));
44✔
674

675
            comment.clear();
44✔
676
            if (newlineOrComment(&comment))
44✔
677
                args->list().back().attachCommentAfter(comment);
2✔
678

679
            if (std::ranges::find(names, spread_name) != names.end())
44✔
680
            {
681
                backtrack(pos);
1✔
682
                errorWithNextToken(fmt::format("Argument names must be unique, can not reuse `{}'", spread_name));
1✔
683
            }
×
684
        }
45✔
685

686
        if (!accept(IsChar(')')))
122✔
687
            return std::nullopt;
32✔
688
        comment.clear();
90✔
689
        if (newlineOrComment(&comment))
90✔
690
        {
691
            if (args->list().empty())
2✔
692
                args->attachCommentAfter(comment);
2✔
693
            else
694
                args->list().back().attachCommentAfter(comment);
×
695
        }
2✔
696

697
        return args;
90✔
698
    }
151✔
699

700
    std::optional<Node> Parser::macro()
22,705✔
701
    {
22,705✔
702
        std::optional<Node> leaf { NodeType::Macro };
22,705✔
703
        setNodePosAndFilename(leaf.value());
22,705✔
704

705
        auto context = generateErrorContext("(");
22,705✔
706
        if (!accept(IsChar('(')))
22,705✔
707
            return std::nullopt;
12,526✔
708
        std::string comment;
10,179✔
709
        newlineOrComment(&comment);
10,179✔
710

711
        if (!oneOf({ "macro" }))
10,179✔
712
            return std::nullopt;
10,030✔
713
        newlineOrComment(&comment);
149✔
714
        leaf->attachNearestCommentBefore(comment);
149✔
715

716
        std::string symbol_name;
149✔
717
        if (!name(&symbol_name))
149✔
718
            errorWithNextToken("Expected a symbol to declare a macro");
1✔
719
        comment.clear();
148✔
720
        newlineOrComment(&comment);
148✔
721

722
        leaf->push_back(Node(NodeType::Symbol, symbol_name).attachNearestCommentBefore(comment));
148✔
723

724
        const auto position = getCount();
148✔
725
        if (const auto args = macroArgs(); args.has_value())
296✔
726
            leaf->push_back(args.value());
90✔
727
        else
728
        {
729
            // if we couldn't parse arguments, then we have a value
730
            backtrack(position);
55✔
731

732
            ++m_allow_macro_behavior;
55✔
733
            const auto value = nodeOrValue();
55✔
734
            --m_allow_macro_behavior;
54✔
735

736
            if (value.has_value())
54✔
737
                leaf->push_back(value.value());
53✔
738
            else
739
                errorWithNextToken(fmt::format("Expected an argument list, atom or node while defining macro `{}'", symbol_name));
1✔
740

741
            setNodePosAndFilename(leaf->list().back());
53✔
742
            comment.clear();
53✔
743
            if (newlineOrComment(&comment))
53✔
UNCOV
744
                leaf->list().back().attachCommentAfter(comment);
×
745
            expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
53✔
746
            return leaf;
52✔
747
        }
55✔
748

749
        ++m_allow_macro_behavior;
90✔
750
        const auto value = nodeOrValue();
90✔
751
        --m_allow_macro_behavior;
89✔
752

753
        if (value.has_value())
89✔
754
            leaf->push_back(value.value());
87✔
755
        else if (leaf->list().size() == 2)  // the argument list is actually a function call and it's okay
2✔
756
        {
757
            setNodePosAndFilename(leaf->list().back());
2✔
758
            comment.clear();
2✔
759
            if (newlineOrComment(&comment))
2✔
UNCOV
760
                leaf->list().back().attachCommentAfter(comment);
×
761

762
            expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
2✔
763
            return leaf;
2✔
764
        }
765
        else
766
        {
UNCOV
767
            backtrack(position);
×
UNCOV
768
            errorWithNextToken(fmt::format("Expected a value while defining macro `{}'", symbol_name), context);
×
769
        }
770

771
        setNodePosAndFilename(leaf->list().back());
87✔
772
        comment.clear();
87✔
773
        if (newlineOrComment(&comment))
87✔
774
            leaf->list().back().attachCommentAfter(comment);
3✔
775

776
        expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
87✔
777
        return leaf;
87✔
778
    }
22,712✔
779

780
    std::optional<Node> Parser::functionCall()
22,545✔
781
    {
22,545✔
782
        auto context = generateErrorContext("(");
22,545✔
783
        if (!accept(IsChar('(')))
22,545✔
784
            return std::nullopt;
12,526✔
785
        std::string comment;
10,019✔
786
        newlineOrComment(&comment);
10,019✔
787
        auto cursor = getCursor();
10,019✔
788

789
        std::optional<Node> func;
10,019✔
790
        if (auto sym_or_field = anyAtomOf({ NodeType::Symbol, NodeType::Field }); sym_or_field.has_value())
20,038✔
791
            func = sym_or_field->attachNearestCommentBefore(comment);
8,973✔
792
        else if (auto nested = node(); nested.has_value())
2,092✔
793
            func = nested->attachNearestCommentBefore(comment);
22✔
794
        else
UNCOV
795
            return std::nullopt;
×
796
        comment.clear();
8,995✔
797
        newlineOrComment(&comment);
8,995✔
798

799
        std::optional<Node> leaf { NodeType::List };
8,995✔
800
        setNodePosAndFilename(leaf.value(), cursor);
8,995✔
801
        setNodePosAndFilename(func.value(), cursor);
8,995✔
802
        leaf->push_back(func.value());
8,995✔
803

804
        while (!isEOF())
90,419✔
805
        {
806
            if (auto arg = nodeOrValue(); arg.has_value())
180,830✔
807
            {
808
                leaf->push_back(arg.value().attachNearestCommentBefore(comment));
81,424✔
809
                comment.clear();
81,424✔
810
                newlineOrComment(&comment);
81,424✔
811
            }
81,424✔
812
            else
813
                break;
8,989✔
814
        }
815

816
        leaf->list().back().attachCommentAfter(comment);
8,993✔
817

818
        comment.clear();
8,993✔
819
        if (newlineOrComment(&comment))
8,993✔
UNCOV
820
            leaf->list().back().attachCommentAfter(comment);
×
821

822
        expectSuffixOrError(')', fmt::format("in function call to `{}'", func.value().repr()), context);
8,993✔
823
        return leaf;
8,989✔
824
    }
32,564✔
825

826
    std::optional<Node> Parser::list()
12,526✔
827
    {
12,526✔
828
        std::optional<Node> leaf { NodeType::List };
12,526✔
829
        setNodePosAndFilename(leaf.value());
12,526✔
830

831
        auto context = generateErrorContext("[");
12,526✔
832
        if (!accept(IsChar('[')))
12,526✔
833
            return std::nullopt;
11,649✔
834
        leaf->push_back(Node(NodeType::Symbol, "list"));
877✔
835

836
        std::string comment;
877✔
837
        newlineOrComment(&comment);
877✔
838
        leaf->attachNearestCommentBefore(comment);
877✔
839

840
        comment.clear();
877✔
841
        while (!isEOF())
2,069✔
842
        {
843
            if (auto value = nodeOrValue(); value.has_value())
4,136✔
844
            {
845
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
1,192✔
846
                comment.clear();
1,192✔
847
                newlineOrComment(&comment);
1,192✔
848
            }
1,192✔
849
            else
850
                break;
876✔
851
        }
852
        leaf->list().back().attachCommentAfter(comment);
877✔
853

854
        expectSuffixOrError(']', "to end list definition", context);
877✔
855
        return leaf;
876✔
856
    }
12,527✔
857

858
    std::optional<Node> Parser::atom()
118,979✔
859
    {
118,979✔
860
        const auto pos = getCount();
118,979✔
861

862
        if (auto res = Parser::number(); res.has_value())
118,979✔
863
            return res;
69,852✔
864
        backtrack(pos);
49,124✔
865

866
        if (auto res = Parser::string(); res.has_value())
49,124✔
867
            return res;
1,230✔
868
        backtrack(pos);
47,894✔
869

870
        if (auto res = Parser::spread(); m_allow_macro_behavior > 0 && res.has_value())
47,894✔
871
            return res;
28✔
872
        backtrack(pos);
47,866✔
873

874
        if (auto res = Parser::field(); res.has_value())
47,866✔
875
            return res;
756✔
876
        backtrack(pos);
47,110✔
877

878
        if (auto res = Parser::symbol(); res.has_value())
47,110✔
879
            return res;
17,693✔
880
        backtrack(pos);
29,417✔
881

882
        if (auto res = Parser::nil(); res.has_value())
29,417✔
883
            return res;
73✔
884
        backtrack(pos);
29,344✔
885

886
        return std::nullopt;
29,344✔
887
    }
118,979✔
888

889
    std::optional<Node> Parser::anyAtomOf(const std::initializer_list<NodeType> types)
10,019✔
890
    {
10,019✔
891
        auto cursor = getCursor();
10,019✔
892
        if (auto value = atom(); value.has_value())
18,992✔
893
        {
894
            setNodePosAndFilename(value.value(), cursor);
8,973✔
895
            for (const auto type : types)
18,032✔
896
            {
897
                if (value->nodeType() == type)
9,059✔
898
                    return value;
8,973✔
899
            }
9,059✔
UNCOV
900
        }
×
901
        return std::nullopt;
1,046✔
902
    }
10,019✔
903

904
    std::optional<Node> Parser::nodeOrValue()
108,957✔
905
    {
108,957✔
906
        auto cursor = getCursor();
108,957✔
907
        if (auto value = atom(); value.has_value())
189,616✔
908
        {
909
            setNodePosAndFilename(value.value(), cursor);
80,659✔
910
            return value;
80,659✔
911
        }
912
        if (auto sub_node = node(); sub_node.has_value())
44,945✔
913
        {
914
            setNodePosAndFilename(sub_node.value(), cursor);
16,650✔
915
            return sub_node;
16,650✔
916
        }
917

918
        return std::nullopt;
11,645✔
919
    }
108,957✔
920

921
    std::optional<Node> Parser::wrapped(std::optional<Node> (Parser::*parser)(), const std::string& name)
154,898✔
922
    {
154,898✔
923
        auto cursor = getCursor();
154,898✔
924
        auto context = generateErrorContext("(");
154,898✔
925
        if (!prefix('('))
154,898✔
926
            return std::nullopt;
81,180✔
927
        std::string comment;
73,718✔
928
        newlineOrComment(&comment);
73,718✔
929

930
        if (auto result = (this->*parser)(); result.has_value())
80,851✔
931
        {
932
            result->attachNearestCommentBefore(result->comment() + comment);
7,133✔
933
            setNodePosAndFilename(result.value(), cursor);
7,133✔
934

935
            comment.clear();
7,133✔
936
            if (newlineOrComment(&comment))
7,133✔
937
                result.value().attachCommentAfter(comment);
9✔
938

939
            if (result->isListLike())
7,133✔
940
                setNodePosAndFilename(result->list().back());
7,133✔
941
            expectSuffixOrError(')', "after " + name, context);
7,133✔
942

943
            comment.clear();
7,132✔
944
            if (spaceComment(&comment))
7,132✔
945
                result.value().attachCommentAfter(comment);
15✔
946

947
            return result;
7,132✔
948
        }
949

950
        return std::nullopt;
66,569✔
951
    }
154,899✔
952
}
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