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

ArkScript-lang / Ark / 15006215616

13 May 2025 08:34PM UTC coverage: 86.726% (+0.3%) from 86.474%
15006215616

push

github

SuperFola
feat(macro processor, error): adding better error messages when a macro fails, to show the macro we were expanding and what failed

60 of 60 new or added lines in 8 files covered. (100.0%)

83 existing lines in 6 files now uncovered.

7017 of 8091 relevant lines covered (86.73%)

79023.01 hits per line

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

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

14
    void Parser::process(const std::string& filename, const std::string& code)
445✔
15
    {
445✔
16
        m_logger.traceStart("process");
486✔
17
        initParser(filename, code);
445✔
18

19
        while (!isEOF())
2,426✔
20
        {
21
            std::string comment;
2,382✔
22
            newlineOrComment(&comment);
2,382✔
23
            if (isEOF())
2,382✔
24
            {
25
                if (!comment.empty())
360✔
26
                    m_ast.list().back().attachCommentAfter(comment);
1✔
27
                break;
360✔
28
            }
29

30
            const auto pos = getCount();
2,022✔
31
            if (auto n = node())
4,044✔
32
            {
33
                m_ast.push_back(n->attachNearestCommentBefore(n->comment() + comment));
1,981✔
34
                comment.clear();
1,981✔
35
                if (spaceComment(&comment))
1,981✔
36
                    m_ast.list().back().attachCommentAfter(comment);
24✔
37
            }
1,981✔
38
            else
39
            {
40
                backtrack(pos);
4✔
41
                std::string out = peek();
4✔
42
                std::string message;
4✔
43
                if (out == ")")
4✔
44
                    message = "Unexpected closing paren";
1✔
45
                else if (out == "}")
3✔
46
                    message = "Unexpected closing bracket";
1✔
47
                else if (out == "]")
2✔
48
                    message = "Unexpected closing square bracket";
1✔
49
                else
50
                    errorWithNextToken("invalid syntax, expected node");
1✔
51
                errorWithNextToken(message);
3✔
52
            }
4✔
53
        }
2,382✔
54

55
        m_logger.traceEnd();
404✔
56
    }
445✔
57

58
    const Node& Parser::ast() const noexcept
418✔
59
    {
418✔
60
        return m_ast;
418✔
61
    }
62

63
    const std::vector<Import>& Parser::imports() const
350✔
64
    {
350✔
65
        return m_imports;
350✔
66
    }
67

68
    Node& Parser::setNodePosAndFilename(Node& node, const std::optional<FilePosition>& cursor) const
281,506✔
69
    {
281,506✔
70
        if (node.line() != 0 || node.col() != 0)
281,506✔
71
            return node;
39,051✔
72

73
        const auto [row, col] = cursor.value_or(getCursor());
242,455✔
74
        node.setPos(row, col);
242,455✔
75
        node.setFilename(m_filename);
242,455✔
76
        return node;
242,455✔
77
    }
281,506✔
78

79
    std::optional<Node> Parser::node()
26,410✔
80
    {
26,410✔
81
        // save current position in buffer to be able to go back if needed
82
        const auto position = getCount();
26,410✔
83

84
        if (auto result = wrapped(&Parser::letMutSet, "variable assignment or declaration"))
26,410✔
85
            return result;
3,696✔
86
        backtrack(position);
22,709✔
87

88
        if (auto result = wrapped(&Parser::function, "function"))
22,709✔
89
            return result;
1,312✔
90
        backtrack(position);
21,393✔
91

92
        if (auto result = wrapped(&Parser::condition, "condition"))
21,393✔
93
            return result;
564✔
94
        backtrack(position);
20,826✔
95

96
        if (auto result = wrapped(&Parser::loop, "loop"))
20,826✔
97
            return result;
517✔
98
        backtrack(position);
20,299✔
99

100
        if (auto result = import_(); result.has_value())
20,299✔
101
            return result;
152✔
102
        backtrack(position);
20,146✔
103

104
        if (auto result = block(); result.has_value())
20,146✔
105
            return result;
1,309✔
106
        backtrack(position);
18,837✔
107

108
        if (auto result = wrapped(&Parser::macroCondition, "$if"))
18,837✔
109
            return result;
49✔
110
        backtrack(position);
18,779✔
111

112
        if (auto result = macro(); result.has_value())
18,779✔
113
            return result;
141✔
114
        backtrack(position);
18,638✔
115

116
        if (auto result = wrapped(&Parser::del, "del"))
18,638✔
117
            return result;
10✔
118
        backtrack(position);
18,621✔
119

120
        if (auto result = functionCall(); result.has_value())
18,621✔
121
            return result;
7,831✔
122
        backtrack(position);
10,789✔
123

124
        if (auto result = list(); result.has_value())
10,789✔
125
            return result;
720✔
126
        backtrack(position);
10,069✔
127

128
        return std::nullopt;  // will never reach
10,069✔
129
    }
26,427✔
130

131
    std::optional<Node> Parser::letMutSet()
14,318✔
132
    {
14,318✔
133
        std::optional<Node> leaf { NodeType::List };
14,318✔
134
        setNodePosAndFilename(leaf.value());
14,318✔
135

136
        std::string token;
14,318✔
137
        if (!oneOf({ "let", "mut", "set" }, &token))
14,318✔
138
            return std::nullopt;
10,617✔
139
        std::string comment;
3,701✔
140
        newlineOrComment(&comment);
3,701✔
141
        leaf->attachNearestCommentBefore(comment);
3,701✔
142

143
        if (token == "let")
3,701✔
144
            leaf->push_back(Node(Keyword::Let));
1,600✔
145
        else if (token == "mut")
2,101✔
146
            leaf->push_back(Node(Keyword::Mut));
1,183✔
147
        else  // "set"
148
            leaf->push_back(Node(Keyword::Set));
918✔
149

150
        if (m_allow_macro_behavior > 0)
3,701✔
151
        {
152
            const auto position = getCount();
22✔
153
            if (const auto value = nodeOrValue(); value.has_value())
44✔
154
            {
155
                const auto sym = value.value();
22✔
156
                if (sym.nodeType() == NodeType::List || sym.nodeType() == NodeType::Symbol || sym.nodeType() == NodeType::Macro || sym.nodeType() == NodeType::Spread)
22✔
157
                    leaf->push_back(sym);
21✔
158
                else
159
                    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✔
160
            }
22✔
161
            else
UNCOV
162
                backtrack(position);
×
163
        }
22✔
164

165
        if (leaf->constList().size() == 1)
3,700✔
166
        {
167
            // we haven't parsed anything while in "macro state"
168
            std::string symbol_name;
3,679✔
169
            if (!name(&symbol_name))
3,679✔
170
                errorWithNextToken(token + " needs a symbol");
2✔
171

172
            leaf->push_back(Node(NodeType::Symbol, symbol_name));
3,677✔
173
        }
3,679✔
174

175
        comment.clear();
3,698✔
176
        newlineOrComment(&comment);
3,698✔
177

178
        if (auto value = nodeOrValue(); value.has_value())
7,396✔
179
            leaf->push_back(value.value().attachNearestCommentBefore(comment));
3,696✔
180
        else
UNCOV
181
            errorWithNextToken("Expected a value");
×
182

183
        return leaf;
3,696✔
184
    }
14,323✔
185

186
    std::optional<Node> Parser::del()
7,848✔
187
    {
7,848✔
188
        std::optional<Node> leaf { NodeType::List };
7,848✔
189
        setNodePosAndFilename(leaf.value());
7,848✔
190

191
        if (!oneOf({ "del" }))
7,848✔
192
            return std::nullopt;
7,837✔
193
        leaf->push_back(Node(Keyword::Del));
11✔
194

195
        std::string comment;
11✔
196
        newlineOrComment(&comment);
11✔
197

198
        std::string symbol_name;
11✔
199
        if (!name(&symbol_name))
11✔
200
            errorWithNextToken("del needs a symbol");
1✔
201

202
        leaf->push_back(Node(NodeType::Symbol, symbol_name));
10✔
203
        leaf->list().back().attachNearestCommentBefore(comment);
10✔
204
        setNodePosAndFilename(leaf->list().back());
10✔
205

206
        return leaf;
10✔
207
    }
7,849✔
208

209
    std::optional<Node> Parser::condition()
9,301✔
210
    {
9,301✔
211
        std::optional<Node> leaf { NodeType::List };
9,301✔
212
        setNodePosAndFilename(leaf.value());
9,301✔
213

214
        if (!oneOf({ "if" }))
9,301✔
215
            return std::nullopt;
8,734✔
216

217
        std::string comment;
567✔
218
        newlineOrComment(&comment);
567✔
219

220
        leaf->push_back(Node(Keyword::If));
567✔
221

222
        if (auto cond_expr = nodeOrValue(); cond_expr.has_value())
1,134✔
223
            leaf->push_back(cond_expr.value().attachNearestCommentBefore(comment));
566✔
224
        else
225
            errorWithNextToken("`if' needs a valid condition");
1✔
226

227
        comment.clear();
566✔
228
        newlineOrComment(&comment);
566✔
229

230
        if (auto value_if_true = nodeOrValue(); value_if_true.has_value())
1,132✔
231
            leaf->push_back(value_if_true.value().attachNearestCommentBefore(comment));
565✔
232
        else
233
            errorWithNextToken("Expected a node or value after condition");
1✔
234

235
        comment.clear();
565✔
236
        newlineOrComment(&comment);
565✔
237

238
        if (auto value_if_false = nodeOrValue(); value_if_false.has_value())
1,130✔
239
        {
240
            leaf->push_back(value_if_false.value().attachNearestCommentBefore(comment));
381✔
241
            comment.clear();
381✔
242
            if (newlineOrComment(&comment))
381✔
243
                leaf->list().back().attachCommentAfter(comment);
2✔
244
        }
381✔
245
        else if (!comment.empty())
184✔
246
            leaf->attachCommentAfter(comment);
2✔
247

248
        setNodePosAndFilename(leaf->list().back());
565✔
249
        return leaf;
565✔
250
    }
9,303✔
251

252
    std::optional<Node> Parser::loop()
8,734✔
253
    {
8,734✔
254
        std::optional<Node> leaf { NodeType::List };
8,734✔
255
        setNodePosAndFilename(leaf.value());
8,734✔
256

257
        if (!oneOf({ "while" }))
8,734✔
258
            return std::nullopt;
8,215✔
259

260
        std::string comment;
519✔
261
        newlineOrComment(&comment);
519✔
262

263
        leaf->push_back(Node(Keyword::While));
519✔
264

265
        if (auto cond_expr = nodeOrValue(); cond_expr.has_value())
1,038✔
266
            leaf->push_back(cond_expr.value().attachNearestCommentBefore(comment));
518✔
267
        else
268
            errorWithNextToken("`while' needs a valid condition");
1✔
269

270
        comment.clear();
518✔
271
        newlineOrComment(&comment);
518✔
272

273
        if (auto body = nodeOrValue(); body.has_value())
1,036✔
274
            leaf->push_back(body.value().attachNearestCommentBefore(comment));
517✔
275
        else
276
            errorWithNextToken("Expected a node or value after loop condition");
1✔
277

278
        setNodePosAndFilename(leaf->list().back());
517✔
279
        return leaf;
517✔
280
    }
8,736✔
281

282
    std::optional<Node> Parser::import_()
20,307✔
283
    {
20,307✔
284
        std::optional<Node> leaf { NodeType::List };
20,307✔
285
        setNodePosAndFilename(leaf.value());
20,307✔
286

287
        auto context = generateErrorContext("(");
20,307✔
288
        if (!accept(IsChar('(')))
20,307✔
289
            return std::nullopt;
12,092✔
290

291
        std::string comment;
8,215✔
292
        newlineOrComment(&comment);
8,215✔
293
        leaf->attachNearestCommentBefore(comment);
8,215✔
294

295
        if (!oneOf({ "import" }))
8,215✔
296
            return std::nullopt;
8,055✔
297
        comment.clear();
160✔
298
        newlineOrComment(&comment);
160✔
299
        leaf->push_back(Node(Keyword::Import));
160✔
300

301
        Import import_data;
160✔
302

303
        const auto pos = getCount();
160✔
304
        if (!packageName(&import_data.prefix))
160✔
305
            errorWithNextToken("Import expected a package name");
1✔
306

307
        if (import_data.prefix.size() > 255)
159✔
308
        {
309
            backtrack(pos);
1✔
310
            errorWithNextToken(fmt::format("Import name too long, expected at most 255 characters, got {}", import_data.prefix.size()));
1✔
UNCOV
311
        }
×
312
        import_data.package.push_back(import_data.prefix);
158✔
313

314
        const auto [row, col] = getCursor();
158✔
315
        import_data.col = col;
158✔
316
        import_data.line = row;
158✔
317

318
        Node packageNode(NodeType::List);
158✔
319
        setNodePosAndFilename(packageNode.attachNearestCommentBefore(comment));
158✔
320
        packageNode.push_back(Node(NodeType::Symbol, import_data.prefix));
158✔
321

322
        // first, parse the package name
323
        while (!isEOF())
275✔
324
        {
325
            // parsing package folder.foo.bar.yes
326
            if (accept(IsChar('.')))
275✔
327
            {
328
                std::string path;
120✔
329
                if (!packageName(&path))
120✔
330
                    errorWithNextToken("Package name expected after '.'");
2✔
331
                else
332
                {
333
                    packageNode.push_back(Node(NodeType::Symbol, path));
118✔
334
                    setNodePosAndFilename(packageNode.list().back());
118✔
335
                    import_data.package.push_back(path);
118✔
336
                    import_data.prefix = path;  // in the end we will store the last element of the package, which is what we want
118✔
337

338
                    if (path.size() > 255)
118✔
339
                    {
340
                        backtrack(pos);
1✔
341
                        errorWithNextToken(fmt::format("Import name too long, expected at most 255 characters, got {}", path.size()));
1✔
UNCOV
342
                    }
×
343
                }
344
            }
120✔
345
            else if (accept(IsChar(':')) && accept(IsChar('*')))  // parsing :*
155✔
346
            {
347
                leaf->push_back(packageNode);
10✔
348
                leaf->push_back(Node(NodeType::Symbol, "*"));
10✔
349
                setNodePosAndFilename(leaf->list().back());
10✔
350

351
                space();
10✔
352
                expectSuffixOrError(')', fmt::format("in import `{}'", import_data.toPackageString()), context);
10✔
353

354
                // save the import data structure to know we encounter an import node, and retrieve its data more easily later on
355
                import_data.with_prefix = false;
10✔
356
                import_data.is_glob = true;
10✔
357
                m_imports.push_back(import_data);
10✔
358

359
                return leaf;
10✔
360
            }
361
            else
362
                break;
145✔
363
        }
364

365
        Node symbols(NodeType::List);
145✔
366
        setNodePosAndFilename(symbols);
145✔
367
        // then parse the symbols to import, if any
368
        if (space())
145✔
369
        {
370
            comment.clear();
64✔
371
            newlineOrComment(&comment);
64✔
372

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

383
                    if (symbol_name.size() >= 2 && symbol_name[symbol_name.size() - 2] == ':' && symbol_name.back() == '*')
95✔
384
                    {
385
                        backtrack(getCount() - 2);  // we can backtrack n-2 safely here because we know the previous chars were ":*"
1✔
386
                        error("Glob pattern can not follow a symbol to import", ":*");
1✔
UNCOV
387
                    }
×
388

389
                    symbols.push_back(Node(NodeType::Symbol, symbol_name).attachNearestCommentBefore(comment));
94✔
390
                    comment.clear();
94✔
391
                    setNodePosAndFilename(symbols.list().back());
94✔
392
                    import_data.symbols.push_back(symbol_name);
94✔
393
                    // we do not need the prefix when importing specific symbols
394
                    import_data.with_prefix = false;
94✔
395
                }
97✔
396

397
                if (!space())
97✔
398
                    break;
61✔
399
                comment.clear();
36✔
400
                newlineOrComment(&comment);
36✔
401
            }
402

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

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

412
        comment.clear();
142✔
413
        if (newlineOrComment(&comment))
142✔
414
            leaf->list().back().attachCommentAfter(comment);
1✔
415

416
        expectSuffixOrError(')', fmt::format("in import `{}'", import_data.toPackageString()), context);
142✔
417
        return leaf;
142✔
418
    }
20,315✔
419

420
    std::optional<Node> Parser::block()
20,147✔
421
    {
20,147✔
422
        std::optional<Node> leaf { NodeType::List };
20,147✔
423
        setNodePosAndFilename(leaf.value());
20,147✔
424

425
        auto context = generateErrorContext("(");
20,147✔
426
        bool alt_syntax = false;
20,147✔
427
        std::string comment;
20,147✔
428
        if (accept(IsChar('(')))
20,147✔
429
        {
430
            newlineOrComment(&comment);
8,055✔
431
            if (!oneOf({ "begin" }))
8,055✔
432
                return std::nullopt;
8,047✔
433
        }
8✔
434
        else if (accept(IsChar('{')))
12,092✔
435
            alt_syntax = true;
1,302✔
436
        else
437
            return std::nullopt;
10,790✔
438

439
        leaf->push_back(Node(Keyword::Begin).attachNearestCommentBefore(comment));
1,310✔
440

441
        comment.clear();
1,310✔
442
        newlineOrComment(&comment);
1,310✔
443

444
        while (!isEOF())
6,019✔
445
        {
446
            if (auto value = nodeOrValue(); value.has_value())
12,036✔
447
            {
448
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
4,709✔
449
                comment.clear();
4,709✔
450
                newlineOrComment(&comment);
4,709✔
451
            }
4,709✔
452
            else
453
                break;
1,309✔
454
        }
455

456
        newlineOrComment(&comment);
1,310✔
457
        expectSuffixOrError(alt_syntax ? '}' : ')', "to close block", context);
1,310✔
458
        setNodePosAndFilename(leaf->list().back());
1,309✔
459
        leaf->list().back().attachCommentAfter(comment);
1,309✔
460
        return leaf;
1,309✔
461
    }
20,148✔
462

463
    std::optional<Node> Parser::functionArgs()
1,295✔
464
    {
1,295✔
465
        expect(IsChar('('));
1,296✔
466
        std::optional<Node> args { NodeType::List };
1,295✔
467
        setNodePosAndFilename(args.value());
1,295✔
468

469
        std::string comment;
1,295✔
470
        newlineOrComment(&comment);
1,295✔
471
        args->attachNearestCommentBefore(comment);
1,295✔
472

473
        bool has_captures = false;
1,295✔
474

475
        while (!isEOF())
3,463✔
476
        {
477
            const auto pos = getCursor();
3,462✔
478
            if (accept(IsChar('&')))  // captures
3,462✔
479
            {
480
                has_captures = true;
184✔
481
                std::string capture;
184✔
482
                if (!name(&capture))
184✔
UNCOV
483
                    break;
×
484
                Node capture_node = Node(NodeType::Capture, capture).attachNearestCommentBefore(comment);
184✔
485
                setNodePosAndFilename(capture_node, pos);
184✔
486
                args->push_back(capture_node);
184✔
487
            }
184✔
488
            else
489
            {
490
                const auto count = getCount();
3,278✔
491
                std::string symbol_name;
3,278✔
492
                if (!name(&symbol_name))
3,278✔
493
                    break;
1,293✔
494
                if (has_captures)
1,985✔
495
                {
496
                    backtrack(count);
1✔
497
                    error("Captured variables should be at the end of the argument list", symbol_name);
1✔
UNCOV
498
                }
×
499

500
                Node arg_node = Node(NodeType::Symbol, symbol_name).attachNearestCommentBefore(comment);
1,984✔
501
                setNodePosAndFilename(arg_node, pos);
1,984✔
502
                args->push_back(arg_node);
1,984✔
503
            }
3,278✔
504

505
            comment.clear();
2,168✔
506
            newlineOrComment(&comment);
2,168✔
507
        }
3,462✔
508

509
        if (accept(IsChar(')')))
1,294✔
510
            return args;
1,292✔
511
        return std::nullopt;
2✔
512
    }
1,296✔
513

514
    std::optional<Node> Parser::function()
10,617✔
515
    {
10,617✔
516
        std::optional<Node> leaf { NodeType::List };
10,617✔
517
        setNodePosAndFilename(leaf.value());
10,617✔
518

519
        if (!oneOf({ "fun" }))
10,617✔
520
            return std::nullopt;
9,301✔
521
        leaf->push_back(Node(Keyword::Fun));
1,316✔
522

523
        std::string comment_before_args;
1,316✔
524
        newlineOrComment(&comment_before_args);
1,316✔
525

526
        while (m_allow_macro_behavior > 0)
1,316✔
527
        {
528
            const auto position = getCount();
21✔
529

530
            // args
531
            if (const auto value = nodeOrValue(); value.has_value())
42✔
532
            {
533
                // if value is nil, just add an empty argument bloc to prevent bugs when
534
                // declaring functions inside macros
535
                Node args = value.value();
21✔
536
                setNodePosAndFilename(args);
21✔
537
                if (args.nodeType() == NodeType::Symbol && args.string() == "nil")
21✔
538
                    leaf->push_back(Node(NodeType::List));
5✔
539
                else
540
                    leaf->push_back(args);
16✔
541
            }
21✔
542
            else
543
            {
UNCOV
544
                backtrack(position);
×
UNCOV
545
                break;
×
546
            }
547

548
            std::string comment;
21✔
549
            newlineOrComment(&comment);
21✔
550
            // body
551
            if (auto value = nodeOrValue(); value.has_value())
42✔
552
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
21✔
553
            else
UNCOV
554
                errorWithNextToken("Expected a body for the function");
×
555
            setNodePosAndFilename(leaf->list().back());
21✔
556
            return leaf;
21✔
557
        }
21✔
558

559
        const auto position = getCount();
1,295✔
560
        if (auto args = functionArgs(); args.has_value())
2,590✔
561
            leaf->push_back(args.value().attachNearestCommentBefore(comment_before_args));
1,292✔
562
        else
563
        {
564
            backtrack(position);
2✔
565

566
            if (auto value = nodeOrValue(); value.has_value())
4✔
567
                leaf->push_back(value.value().attachNearestCommentBefore(comment_before_args));
1✔
568
            else
UNCOV
569
                errorWithNextToken("Expected an argument list");
×
570
        }
571

572
        std::string comment;
1,293✔
573
        newlineOrComment(&comment);
1,293✔
574

575
        if (auto value = nodeOrValue(); value.has_value())
2,586✔
576
            leaf->push_back(value.value().attachNearestCommentBefore(comment));
1,291✔
577
        else
578
            errorWithNextToken("Expected a body for the function");
2✔
579

580
        setNodePosAndFilename(leaf->list().back());
1,291✔
581
        return leaf;
1,291✔
582
    }
10,621✔
583

584
    std::optional<Node> Parser::macroCondition()
8,047✔
585
    {
8,047✔
586
        std::optional<Node> leaf { NodeType::Macro };
8,047✔
587
        setNodePosAndFilename(leaf.value());
8,047✔
588

589
        if (!oneOf({ "$if" }))
8,047✔
590
            return std::nullopt;
7,996✔
591
        leaf->push_back(Node(Keyword::If));
51✔
592

593
        std::string comment;
51✔
594
        newlineOrComment(&comment);
51✔
595
        leaf->attachNearestCommentBefore(comment);
51✔
596

597
        if (const auto cond_expr = nodeOrValue(); cond_expr.has_value())
102✔
598
            leaf->push_back(cond_expr.value());
50✔
599
        else
600
            errorWithNextToken("$if need a valid condition");
1✔
601

602
        comment.clear();
50✔
603
        newlineOrComment(&comment);
50✔
604

605
        if (auto value_if_true = nodeOrValue(); value_if_true.has_value())
100✔
606
            leaf->push_back(value_if_true.value().attachNearestCommentBefore(comment));
49✔
607
        else
608
            errorWithNextToken("Expected a node or value after condition");
1✔
609

610
        comment.clear();
49✔
611
        newlineOrComment(&comment);
49✔
612

613
        if (auto value_if_false = nodeOrValue(); value_if_false.has_value())
88✔
614
        {
615
            leaf->push_back(value_if_false.value().attachNearestCommentBefore(comment));
39✔
616
            comment.clear();
39✔
617
            newlineOrComment(&comment);
39✔
618
            leaf->list().back().attachCommentAfter(comment);
39✔
619
        }
39✔
620

621
        setNodePosAndFilename(leaf->list().back());
49✔
622
        return leaf;
49✔
623
    }
8,049✔
624

625
    std::optional<Node> Parser::macroArgs()
147✔
626
    {
147✔
627
        if (!accept(IsChar('(')))
150✔
628
            return std::nullopt;
22✔
629

630
        std::optional<Node> args { NodeType::List };
125✔
631
        setNodePosAndFilename(args.value());
125✔
632

633
        std::string comment;
125✔
634
        newlineOrComment(&comment);
125✔
635
        args->attachNearestCommentBefore(comment);
125✔
636

637
        std::vector<std::string> names;
125✔
638
        while (!isEOF())
304✔
639
        {
640
            const auto pos = getCount();
303✔
641

642
            std::string arg_name;
303✔
643
            if (!name(&arg_name))
303✔
644
                break;
123✔
645
            comment.clear();
180✔
646
            newlineOrComment(&comment);
180✔
647
            args->push_back(Node(NodeType::Symbol, arg_name).attachNearestCommentBefore(comment));
180✔
648

649
            if (std::ranges::find(names, arg_name) != names.end())
180✔
650
            {
651
                backtrack(pos);
1✔
652
                errorWithNextToken(fmt::format("Argument names must be unique, can not reuse `{}'", arg_name));
1✔
UNCOV
653
            }
×
654
            names.push_back(arg_name);
179✔
655
        }
303✔
656

657
        const auto pos = getCount();
124✔
658
        if (sequence("..."))
124✔
659
        {
660
            std::string spread_name;
45✔
661
            if (!name(&spread_name))
45✔
662
                errorWithNextToken("Expected a name for the variadic arguments list");
1✔
663
            args->push_back(Node(NodeType::Spread, spread_name));
44✔
664

665
            comment.clear();
44✔
666
            if (newlineOrComment(&comment))
44✔
667
                args->list().back().attachCommentAfter(comment);
2✔
668

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

676
        if (!accept(IsChar(')')))
122✔
677
            return std::nullopt;
32✔
678
        comment.clear();
90✔
679
        if (newlineOrComment(&comment))
90✔
680
        {
681
            if (args->list().empty())
2✔
682
                args->attachCommentAfter(comment);
2✔
683
            else
UNCOV
684
                args->list().back().attachCommentAfter(comment);
×
685
        }
2✔
686

687
        return args;
90✔
688
    }
150✔
689

690
    std::optional<Node> Parser::macro()
18,786✔
691
    {
18,786✔
692
        std::optional<Node> leaf { NodeType::Macro };
18,786✔
693
        setNodePosAndFilename(leaf.value());
18,786✔
694

695
        auto context = generateErrorContext("(");
18,786✔
696
        if (!accept(IsChar('(')))
18,786✔
697
            return std::nullopt;
10,790✔
698
        std::string comment;
7,996✔
699
        newlineOrComment(&comment);
7,996✔
700

701
        if (!oneOf({ "$" }))
7,996✔
702
            return std::nullopt;
7,848✔
703
        newlineOrComment(&comment);
148✔
704
        leaf->attachNearestCommentBefore(comment);
148✔
705

706
        std::string symbol_name;
148✔
707
        if (!name(&symbol_name))
148✔
708
            errorWithNextToken("$ needs a symbol to declare a macro");
1✔
709
        comment.clear();
147✔
710
        newlineOrComment(&comment);
147✔
711

712
        leaf->push_back(Node(NodeType::Symbol, symbol_name).attachNearestCommentBefore(comment));
147✔
713

714
        const auto position = getCount();
147✔
715
        if (const auto args = macroArgs(); args.has_value())
294✔
716
            leaf->push_back(args.value());
90✔
717
        else
718
        {
719
            backtrack(position);
54✔
720

721
            ++m_allow_macro_behavior;
54✔
722
            const auto value = nodeOrValue();
54✔
723
            --m_allow_macro_behavior;
53✔
724

725
            if (value.has_value())
53✔
726
                leaf->push_back(value.value());
52✔
727
            else
728
                errorWithNextToken(fmt::format("Expected an argument list, atom or node while defining macro `{}'", symbol_name));
1✔
729

730
            setNodePosAndFilename(leaf->list().back());
52✔
731
            if (accept(IsChar(')')))
52✔
732
                return leaf;
52✔
733
        }
54✔
734

735
        ++m_allow_macro_behavior;
90✔
736
        const auto value = nodeOrValue();
90✔
737
        --m_allow_macro_behavior;
89✔
738

739
        if (value.has_value())
89✔
740
            leaf->push_back(value.value());
87✔
741
        else if (leaf->list().size() == 2)
2✔
742
        {
743
            setNodePosAndFilename(leaf->list().back());
2✔
744
            comment.clear();
2✔
745
            if (newlineOrComment(&comment))
2✔
UNCOV
746
                leaf->list().back().attachCommentAfter(comment);
×
747

748
            expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
2✔
749
            return leaf;
2✔
750
        }
751
        else
752
        {
UNCOV
753
            backtrack(position);
×
UNCOV
754
            errorWithNextToken(fmt::format("Expected a value while defining macro `{}'", symbol_name), context);
×
755
        }
756

757
        setNodePosAndFilename(leaf->list().back());
87✔
758
        comment.clear();
87✔
759
        if (newlineOrComment(&comment))
87✔
760
            leaf->list().back().attachCommentAfter(comment);
3✔
761

762
        expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
87✔
763
        return leaf;
87✔
764
    }
18,792✔
765

766
    std::optional<Node> Parser::functionCall()
18,627✔
767
    {
18,627✔
768
        auto context = generateErrorContext("(");
18,627✔
769
        if (!accept(IsChar('(')))
18,627✔
770
            return std::nullopt;
10,790✔
771
        std::string comment;
7,837✔
772
        newlineOrComment(&comment);
7,837✔
773
        auto cursor = getCursor();
7,837✔
774

775
        std::optional<Node> func;
7,837✔
776
        if (auto sym_or_field = anyAtomOf({ NodeType::Symbol, NodeType::Field }); sym_or_field.has_value())
15,674✔
777
            func = sym_or_field->attachNearestCommentBefore(comment);
7,815✔
778
        else if (auto nested = node(); nested.has_value())
44✔
779
            func = nested->attachNearestCommentBefore(comment);
22✔
780
        else
UNCOV
781
            return std::nullopt;
×
782
        comment.clear();
7,837✔
783
        newlineOrComment(&comment);
7,837✔
784

785
        std::optional<Node> leaf { NodeType::List };
7,837✔
786
        setNodePosAndFilename(leaf.value(), cursor);
7,837✔
787
        setNodePosAndFilename(func.value(), cursor);
7,837✔
788
        leaf->push_back(func.value());
7,837✔
789

790
        while (!isEOF())
87,155✔
791
        {
792
            if (auto arg = nodeOrValue(); arg.has_value())
174,302✔
793
            {
794
                leaf->push_back(arg.value().attachNearestCommentBefore(comment));
79,318✔
795
                comment.clear();
79,318✔
796
                newlineOrComment(&comment);
79,318✔
797
            }
79,318✔
798
            else
799
                break;
7,831✔
800
        }
801

802
        leaf->list().back().attachCommentAfter(comment);
7,835✔
803

804
        comment.clear();
7,835✔
805
        if (newlineOrComment(&comment))
7,835✔
UNCOV
806
            leaf->list().back().attachCommentAfter(comment);
×
807

808
        expectSuffixOrError(')', fmt::format("in function call to `{}'", func.value().repr()), context);
7,835✔
809
        return leaf;
7,831✔
810
    }
26,464✔
811

812
    std::optional<Node> Parser::list()
10,790✔
813
    {
10,790✔
814
        std::optional<Node> leaf { NodeType::List };
10,790✔
815
        setNodePosAndFilename(leaf.value());
10,790✔
816

817
        auto context = generateErrorContext("[");
10,790✔
818
        if (!accept(IsChar('[')))
10,790✔
819
            return std::nullopt;
10,069✔
820
        leaf->push_back(Node(NodeType::Symbol, "list"));
721✔
821

822
        std::string comment;
721✔
823
        newlineOrComment(&comment);
721✔
824
        leaf->attachNearestCommentBefore(comment);
721✔
825

826
        comment.clear();
721✔
827
        while (!isEOF())
1,739✔
828
        {
829
            if (auto value = nodeOrValue(); value.has_value())
3,476✔
830
            {
831
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
1,018✔
832
                comment.clear();
1,018✔
833
                newlineOrComment(&comment);
1,018✔
834
            }
1,018✔
835
            else
836
                break;
720✔
837
        }
838
        leaf->list().back().attachCommentAfter(comment);
721✔
839

840
        expectSuffixOrError(']', "to end list definition", context);
721✔
841
        return leaf;
720✔
842
    }
10,791✔
843

844
    std::optional<Node> Parser::atom()
110,829✔
845
    {
110,829✔
846
        const auto pos = getCount();
110,829✔
847

848
        if (auto res = Parser::number(); res.has_value())
110,829✔
849
            return res;
69,320✔
850
        backtrack(pos);
41,506✔
851

852
        if (auto res = Parser::string(); res.has_value())
41,506✔
853
            return res;
1,152✔
854
        backtrack(pos);
40,354✔
855

856
        if (auto res = Parser::spread(); m_allow_macro_behavior > 0 && res.has_value())
40,354✔
857
            return res;
28✔
858
        backtrack(pos);
40,326✔
859

860
        if (auto res = Parser::field(); res.has_value())
40,326✔
861
            return res;
717✔
862
        backtrack(pos);
39,609✔
863

864
        if (auto res = Parser::symbol(); res.has_value())
39,609✔
865
            return res;
15,149✔
866
        backtrack(pos);
24,460✔
867

868
        if (auto res = Parser::nil(); res.has_value())
24,460✔
869
            return res;
72✔
870
        backtrack(pos);
24,388✔
871

872
        return std::nullopt;
24,388✔
873
    }
110,829✔
874

875
    std::optional<Node> Parser::anyAtomOf(const std::initializer_list<NodeType> types)
7,837✔
876
    {
7,837✔
877
        auto cursor = getCursor();
7,837✔
878
        if (auto value = atom(); value.has_value())
15,652✔
879
        {
880
            setNodePosAndFilename(value.value(), cursor);
7,815✔
881
            for (const auto type : types)
15,708✔
882
            {
883
                if (value->nodeType() == type)
7,893✔
884
                    return value;
7,815✔
885
            }
7,893✔
UNCOV
886
        }
×
887
        return std::nullopt;
22✔
888
    }
7,837✔
889

890
    std::optional<Node> Parser::nodeOrValue()
102,989✔
891
    {
102,989✔
892
        auto cursor = getCursor();
102,989✔
893
        if (auto value = atom(); value.has_value())
181,612✔
894
        {
895
            setNodePosAndFilename(value.value(), cursor);
78,623✔
896
            return value;
78,623✔
897
        }
898
        if (auto sub_node = node(); sub_node.has_value())
38,661✔
899
        {
900
            setNodePosAndFilename(sub_node.value(), cursor);
14,298✔
901
            return sub_node;
14,298✔
902
        }
903

904
        return std::nullopt;
10,065✔
905
    }
102,989✔
906

907
    std::optional<Node> Parser::wrapped(std::optional<Node> (Parser::*parser)(), const std::string& name)
128,813✔
908
    {
128,813✔
909
        auto cursor = getCursor();
128,813✔
910
        auto context = generateErrorContext("(");
128,813✔
911
        if (!prefix('('))
128,813✔
912
            return std::nullopt;
69,948✔
913
        std::string comment;
58,865✔
914
        newlineOrComment(&comment);
58,865✔
915

916
        if (auto result = (this->*parser)(); result.has_value())
65,014✔
917
        {
918
            result->attachNearestCommentBefore(result->comment() + comment);
6,149✔
919
            setNodePosAndFilename(result.value(), cursor);
6,149✔
920

921
            comment.clear();
6,149✔
922
            if (newlineOrComment(&comment))
6,149✔
923
                result.value().attachCommentAfter(comment);
9✔
924

925
            if (result->isListLike())
6,149✔
926
                setNodePosAndFilename(result->list().back());
6,149✔
927
            expectSuffixOrError(')', "after " + name, context);
6,149✔
928

929
            comment.clear();
6,148✔
930
            if (spaceComment(&comment))
6,148✔
931
                result.value().attachCommentAfter(comment);
15✔
932

933
            return result;
6,148✔
934
        }
935

936
        return std::nullopt;
52,700✔
937
    }
128,814✔
938
}
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