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

ArkScript-lang / Ark / 27218257428

09 Jun 2026 03:48PM UTC coverage: 94.338% (-0.005%) from 94.343%
27218257428

push

github

SuperFola
chore: ArkScript 4.7.0 prerelease 1

10281 of 10898 relevant lines covered (94.34%)

925814.04 hits per line

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

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

14
        m_parsers = {
1,752✔
15
            [this](FilePosition) {
147,097✔
16
                return wrapped(&Parser::letMutSet, "variable assignment or declaration");
146,221✔
17
            },
9✔
18
            [this](FilePosition) {
126,114✔
19
                return wrapped(&Parser::function, "function");
125,238✔
20
            },
8✔
21
            [this](FilePosition) {
118,686✔
22
                return wrapped(&Parser::condition, "condition");
117,810✔
23
            },
3✔
24
            [this](FilePosition) {
114,756✔
25
                return wrapped(&Parser::loop, "loop");
113,880✔
26
            },
2✔
27
            [this](const FilePosition filepos) {
112,191✔
28
                return import_(filepos);
111,315✔
29
            },
30
            [this](const FilePosition filepos) {
111,865✔
31
                return block(filepos);
110,989✔
32
            },
33
            [this](FilePosition) {
104,774✔
34
                return wrapped(&Parser::macroCondition, "$if");
103,898✔
35
            },
2✔
36
            [this](const FilePosition filepos) {
104,671✔
37
                return macro(filepos);
103,795✔
38
            },
39
            [this](FilePosition) {
104,420✔
40
                return wrapped(&Parser::del, "del");
103,544✔
41
            },
1✔
42
            [this](const FilePosition filepos) {
104,408✔
43
                return functionCall(filepos);
103,532✔
44
            },
45
            [this](const FilePosition filepos) {
59,078✔
46
                return list(filepos);
58,202✔
47
            }
48
        };
49
    }
876✔
50

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

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

66
            const auto pos = getCount();
8,814✔
67
            if (auto n = node())
17,628✔
68
            {
69
                m_ast.push_back(n->attachNearestCommentBefore(n->comment() + comment));
8,755✔
70
                m_ast.list().back().attachCommentAfter(spaceComment());
8,755✔
71
            }
8,755✔
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
        }
9,586✔
88

89
        m_logger.traceEnd();
817✔
90
    }
876✔
91

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

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

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

107
        node.m_filename = m_filename;
268,630✔
108
        node.m_pos = FileSpan {
537,260✔
109
            .start = FilePos { .line = row, .column = col },
805,890✔
110
            .end = FilePos { .line = end_row, .column = end_col }
805,890✔
111
        };
112
        return node;
268,630✔
113
    }
268,630✔
114

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

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

123
        node->m_filename = m_filename;
98,438✔
124
        node->m_pos = FileSpan {
196,876✔
125
            .start = FilePos { .line = row, .column = col },
295,314✔
126
            .end = FilePos { .line = end_row, .column = end_col }
295,314✔
127
        };
128
        return node;
98,438✔
129
    }
98,438✔
130

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

135
        if (m_nested_nodes > MaxNestedNodes)
146,222✔
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,087✔
137

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

143
        for (auto&& parser : m_parsers)
1,344,645✔
144
        {
145
            result = parser(filepos);
1,198,424✔
146

147
            if (result)
1,197,338✔
148
                break;
89,580✔
149
            backtrack(position);
1,107,758✔
150
        }
1,198,424✔
151

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

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

161
        std::string token;
80,938✔
162
        if (!oneOf({ "let", "mut", "set" }, &token))
80,938✔
163
            return std::nullopt;
59,955✔
164

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

168
        if (token == "let")
20,983✔
169
            leaf->push_back(Node(Keyword::Let));
10,166✔
170
        else if (token == "mut")
10,817✔
171
            leaf->push_back(Node(Keyword::Mut));
5,492✔
172
        else  // "set"
173
            leaf->push_back(Node(Keyword::Set));
5,325✔
174

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

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

199
            leaf->push_back(positioned(Node(NodeType::Symbol, symbol_name), value_pos));
20,900✔
200
        }
20,902✔
201

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

208
        return positioned(leaf, filepos);
20,974✔
209
    }
80,947✔
210

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

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

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

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

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

228
        return positioned(leaf, filepos);
11✔
229
    }
45,344✔
230

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

235
        if (!oneOf({ "if" }))
52,527✔
236
            return std::nullopt;
48,597✔
237

238
        std::string comment = newlineOrComment();
3,930✔
239

240
        leaf->push_back(Node(Keyword::If));
3,930✔
241

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

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

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

262
        return positioned(leaf, filepos);
3,928✔
263
    }
52,529✔
264

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

269
        if (!oneOf({ "while" }))
48,597✔
270
            return std::nullopt;
46,032✔
271

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

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

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

286
        return positioned(leaf, filepos);
2,563✔
287
    }
48,599✔
288

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

293
        auto context = generateErrorContextAtCurrentPosition();
111,315✔
294
        if (!accept(IsChar('(')))
111,315✔
295
            return std::nullopt;
65,283✔
296

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

300
        if (!oneOf({ "import" }))
46,032✔
301
            return std::nullopt;
45,706✔
302

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

397
                if (!space())
92✔
398
                    break;
62✔
399
                comment = newlineOrComment();
30✔
400
            }
401

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

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

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

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

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

423
        auto context = generateErrorContextAtCurrentPosition();
110,989✔
424
        bool alt_syntax = false;
110,989✔
425
        std::string comment;
110,989✔
426
        if (accept(IsChar('(')))
110,989✔
427
        {
428
            comment = newlineOrComment();
45,706✔
429
            if (!oneOf({ "begin" }))
45,706✔
430
                return std::nullopt;
45,697✔
431
        }
9✔
432
        else if (accept(IsChar('{')))
65,283✔
433
            alt_syntax = true;
7,082✔
434
        else
435
            return std::nullopt;
58,201✔
436

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

440
        comment = newlineOrComment();
7,091✔
441

442
        while (!isEOF())
31,496✔
443
        {
444
            if (auto value = nodeOrValue(); value.has_value())
62,990✔
445
            {
446
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
24,405✔
447
                comment = newlineOrComment();
24,405✔
448
            }
24,405✔
449
            else
450
                break;
7,089✔
451
        }
452

453
        comment += newlineOrComment();
7,090✔
454
        expectSuffixOrError(alt_syntax ? '}' : ')', "to close block", context);
7,090✔
455
        leaf->list().back().attachCommentAfter(comment);
7,089✔
456
        return positioned(leaf, filepos);
7,089✔
457
    }
110,991✔
458

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

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

467
        bool has_captures = false;
7,398✔
468

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

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

492
                NodeType type = NodeType::Unused;
2,314✔
493
                if (modifier == "mut")
2,314✔
494
                    type = NodeType::MutArg;
479✔
495
                else if (modifier == "ref")
1,835✔
496
                    type = NodeType::RefArg;
1,835✔
497

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

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

507
                args->push_back(positioned(arg_with_attr, pos));
2,313✔
508
                std::ignore = newlineOrComment();
2,313✔
509
                expect(IsChar(')'));
2,313✔
510
            }
2,315✔
511
            else
512
            {
513
                std::string symbol_name;
17,005✔
514
                if (!name(&symbol_name))
17,005✔
515
                    break;
7,393✔
516
                if (has_captures)
9,612✔
517
                    error("Captured variables should be at the end of the argument list", pos);
1✔
518

519
                args->push_back(positioned(Node(NodeType::Symbol, symbol_name), pos));
9,611✔
520
            }
17,005✔
521

522
            if (!comment.empty())
12,277✔
523
                args->list().back().attachNearestCommentBefore(comment);
12✔
524
            comment = newlineOrComment();
12,277✔
525
        }
19,674✔
526

527
        if (accept(IsChar(')')))
7,394✔
528
            return positioned(args, filepos);
7,393✔
529
        return std::nullopt;
1✔
530
    }
7,401✔
531

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

536
        if (!oneOf({ "fun" }))
59,955✔
537
            return std::nullopt;
52,527✔
538
        leaf->push_back(Node(Keyword::Fun));
7,428✔
539

540
        const std::string comment_before_args = newlineOrComment();
7,428✔
541

542
        while (m_allow_macro_behavior > 0)
7,428✔
543
        {
544
            const auto position = getCount();
30✔
545

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

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

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

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

586
        const std::string comment = newlineOrComment();
7,394✔
587

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

593
        return positioned(leaf, filepos);
7,392✔
594
    }
59,962✔
595

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

600
        if (!oneOf({ "$if" }))
45,697✔
601
            return std::nullopt;
45,594✔
602
        leaf->push_back(Node(Keyword::If));
103✔
603

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

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

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

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

626
        return positioned(leaf, filepos);
101✔
627
    }
45,699✔
628

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

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

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

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

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

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

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

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

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

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

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

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

683
        return positioned(args, filepos);
173✔
684
    }
254✔
685

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

690
        auto context = generateErrorContextAtCurrentPosition();
103,795✔
691
        if (!accept(IsChar('(')))
103,795✔
692
            return std::nullopt;
58,201✔
693

694
        if (!oneOf({ "macro" }))
45,594✔
695
            return std::nullopt;
45,343✔
696
        std::string comment = newlineOrComment();
251✔
697
        leaf->attachNearestCommentBefore(comment);
251✔
698

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

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

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

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

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

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

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

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

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

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

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

754
    std::optional<Node> Parser::functionCall(const FilePosition filepos)
103,532✔
755
    {
103,532✔
756
        auto context = generateErrorContextAtCurrentPosition();
103,532✔
757
        if (!accept(IsChar('(')))
103,532✔
758
            return std::nullopt;
58,201✔
759
        std::string comment = newlineOrComment();
45,331✔
760

761
        const auto func_name_pos = getCursor();
45,331✔
762
        std::optional<Node> func;
45,331✔
763
        if (auto sym_or_field = anyAtomOf({ NodeType::Symbol, NodeType::Field }); sym_or_field.has_value())
90,662✔
764
            func = sym_or_field->attachNearestCommentBefore(comment);
44,236✔
765
        else if (auto nested = node(); nested.has_value())
2,190✔
766
            func = nested->attachNearestCommentBefore(comment);
70✔
767
        else
768
            return std::nullopt;
1✔
769

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

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

776
        comment = newlineOrComment();
44,305✔
777

778
        while (!isEOF())
198,435✔
779
        {
780
            if (auto arg = nodeOrValue(); arg.has_value())
396,862✔
781
            {
782
                leaf->push_back(arg.value().attachNearestCommentBefore(comment));
154,130✔
783
                comment = newlineOrComment();
154,130✔
784
            }
154,130✔
785
            else
786
                break;
44,291✔
787
        }
788

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

794
        expectSuffixOrError(')', fmt::format("in function call to `{}'", func.value().repr()), context);
44,295✔
795
        return positioned(leaf, filepos);
44,291✔
796
    }
104,571✔
797

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

802
        auto context = generateErrorContextAtCurrentPosition();
58,202✔
803
        if (!accept(IsChar('[')))
58,202✔
804
            return std::nullopt;
55,555✔
805
        leaf->setAltSyntax(true);
2,647✔
806
        leaf->push_back(Node(NodeType::Symbol, "list"));
2,647✔
807

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

811
        while (!isEOF())
6,299✔
812
        {
813
            if (auto value = nodeOrValue(); value.has_value())
12,596✔
814
            {
815
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
3,652✔
816
                comment = newlineOrComment();
3,652✔
817
            }
3,652✔
818
            else
819
                break;
2,646✔
820
        }
821
        leaf->list().back().attachCommentAfter(comment);
2,647✔
822

823
        expectSuffixOrError(']', "to end list definition", context);
2,647✔
824
        return positioned(leaf, filepos);
2,646✔
825
    }
58,203✔
826

827
    std::optional<Node> Parser::number(const FilePosition filepos)
327,540✔
828
    {
327,540✔
829
        std::string res;
327,540✔
830
        if (signedNumber(&res))
327,540✔
831
        {
832
            double output;
89,163✔
833
            if (Utils::isDouble(res, &output))
89,163✔
834
                return positioned(Node(output), filepos);
89,162✔
835

836
            error("Is not a valid number", filepos);
1✔
837
        }
89,163✔
838
        return std::nullopt;
238,377✔
839
    }
327,541✔
840

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

850
                if (accept(IsChar('\\')))
88,998✔
851
                {
852
                    if (m_mode != ParserMode::Interpret)
663✔
853
                        res += '\\';
52✔
854

855
                    if (accept(IsChar('"')))
663✔
856
                        res += '"';
95✔
857
                    else if (accept(IsChar('\\')))
568✔
858
                        res += '\\';
54✔
859
                    else if (accept(IsChar('n')))
514✔
860
                        res += m_mode == ParserMode::Interpret ? '\n' : 'n';
213✔
861
                    else if (accept(IsChar('t')))
301✔
862
                        res += m_mode == ParserMode::Interpret ? '\t' : 't';
141✔
863
                    else if (accept(IsChar('v')))
160✔
864
                        res += m_mode == ParserMode::Interpret ? '\v' : 'v';
4✔
865
                    else if (accept(IsChar('r')))
156✔
866
                        res += m_mode == ParserMode::Interpret ? '\r' : 'r';
119✔
867
                    else if (accept(IsChar('a')))
37✔
868
                        res += m_mode == ParserMode::Interpret ? '\a' : 'a';
4✔
869
                    else if (accept(IsChar('b')))
33✔
870
                        res += m_mode == ParserMode::Interpret ? '\b' : 'b';
4✔
871
                    else if (accept(IsChar('f')))
29✔
872
                        res += m_mode == ParserMode::Interpret ? '\f' : 'f';
4✔
873
                    else if (accept(IsChar('u')))
25✔
874
                    {
875
                        std::string seq;
7✔
876
                        if (hexNumber(4, &seq))
7✔
877
                        {
878
                            if (m_mode == ParserMode::Interpret)
6✔
879
                            {
880
                                char utf8_str[5];
2✔
881
                                utf8::decode(seq.c_str(), utf8_str);
2✔
882
                                if (*utf8_str == '\0')
2✔
883
                                    error("Invalid escape sequence", pos);
×
884
                                res += utf8_str;
2✔
885
                            }
2✔
886
                            else
887
                                res += "u" + seq;
4✔
888
                        }
6✔
889
                        else
890
                            error("Invalid escape sequence, expected 4 hex digits: \\uabcd", pos);
1✔
891
                    }
7✔
892
                    else if (accept(IsChar('U')))
18✔
893
                    {
894
                        std::string seq;
8✔
895
                        if (hexNumber(8, &seq))
8✔
896
                        {
897
                            if (m_mode == ParserMode::Interpret)
7✔
898
                            {
899
                                std::size_t begin = 0;
3✔
900
                                for (; seq[begin] == '0'; ++begin)
9✔
901
                                    ;
902
                                char utf8_str[5];
3✔
903
                                utf8::decode(seq.c_str() + begin, utf8_str);
3✔
904
                                if (*utf8_str == '\0')
3✔
905
                                    error("Invalid escape sequence", pos);
1✔
906
                                res += utf8_str;
2✔
907
                            }
3✔
908
                            else
909
                                res += "U" + seq;
4✔
910
                        }
6✔
911
                        else
912
                            error("Invalid escape sequence, expected 8 hex digits: \\UABCDEF78", pos);
1✔
913
                    }
8✔
914
                    else if (accept(IsChar('x')))
10✔
915
                    {
916
                        std::string seq;
4✔
917
                        if (hexNumber(2, &seq))
4✔
918
                        {
919
                            if (m_mode == ParserMode::Interpret)
3✔
920
                            {
921
                                std::size_t begin = 0;
1✔
922
                                for (; seq[begin] == '0'; ++begin)
3✔
923
                                    ;
924
                                const char c = static_cast<char>(16 * utf8::details::ASCIIHexToInt[static_cast<unsigned char>(seq[0])] +
2✔
925
                                                                 utf8::details::ASCIIHexToInt[static_cast<unsigned char>(seq[1])]);
1✔
926
                                if (c == '\0')
1✔
927
                                    error("Invalid escape sequence", pos);
1✔
928
                                res += c;
×
929
                            }
1✔
930
                            else
931
                                res += "x" + seq;
2✔
932
                        }
2✔
933
                        else
934
                            error("Invalid escape sequence, expected 2 hex digits: \\x1b", pos);
1✔
935
                    }
4✔
936
                    else if (accept(IsChar('0')))
6✔
937
                    {
938
                        std::string seq;
4✔
939
                        if (octNumber(2, &seq))
4✔
940
                        {
941
                            if (m_mode == ParserMode::Interpret)
4✔
942
                            {
943
                                std::size_t begin = 0;
2✔
944
                                for (; seq[begin] == '0'; ++begin)
4✔
945
                                    ;
946
                                const char c = static_cast<char>(8 * (seq[0] - '0') + (seq[1] - '0'));
2✔
947
                                if (c == '\0')
2✔
948
                                    error("Invalid escape sequence", pos);
1✔
949
                                res += c;
1✔
950
                            }
2✔
951
                            else
952
                                res += "0" + seq;
2✔
953
                        }
3✔
954
                        else
955
                            error("Invalid escape sequence, expected 2 oct digits: \\033", pos);
×
956
                    }
4✔
957
                    else
958
                    {
959
                        backtrack(getCount() - 1);
2✔
960
                        error("Unknown escape sequence", pos);
2✔
961
                    }
962
                }
655✔
963
                else
964
                    accept(IsNot(IsEither(IsChar('\\'), IsChar('"'))), &res);
88,335✔
965

966
                if (accept(IsChar('"')))
88,990✔
967
                    break;
7,101✔
968
                if (isEOF())
81,889✔
969
                    expectSuffixOrError('"', "after string");
1✔
970
            }
88,998✔
971

972
            return positioned(Node(NodeType::String, res), filepos);
7,101✔
973
        }
974
        return std::nullopt;
231,267✔
975
    }
238,386✔
976

977
    std::optional<Node> Parser::field(const FilePosition filepos)
231,187✔
978
    {
231,187✔
979
        std::string sym;
231,187✔
980
        if (!name(&sym))
231,187✔
981
            return std::nullopt;
137,521✔
982

983
        std::optional<Node> leaf { Node(NodeType::Field) };
93,666✔
984
        leaf->push_back(Node(NodeType::Symbol, sym));
93,666✔
985

986
        while (true)
94,985✔
987
        {
988
            if (leaf->list().size() == 1 && !accept(IsChar('.')))  // Symbol:abc
94,985✔
989
                return std::nullopt;
92,375✔
990

991
            if (leaf->list().size() > 1 && !accept(IsChar('.')))
2,610✔
992
                break;
1,290✔
993

994
            const auto filepos_inner = getCursor();
1,320✔
995
            std::string res;
1,320✔
996
            if (!name(&res))
1,320✔
997
                errorWithNextToken("Expected a field name: <symbol>.<field>");
1✔
998
            leaf->push_back(positioned(Node(NodeType::Symbol, res), filepos_inner));
1,319✔
999
        }
1,320✔
1000

1001
        return positioned(leaf, filepos);
1,290✔
1002
    }
231,188✔
1003

1004
    std::optional<Node> Parser::symbol(const FilePosition filepos)
229,896✔
1005
    {
229,896✔
1006
        std::string res;
229,896✔
1007
        if (!name(&res))
229,896✔
1008
            return std::nullopt;
137,521✔
1009
        return positioned(Node(NodeType::Symbol, res), filepos);
92,375✔
1010
    }
229,896✔
1011

1012
    std::optional<Node> Parser::spread(const FilePosition filepos)
231,267✔
1013
    {
231,267✔
1014
        std::string res;
231,267✔
1015
        if (sequence("..."))
231,267✔
1016
        {
1017
            if (!name(&res))
80✔
1018
                errorWithNextToken("Expected a name for the variadic");
1✔
1019
            return positioned(Node(NodeType::Spread, res), filepos);
79✔
1020
        }
1021
        return std::nullopt;
231,187✔
1022
    }
231,268✔
1023

1024
    std::optional<Node> Parser::nil(const FilePosition filepos)
137,521✔
1025
    {
137,521✔
1026
        if (!accept(IsChar('(')))
137,521✔
1027
            return std::nullopt;
65,256✔
1028

1029
        const std::string comment = newlineOrComment();
72,265✔
1030
        if (!accept(IsChar(')')))
72,265✔
1031
            return std::nullopt;
72,151✔
1032

1033
        if (m_mode == ParserMode::Interpret)
114✔
1034
            return positioned(Node(NodeType::Symbol, "nil").attachNearestCommentBefore(comment), filepos);
104✔
1035
        return positioned(Node(NodeType::List).attachNearestCommentBefore(comment), filepos);
10✔
1036
    }
137,521✔
1037

1038
    std::optional<Node> Parser::atom()
327,539✔
1039
    {
327,539✔
1040
        const auto pos = getCount();
327,539✔
1041
        const auto filepos = getCursor();
327,539✔
1042

1043
        if (auto res = Parser::number(filepos); res.has_value())
327,539✔
1044
            return res;
89,162✔
1045
        backtrack(pos);
238,368✔
1046

1047
        if (auto res = Parser::string(filepos); res.has_value())
238,368✔
1048
            return res;
7,101✔
1049
        backtrack(pos);
231,266✔
1050

1051
        if (auto res = Parser::spread(filepos); m_allow_macro_behavior > 0 && res.has_value())
231,266✔
1052
            return res;
79✔
1053
        backtrack(pos);
231,186✔
1054

1055
        if (auto res = Parser::field(filepos); res.has_value())
231,186✔
1056
            return res;
1,290✔
1057
        backtrack(pos);
229,896✔
1058

1059
        if (auto res = Parser::symbol(filepos); res.has_value())
229,896✔
1060
            return res;
92,375✔
1061
        backtrack(pos);
137,521✔
1062

1063
        if (auto res = Parser::nil(filepos); res.has_value())
137,521✔
1064
            return res;
114✔
1065
        backtrack(pos);
137,407✔
1066

1067
        return std::nullopt;
137,407✔
1068
    }
327,539✔
1069

1070
    std::optional<Node> Parser::anyAtomOf(const std::initializer_list<NodeType> types)
45,331✔
1071
    {
45,331✔
1072
        if (auto value = atom(); value.has_value())
89,568✔
1073
        {
1074
            for (const auto type : types)
88,581✔
1075
            {
1076
                if (value->nodeType() == type)
44,344✔
1077
                    return value;
44,236✔
1078
            }
44,344✔
1079
        }
1✔
1080
        return std::nullopt;
1,095✔
1081
    }
45,331✔
1082

1083
    std::optional<Node> Parser::nodeOrValue()
282,197✔
1084
    {
282,197✔
1085
        if (auto value = atom(); value.has_value())
282,197✔
1086
            return value;
145,884✔
1087
        if (auto sub_node = node(); sub_node.has_value())
136,303✔
1088
            return sub_node;
80,755✔
1089

1090
        return std::nullopt;
55,548✔
1091
    }
282,197✔
1092

1093
    std::optional<Node> Parser::wrapped(std::optional<Node> (Parser::*parser)(FilePosition), const std::string& name)
710,591✔
1094
    {
710,591✔
1095
        const auto cursor = getCursor();
710,591✔
1096
        auto context = generateErrorContextAtCurrentPosition();
710,591✔
1097
        if (!prefix('('))
710,591✔
1098
            return std::nullopt;
377,534✔
1099

1100
        const std::string comment = newlineOrComment();
333,057✔
1101

1102
        if (auto result = (this->*parser)(cursor); result.has_value())
368,055✔
1103
        {
1104
            result->attachNearestCommentBefore(result->comment() + comment);
34,998✔
1105
            result.value().attachCommentAfter(newlineOrComment());
34,998✔
1106

1107
            if (name == "function")
34,998✔
1108
                expectSuffixOrError(')', "after function body. Did you forget to wrap the body with `{}'?", context);
7,421✔
1109
            else
1110
                expectSuffixOrError(')', "after " + name, context);
27,577✔
1111

1112
            result.value().attachCommentAfter(spaceComment());
34,996✔
1113
            return result;
34,996✔
1114
        }
1115

1116
        return std::nullopt;
298,036✔
1117
    }
710,593✔
1118
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc