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

ArkScript-lang / Ark / 20065764942

09 Dec 2025 01:49PM UTC coverage: 90.564% (+0.008%) from 90.556%
20065764942

push

github

SuperFola
chore(tests): adding IR generation tests for arg attributes

8043 of 8881 relevant lines covered (90.56%)

180911.91 hits per line

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

98.11
/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,164✔
8
        BaseParser(), m_mode(mode), m_logger("Parser", debug),
582✔
9
        m_ast(NodeType::List), m_imports({}), m_allow_macro_behavior(0),
582✔
10
        m_nested_nodes(0)
582✔
11
    {
582✔
12
        m_ast.push_back(Node(Keyword::Begin));
582✔
13

14
        m_parsers = {
1,164✔
15
            [this](FilePosition) {
54,357✔
16
                return wrapped(&Parser::letMutSet, "variable assignment or declaration");
53,775✔
17
            },
7✔
18
            [this](FilePosition) {
46,232✔
19
                return wrapped(&Parser::function, "function");
45,650✔
20
            },
7✔
21
            [this](FilePosition) {
43,049✔
22
                return wrapped(&Parser::condition, "condition");
42,467✔
23
            },
3✔
24
            [this](FilePosition) {
41,872✔
25
                return wrapped(&Parser::loop, "loop");
41,290✔
26
            },
2✔
27
            [this](const FilePosition filepos) {
40,887✔
28
                return import_(filepos);
40,305✔
29
            },
30
            [this](const FilePosition filepos) {
40,659✔
31
                return block(filepos);
40,077✔
32
            },
33
            [this](FilePosition) {
38,191✔
34
                return wrapped(&Parser::macroCondition, "$if");
37,609✔
35
            },
2✔
36
            [this](const FilePosition filepos) {
38,136✔
37
                return macro(filepos);
37,554✔
38
            },
39
            [this](FilePosition) {
37,976✔
40
                return wrapped(&Parser::del, "del");
37,394✔
41
            },
1✔
42
            [this](const FilePosition filepos) {
37,964✔
43
                return functionCall(filepos);
37,382✔
44
            },
45
            [this](const FilePosition filepos) {
21,383✔
46
                return list(filepos);
20,801✔
47
            }
48
        };
49
    }
582✔
50

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

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

66
            const auto pos = getCount();
4,252✔
67
            if (auto n = node())
8,504✔
68
            {
69
                m_ast.push_back(n->attachNearestCommentBefore(n->comment() + comment));
4,198✔
70
                m_ast.list().back().attachCommentAfter(spaceComment());
4,198✔
71
            }
4,198✔
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
        }
4,732✔
88

89
        m_logger.traceEnd();
528✔
90
    }
582✔
91

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

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

102
    Node Parser::positioned(Node node, const FilePosition cursor) const
132,210✔
103
    {
132,210✔
104
        const auto [row, col] = cursor;
396,630✔
105
        const auto [end_row, end_col] = getCursor();
396,630✔
106

107
        node.m_filename = m_filename;
132,210✔
108
        node.m_pos = FileSpan {
264,420✔
109
            .start = FilePos { .line = row, .column = col },
396,630✔
110
            .end = FilePos { .line = end_row, .column = end_col }
396,630✔
111
        };
112
        return node;
132,210✔
113
    }
132,210✔
114

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

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

123
        node->m_filename = m_filename;
37,279✔
124
        node->m_pos = FileSpan {
74,558✔
125
            .start = FilePos { .line = row, .column = col },
111,837✔
126
            .end = FilePos { .line = end_row, .column = end_col }
111,837✔
127
        };
128
        return node;
37,279✔
129
    }
37,279✔
130

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

135
        if (m_nested_nodes > MaxNestedNodes)
53,776✔
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,080✔
137

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

143
        for (auto&& parser : m_parsers)
488,079✔
144
        {
145
            result = parser(filepos);
434,304✔
146

147
            if (result)
433,225✔
148
                break;
33,057✔
149
            backtrack(position);
400,168✔
150
        }
434,304✔
151

152
        // return std::nullopt only on parsing error, nothing matched, the user provided terrible code
153
        --m_nested_nodes;
52,696✔
154
        return result;
52,696✔
155
    }
53,776✔
156

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

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

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

168
        if (token == "let")
8,125✔
169
            leaf->push_back(Node(Keyword::Let));
4,076✔
170
        else if (token == "mut")
4,049✔
171
            leaf->push_back(Node(Keyword::Mut));
2,220✔
172
        else  // "set"
173
            leaf->push_back(Node(Keyword::Set));
1,829✔
174

175
        if (m_allow_macro_behavior > 0)
8,125✔
176
        {
177
            const auto position = getCount();
23✔
178
            const auto value_pos = getCursor();
23✔
179
            if (const auto value = nodeOrValue(); value.has_value())
46✔
180
            {
181
                const Node& sym = value.value();
23✔
182
                if (sym.nodeType() == NodeType::List || sym.nodeType() == NodeType::Symbol || sym.nodeType() == NodeType::Macro || sym.nodeType() == NodeType::Spread)
23✔
183
                    leaf->push_back(sym);
22✔
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
            }
23✔
187
            else
188
                backtrack(position);
×
189
        }
23✔
190

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

198
            leaf->push_back(Node(NodeType::Symbol, symbol_name));
8,100✔
199
        }
8,102✔
200

201
        comment = newlineOrComment();
8,122✔
202
        if (auto value = nodeOrValue(); value.has_value())
16,244✔
203
            leaf->push_back(value.value().attachNearestCommentBefore(comment));
8,118✔
204
        else
205
            errorWithNextToken("Expected a value");
×
206

207
        return positioned(leaf, filepos);
8,118✔
208
    }
30,523✔
209

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

214
        if (!oneOf({ "del" }))
16,594✔
215
            return std::nullopt;
16,582✔
216
        leaf->push_back(Node(Keyword::Del));
12✔
217

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

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

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

227
        return positioned(leaf, filepos);
11✔
228
    }
16,595✔
229

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

234
        if (!oneOf({ "if" }))
19,208✔
235
            return std::nullopt;
18,031✔
236

237
        std::string comment = newlineOrComment();
1,177✔
238

239
        leaf->push_back(Node(Keyword::If));
1,177✔
240

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

246
        comment = newlineOrComment();
1,176✔
247
        if (auto value_if_true = nodeOrValue(); value_if_true.has_value())
2,352✔
248
            leaf->push_back(value_if_true.value().attachNearestCommentBefore(comment));
1,175✔
249
        else
250
            errorWithNextToken("Expected a node or value after condition");
1✔
251

252
        comment = newlineOrComment();
1,175✔
253
        if (auto value_if_false = nodeOrValue(); value_if_false.has_value())
2,350✔
254
        {
255
            leaf->push_back(value_if_false.value().attachNearestCommentBefore(comment));
744✔
256
            leaf->list().back().attachCommentAfter(newlineOrComment());
744✔
257
        }
744✔
258
        else if (!comment.empty())
431✔
259
            leaf->attachCommentAfter(comment);
2✔
260

261
        return positioned(leaf, filepos);
1,175✔
262
    }
19,210✔
263

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

268
        if (!oneOf({ "while" }))
18,031✔
269
            return std::nullopt;
17,046✔
270

271
        std::string comment = newlineOrComment();
985✔
272
        leaf->push_back(Node(Keyword::While));
985✔
273

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

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

285
        return positioned(leaf, filepos);
983✔
286
    }
18,033✔
287

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

292
        auto context = generateErrorContextAtCurrentPosition();
40,305✔
293
        if (!accept(IsChar('(')))
40,305✔
294
            return std::nullopt;
23,259✔
295

296
        std::string comment = newlineOrComment();
17,046✔
297
        leaf->attachNearestCommentBefore(comment);
17,046✔
298

299
        if (!oneOf({ "import" }))
17,046✔
300
            return std::nullopt;
16,818✔
301

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

305
        Import import_data;
228✔
306
        import_data.col = filepos.col;
228✔
307
        import_data.line = filepos.row;
228✔
308

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

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

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

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

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

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

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

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

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

362
                return positioned(leaf, filepos);
11✔
363
            }
364
            else
365
                break;
212✔
366
        }
396✔
367

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

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

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

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

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

396
                if (!space())
125✔
397
                    break;
79✔
398
                comment = newlineOrComment();
46✔
399
            }
400

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

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

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

414
        expectSuffixOrError(')', fmt::format("in import `{}'", import_data.toPackageString()), context);
209✔
415
        return positioned(leaf, filepos);
209✔
416
    }
40,313✔
417

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

422
        auto context = generateErrorContextAtCurrentPosition();
40,077✔
423
        bool alt_syntax = false;
40,077✔
424
        std::string comment;
40,077✔
425
        if (accept(IsChar('(')))
40,077✔
426
        {
427
            comment = newlineOrComment();
16,818✔
428
            if (!oneOf({ "begin" }))
16,818✔
429
                return std::nullopt;
16,809✔
430
        }
9✔
431
        else if (accept(IsChar('{')))
23,259✔
432
            alt_syntax = true;
2,459✔
433
        else
434
            return std::nullopt;
20,800✔
435

436
        leaf->setAltSyntax(alt_syntax);
2,468✔
437
        leaf->push_back(Node(Keyword::Begin).attachNearestCommentBefore(comment));
2,468✔
438

439
        comment = newlineOrComment();
2,468✔
440

441
        while (!isEOF())
11,330✔
442
        {
443
            if (auto value = nodeOrValue(); value.has_value())
22,658✔
444
            {
445
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
8,862✔
446
                comment = newlineOrComment();
8,862✔
447
            }
8,862✔
448
            else
449
                break;
2,466✔
450
        }
451

452
        comment += newlineOrComment();
2,467✔
453
        expectSuffixOrError(alt_syntax ? '}' : ')', "to close block", context);
2,467✔
454
        leaf->list().back().attachCommentAfter(comment);
2,466✔
455
        return positioned(leaf, filepos);
2,466✔
456
    }
40,079✔
457

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

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

466
        bool has_captures = false;
3,160✔
467

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

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

491
                NodeType type = NodeType::Unused;
750✔
492
                if (modifier == "mut")
750✔
493
                    type = NodeType::MutArg;
73✔
494
                else if (modifier == "ref")
677✔
495
                    type = NodeType::RefArg;
677✔
496

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

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

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

518
                args->push_back(positioned(Node(NodeType::Symbol, symbol_name), pos));
4,101✔
519
            }
7,257✔
520

521
            if (!comment.empty())
5,136✔
522
                args->list().back().attachNearestCommentBefore(comment);
12✔
523
            comment = newlineOrComment();
5,136✔
524
        }
8,295✔
525

526
        if (accept(IsChar(')')))
3,156✔
527
            return positioned(args, filepos);
3,155✔
528
        return std::nullopt;
1✔
529
    }
3,163✔
530

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

535
        if (!oneOf({ "fun" }))
22,391✔
536
            return std::nullopt;
19,208✔
537
        leaf->push_back(Node(Keyword::Fun));
3,183✔
538

539
        const std::string comment_before_args = newlineOrComment();
3,183✔
540

541
        while (m_allow_macro_behavior > 0)
3,183✔
542
        {
543
            const auto position = getCount();
23✔
544

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

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

571
        const auto position = getCount();
3,160✔
572
        const auto args_file_pos = getCursor();
3,160✔
573
        if (auto args = functionArgs(args_file_pos); args.has_value())
6,320✔
574
            leaf->push_back(args.value().attachNearestCommentBefore(comment_before_args));
3,155✔
575
        else
576
        {
577
            backtrack(position);
2✔
578

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

585
        const std::string comment = newlineOrComment();
3,156✔
586

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

592
        return positioned(leaf, filepos);
3,154✔
593
    }
22,398✔
594

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

599
        if (!oneOf({ "$if" }))
16,809✔
600
            return std::nullopt;
16,754✔
601
        leaf->push_back(Node(Keyword::If));
55✔
602

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

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

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

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

625
        return positioned(leaf, filepos);
53✔
626
    }
16,811✔
627

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

633
        std::optional<Node> args { NodeType::List };
134✔
634

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

638
        std::vector<std::string> names;
134✔
639
        while (!isEOF())
324✔
640
        {
641
            const auto pos = getCount();
323✔
642

643
            std::string arg_name;
323✔
644
            if (!name(&arg_name))
323✔
645
                break;
132✔
646

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

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

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

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

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

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

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

682
        return positioned(args, filepos);
96✔
683
    }
163✔
684

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

689
        auto context = generateErrorContextAtCurrentPosition();
37,554✔
690
        if (!accept(IsChar('(')))
37,554✔
691
            return std::nullopt;
20,800✔
692

693
        if (!oneOf({ "macro" }))
16,754✔
694
            return std::nullopt;
16,594✔
695
        std::string comment = newlineOrComment();
160✔
696
        leaf->attachNearestCommentBefore(comment);
160✔
697

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

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

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

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

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

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

728
        ++m_allow_macro_behavior;
96✔
729
        const auto value = nodeOrValue();
96✔
730
        --m_allow_macro_behavior;
94✔
731

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

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

747
        leaf->list().back().attachCommentAfter(newlineOrComment());
90✔
748

749
        expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
90✔
750
        return positioned(leaf, filepos);
90✔
751
    }
37,563✔
752

753
    std::optional<Node> Parser::functionCall(const FilePosition filepos)
37,382✔
754
    {
37,382✔
755
        auto context = generateErrorContextAtCurrentPosition();
37,382✔
756
        if (!accept(IsChar('(')))
37,382✔
757
            return std::nullopt;
20,800✔
758
        std::string comment = newlineOrComment();
16,582✔
759

760
        const auto func_name_pos = getCursor();
16,582✔
761
        std::optional<Node> func;
16,582✔
762
        if (auto sym_or_field = anyAtomOf({ NodeType::Symbol, NodeType::Field }); sym_or_field.has_value())
33,164✔
763
            func = sym_or_field->attachNearestCommentBefore(comment);
15,521✔
764
        else if (auto nested = node(); nested.has_value())
2,122✔
765
            func = nested->attachNearestCommentBefore(comment);
36✔
766
        else
767
            return std::nullopt;
1✔
768

769
        std::optional<Node> leaf { NodeType::List };
15,557✔
770
        leaf->push_back(positioned(func.value(), func_name_pos));
15,557✔
771

772
        comment = newlineOrComment();
15,557✔
773

774
        while (!isEOF())
112,543✔
775
        {
776
            if (auto arg = nodeOrValue(); arg.has_value())
225,078✔
777
            {
778
                leaf->push_back(arg.value().attachNearestCommentBefore(comment));
96,986✔
779
                comment = newlineOrComment();
96,986✔
780
            }
96,986✔
781
            else
782
                break;
15,546✔
783
        }
784

785
        leaf->list().back().attachCommentAfter(comment);
15,550✔
786
        comment = newlineOrComment();
15,550✔
787
        if (!comment.empty())
15,550✔
788
            leaf->list().back().attachCommentAfter(comment);
×
789

790
        expectSuffixOrError(')', fmt::format("in function call to `{}'", func.value().repr()), context);
15,550✔
791
        return positioned(leaf, filepos);
15,546✔
792
    }
38,417✔
793

794
    std::optional<Node> Parser::list(const FilePosition filepos)
20,801✔
795
    {
20,801✔
796
        std::optional<Node> leaf { NodeType::List };
20,801✔
797

798
        auto context = generateErrorContextAtCurrentPosition();
20,801✔
799
        if (!accept(IsChar('[')))
20,801✔
800
            return std::nullopt;
19,639✔
801
        leaf->setAltSyntax(true);
1,162✔
802
        leaf->push_back(Node(NodeType::Symbol, "list"));
1,162✔
803

804
        std::string comment = newlineOrComment();
1,162✔
805
        leaf->attachNearestCommentBefore(comment);
1,162✔
806

807
        while (!isEOF())
2,666✔
808
        {
809
            if (auto value = nodeOrValue(); value.has_value())
5,330✔
810
            {
811
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
1,504✔
812
                comment = newlineOrComment();
1,504✔
813
            }
1,504✔
814
            else
815
                break;
1,161✔
816
        }
817
        leaf->list().back().attachCommentAfter(comment);
1,162✔
818

819
        expectSuffixOrError(']', "to end list definition", context);
1,162✔
820
        return positioned(leaf, filepos);
1,161✔
821
    }
20,802✔
822

823
    std::optional<Node> Parser::number(const FilePosition filepos)
160,278✔
824
    {
160,278✔
825
        std::string res;
160,278✔
826
        if (signedNumber(&res))
160,278✔
827
        {
828
            double output;
75,909✔
829
            if (Utils::isDouble(res, &output))
75,909✔
830
                return positioned(Node(output), filepos);
75,908✔
831

832
            error("Is not a valid number", filepos);
1✔
833
        }
75,909✔
834
        return std::nullopt;
84,369✔
835
    }
160,279✔
836

837
    std::optional<Node> Parser::string(const FilePosition filepos)
84,369✔
838
    {
84,369✔
839
        std::string res;
84,369✔
840
        if (accept(IsChar('"')))
84,369✔
841
        {
842
            while (true)
26,122✔
843
            {
844
                const auto pos = getCursor();
26,122✔
845

846
                if (accept(IsChar('\\')))
26,122✔
847
                {
848
                    if (m_mode != ParserMode::Interpret)
402✔
849
                        res += '\\';
22✔
850

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

919
                if (accept(IsChar('"')))
26,117✔
920
                    break;
2,148✔
921
                if (isEOF())
23,969✔
922
                    expectSuffixOrError('"', "after string");
1✔
923
            }
26,122✔
924

925
            return positioned(Node(NodeType::String, res), filepos);
2,148✔
926
        }
927
        return std::nullopt;
82,215✔
928
    }
84,375✔
929

930
    std::optional<Node> Parser::field(const FilePosition filepos)
82,186✔
931
    {
82,186✔
932
        std::string sym;
82,186✔
933
        if (!name(&sym))
82,186✔
934
            return std::nullopt;
49,621✔
935

936
        std::optional<Node> leaf { Node(NodeType::Field) };
32,565✔
937
        leaf->push_back(Node(NodeType::Symbol, sym));
32,565✔
938

939
        while (true)
33,564✔
940
        {
941
            if (leaf->list().size() == 1 && !accept(IsChar('.')))  // Symbol:abc
33,564✔
942
                return std::nullopt;
31,594✔
943

944
            if (leaf->list().size() > 1 && !accept(IsChar('.')))
1,970✔
945
                break;
970✔
946

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

954
        return positioned(leaf, filepos);
970✔
955
    }
82,187✔
956

957
    std::optional<Node> Parser::symbol(const FilePosition filepos)
81,215✔
958
    {
81,215✔
959
        std::string res;
81,215✔
960
        if (!name(&res))
81,215✔
961
            return std::nullopt;
49,621✔
962
        return positioned(Node(NodeType::Symbol, res), filepos);
31,594✔
963
    }
81,215✔
964

965
    std::optional<Node> Parser::spread(const FilePosition filepos)
82,215✔
966
    {
82,215✔
967
        std::string res;
82,215✔
968
        if (sequence("..."))
82,215✔
969
        {
970
            if (!name(&res))
29✔
971
                errorWithNextToken("Expected a name for the variadic");
1✔
972
            return positioned(Node(NodeType::Spread, res), filepos);
28✔
973
        }
974
        return std::nullopt;
82,186✔
975
    }
82,216✔
976

977
    std::optional<Node> Parser::nil(const FilePosition filepos)
49,621✔
978
    {
49,621✔
979
        if (!accept(IsChar('(')))
49,621✔
980
            return std::nullopt;
23,232✔
981

982
        const std::string comment = newlineOrComment();
26,389✔
983
        if (!accept(IsChar(')')))
26,389✔
984
            return std::nullopt;
26,291✔
985

986
        if (m_mode == ParserMode::Interpret)
98✔
987
            return positioned(Node(NodeType::Symbol, "nil").attachNearestCommentBefore(comment), filepos);
88✔
988
        return positioned(Node(NodeType::List).attachNearestCommentBefore(comment), filepos);
10✔
989
    }
49,621✔
990

991
    std::optional<Node> Parser::atom()
160,277✔
992
    {
160,277✔
993
        const auto pos = getCount();
160,277✔
994
        const auto filepos = getCursor();
160,277✔
995

996
        if (auto res = Parser::number(filepos); res.has_value())
160,277✔
997
            return res;
75,908✔
998
        backtrack(pos);
84,363✔
999

1000
        if (auto res = Parser::string(filepos); res.has_value())
84,363✔
1001
            return res;
2,148✔
1002
        backtrack(pos);
82,214✔
1003

1004
        if (auto res = Parser::spread(filepos); m_allow_macro_behavior > 0 && res.has_value())
82,214✔
1005
            return res;
28✔
1006
        backtrack(pos);
82,185✔
1007

1008
        if (auto res = Parser::field(filepos); res.has_value())
82,185✔
1009
            return res;
970✔
1010
        backtrack(pos);
81,215✔
1011

1012
        if (auto res = Parser::symbol(filepos); res.has_value())
81,215✔
1013
            return res;
31,594✔
1014
        backtrack(pos);
49,621✔
1015

1016
        if (auto res = Parser::nil(filepos); res.has_value())
49,621✔
1017
            return res;
98✔
1018
        backtrack(pos);
49,523✔
1019

1020
        return std::nullopt;
49,523✔
1021
    }
160,277✔
1022

1023
    std::optional<Node> Parser::anyAtomOf(const std::initializer_list<NodeType> types)
16,582✔
1024
    {
16,582✔
1025
        if (auto value = atom(); value.has_value())
32,104✔
1026
        {
1027
            for (const auto type : types)
31,139✔
1028
            {
1029
                if (value->nodeType() == type)
15,617✔
1030
                    return value;
15,521✔
1031
            }
15,617✔
1032
        }
1✔
1033
        return std::nullopt;
1,061✔
1034
    }
16,582✔
1035

1036
    std::optional<Node> Parser::nodeOrValue()
143,687✔
1037
    {
143,687✔
1038
        if (auto value = atom(); value.has_value())
143,687✔
1039
            return value;
95,224✔
1040
        if (auto sub_node = node(); sub_node.has_value())
48,455✔
1041
            return sub_node;
28,823✔
1042

1043
        return std::nullopt;
19,632✔
1044
    }
143,687✔
1045

1046
    std::optional<Node> Parser::wrapped(std::optional<Node> (Parser::*parser)(FilePosition), const std::string& name)
258,185✔
1047
    {
258,185✔
1048
        const auto cursor = getCursor();
258,185✔
1049
        auto context = generateErrorContextAtCurrentPosition();
258,185✔
1050
        if (!prefix('('))
258,185✔
1051
            return std::nullopt;
134,636✔
1052

1053
        const std::string comment = newlineOrComment();
123,549✔
1054

1055
        if (auto result = (this->*parser)(cursor); result.has_value())
137,065✔
1056
        {
1057
            result->attachNearestCommentBefore(result->comment() + comment);
13,516✔
1058
            result.value().attachCommentAfter(newlineOrComment());
13,516✔
1059

1060
            expectSuffixOrError(')', "after " + name, context);
13,516✔
1061

1062
            result.value().attachCommentAfter(spaceComment());
13,515✔
1063
            return result;
13,515✔
1064
        }
1065

1066
        return std::nullopt;
110,012✔
1067
    }
258,186✔
1068
}
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