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

ArkScript-lang / Ark / 17100280902

20 Aug 2025 01:45PM UTC coverage: 87.348% (-0.08%) from 87.426%
17100280902

push

github

SuperFola
feat(tests): adding parser tests to improve coverage

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

171 existing lines in 10 files now uncovered.

7553 of 8647 relevant lines covered (87.35%)

129750.62 hits per line

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

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

3
#include <fmt/core.h>
4

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

14
        m_parsers = {
1,044✔
15
            [this](FilePosition) {
40,976✔
16
                return wrapped(&Parser::letMutSet, "variable assignment or declaration");
40,454✔
17
            },
7✔
18
            [this](FilePosition) {
35,002✔
19
                return wrapped(&Parser::function, "function");
34,480✔
20
            },
6✔
21
            [this](FilePosition) {
32,584✔
22
                return wrapped(&Parser::condition, "condition");
32,062✔
23
            },
3✔
24
            [this](FilePosition) {
31,693✔
25
                return wrapped(&Parser::loop, "loop");
31,171✔
26
            },
2✔
27
            [this](const FilePosition filepos) {
30,985✔
28
                return import_(filepos);
30,463✔
29
            },
30
            [this](const FilePosition filepos) {
30,776✔
31
                return block(filepos);
30,254✔
32
            },
33
            [this](FilePosition) {
28,977✔
34
                return wrapped(&Parser::macroCondition, "$if");
28,455✔
35
            },
2✔
36
            [this](const FilePosition filepos) {
28,926✔
37
                return macro(filepos);
28,404✔
38
            },
39
            [this](FilePosition) {
28,773✔
40
                return wrapped(&Parser::del, "del");
28,251✔
41
            },
1✔
42
            [this](const FilePosition filepos) {
28,762✔
43
                return functionCall(filepos);
28,240✔
44
            },
45
            [this](const FilePosition filepos) {
16,160✔
46
                return list(filepos);
15,638✔
47
            }
48
        };
49
    }
522✔
50

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

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

66
            const auto pos = getCount();
3,394✔
67
            if (auto n = node())
6,788✔
68
            {
69
                m_ast.push_back(n->attachNearestCommentBefore(n->comment() + comment));
3,342✔
70
                m_ast.list().back().attachCommentAfter(spaceComment());
3,342✔
71
            }
3,342✔
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
        }
3,822✔
88

89
        m_logger.traceEnd();
470✔
90
    }
522✔
91

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

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

102
    Node Parser::positioned(Node node, const FilePosition cursor) const
112,485✔
103
    {
112,485✔
104
        const auto [row, col] = cursor;
337,455✔
105
        const auto [end_row, end_col] = getCursor();
337,455✔
106

107
        node.m_filename = m_filename;
112,485✔
108
        node.m_pos = FileSpan {
224,970✔
109
            .start = FilePos { .line = row, .column = col },
337,455✔
110
            .end = FilePos { .line = end_row, .column = end_col }
337,455✔
111
        };
112
        return node;
112,485✔
113
    }
112,485✔
114

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

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

123
        node->m_filename = m_filename;
28,062✔
124
        node->m_pos = FileSpan {
56,124✔
125
            .start = FilePos { .line = row, .column = col },
84,186✔
126
            .end = FilePos { .line = end_row, .column = end_col }
84,186✔
127
        };
128
        return node;
28,062✔
129
    }
28,062✔
130

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

135
        if (m_nested_nodes > MaxNestedNodes)
40,455✔
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,077✔
137

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

143
        for (auto&& parser : m_parsers)
368,326✔
144
        {
145
            result = parser(filepos);
327,872✔
146

147
            if (result)
326,796✔
148
                break;
24,701✔
149
            backtrack(position);
302,095✔
150
        }
327,872✔
151

152
        // return std::nullopt only on parsing error, nothing matched, the user provided terrible code
153
        --m_nested_nodes;
39,378✔
154
        return result;
39,378✔
155
    }
40,455✔
156

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

161
        std::string token;
23,026✔
162
        if (!oneOf({ "let", "mut", "set" }, &token))
23,026✔
163
            return std::nullopt;
17,052✔
164

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

168
        if (token == "let")
5,974✔
169
            leaf->push_back(Node(Keyword::Let));
3,071✔
170
        else if (token == "mut")
2,903✔
171
            leaf->push_back(Node(Keyword::Mut));
1,602✔
172
        else  // "set"
173
            leaf->push_back(Node(Keyword::Set));
1,301✔
174

175
        if (m_allow_macro_behavior > 0)
5,974✔
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
UNCOV
188
                backtrack(position);
×
189
        }
23✔
190

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

198
            leaf->push_back(Node(NodeType::Symbol, symbol_name));
5,949✔
199
        }
5,951✔
200

201
        comment = newlineOrComment();
5,971✔
202
        if (auto value = nodeOrValue(); value.has_value())
11,942✔
203
            leaf->push_back(value.value().attachNearestCommentBefore(comment));
5,967✔
204
        else
UNCOV
205
            errorWithNextToken("Expected a value");
×
206

207
        return positioned(leaf, filepos);
5,967✔
208
    }
23,033✔
209

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

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

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

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

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

227
        return positioned(leaf, filepos);
10✔
228
    }
12,615✔
229

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

234
        if (!oneOf({ "if" }))
14,634✔
235
            return std::nullopt;
13,743✔
236

237
        std::string comment = newlineOrComment();
891✔
238

239
        leaf->push_back(Node(Keyword::If));
891✔
240

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

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

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

261
        return positioned(leaf, filepos);
889✔
262
    }
14,636✔
263

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

268
        if (!oneOf({ "while" }))
13,743✔
269
            return std::nullopt;
13,035✔
270

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

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

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

285
        return positioned(leaf, filepos);
706✔
286
    }
13,745✔
287

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

292
        auto context = generateErrorContextAtCurrentPosition();
30,463✔
293
        if (!accept(IsChar('(')))
30,463✔
294
            return std::nullopt;
17,428✔
295

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

299
        if (!oneOf({ "import" }))
13,035✔
300
            return std::nullopt;
12,826✔
301

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

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

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

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

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

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

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

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

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

354
                space();
10✔
355
                expectSuffixOrError(')', fmt::format("in import `{}'", import_data.toPackageString()), context);
10✔
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;
10✔
359
                import_data.is_glob = true;
10✔
360
                m_imports.push_back(import_data);
10✔
361

362
                return positioned(leaf, filepos);
10✔
363
            }
364
            else
365
                break;
194✔
366
        }
362✔
367

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

374
            while (!isEOF())
118✔
375
            {
376
                if (accept(IsChar(':')))  // parsing potential :a :b :c
118✔
377
                {
378
                    const auto symbol_pos = getCursor();
115✔
379
                    std::string symbol_name;
115✔
380
                    if (!name(&symbol_name))
115✔
381
                        errorWithNextToken("Expected a valid symbol to import");
1✔
382
                    if (symbol_name == "*")
114✔
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() == '*')
113✔
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));
112✔
389
                    comment.clear();
112✔
390

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

396
                if (!space())
115✔
397
                    break;
73✔
398
                comment = newlineOrComment();
42✔
399
            }
400

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

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

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

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

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

422
        auto context = generateErrorContextAtCurrentPosition();
30,254✔
423
        bool alt_syntax = false;
30,254✔
424
        std::string comment;
30,254✔
425
        if (accept(IsChar('(')))
30,254✔
426
        {
427
            comment = newlineOrComment();
12,826✔
428
            if (!oneOf({ "begin" }))
12,826✔
429
                return std::nullopt;
12,818✔
430
        }
8✔
431
        else if (accept(IsChar('{')))
17,428✔
432
            alt_syntax = true;
1,791✔
433
        else
434
            return std::nullopt;
15,637✔
435

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

439
        comment = newlineOrComment();
1,799✔
440

441
        while (!isEOF())
8,345✔
442
        {
443
            if (auto value = nodeOrValue(); value.has_value())
16,688✔
444
            {
445
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
6,546✔
446
                comment = newlineOrComment();
6,546✔
447
            }
6,546✔
448
            else
449
                break;
1,797✔
450
        }
451

452
        comment += newlineOrComment();
1,798✔
453
        expectSuffixOrError(alt_syntax ? '}' : ')', "to close block", context);
1,798✔
454
        leaf->list().back().attachCommentAfter(comment);
1,797✔
455
        return positioned(leaf, filepos);
1,797✔
456
    }
30,256✔
457

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

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

466
        bool has_captures = false;
2,396✔
467

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

478
                args->push_back(positioned(Node(NodeType::Capture, capture), pos));
267✔
479
            }
268✔
480
            else
481
            {
482
                std::string symbol_name;
6,015✔
483
                if (!name(&symbol_name))
6,015✔
484
                    break;
2,393✔
485
                if (has_captures)
3,622✔
486
                    error("Captured variables should be at the end of the argument list", pos);
1✔
487

488
                args->push_back(positioned(Node(NodeType::Symbol, symbol_name), pos));
3,621✔
489
            }
6,015✔
490

491
            if (!comment.empty())
3,888✔
492
                args->list().back().attachNearestCommentBefore(comment);
8✔
493
            comment = newlineOrComment();
3,888✔
494
        }
6,283✔
495

496
        if (accept(IsChar(')')))
2,394✔
497
            return positioned(args, filepos);
2,392✔
498
        return std::nullopt;
2✔
499
    }
2,398✔
500

501
    std::optional<Node> Parser::function(const FilePosition filepos)
17,052✔
502
    {
17,052✔
503
        std::optional<Node> leaf { NodeType::List };
17,052✔
504

505
        if (!oneOf({ "fun" }))
17,052✔
506
            return std::nullopt;
14,634✔
507
        leaf->push_back(Node(Keyword::Fun));
2,418✔
508

509
        const std::string comment_before_args = newlineOrComment();
2,418✔
510

511
        while (m_allow_macro_behavior > 0)
2,418✔
512
        {
513
            const auto position = getCount();
22✔
514

515
            // args
516
            if (const auto value = nodeOrValue(); value.has_value())
44✔
517
            {
518
                // if value is nil, just add an empty argument bloc to prevent bugs when
519
                // declaring functions inside macros
520
                const Node& args = value.value();
22✔
521
                if (args.nodeType() == NodeType::Symbol && args.string() == "nil")
22✔
522
                    leaf->push_back(Node(NodeType::List));
5✔
523
                else
524
                    leaf->push_back(args);
17✔
525
            }
22✔
526
            else
527
            {
UNCOV
528
                backtrack(position);
×
UNCOV
529
                break;
×
530
            }
531

532
            const std::string comment = newlineOrComment();
22✔
533
            // body
534
            if (auto value = nodeOrValue(); value.has_value())
44✔
535
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
21✔
536
            else
537
                errorWithNextToken("Expected a body for the function");
1✔
538
            return positioned(leaf, filepos);
21✔
539
        }
22✔
540

541
        const auto position = getCount();
2,396✔
542
        const auto args_file_pos = getCursor();
2,396✔
543
        if (auto args = functionArgs(args_file_pos); args.has_value())
4,792✔
544
            leaf->push_back(args.value().attachNearestCommentBefore(comment_before_args));
2,392✔
545
        else
546
        {
547
            backtrack(position);
2✔
548

549
            if (auto value = nodeOrValue(); value.has_value())
4✔
550
                leaf->push_back(value.value().attachNearestCommentBefore(comment_before_args));
1✔
551
            else
UNCOV
552
                errorWithNextToken("Expected an argument list");
×
553
        }
554

555
        const std::string comment = newlineOrComment();
2,393✔
556

557
        if (auto value = nodeOrValue(); value.has_value())
4,786✔
558
            leaf->push_back(value.value().attachNearestCommentBefore(comment));
2,391✔
559
        else
560
            errorWithNextToken("Expected a body for the function");
2✔
561

562
        return positioned(leaf, filepos);
2,391✔
563
    }
17,058✔
564

565
    std::optional<Node> Parser::macroCondition(const FilePosition filepos)
12,818✔
566
    {
12,818✔
567
        std::optional<Node> leaf { NodeType::Macro };
12,818✔
568

569
        if (!oneOf({ "$if" }))
12,818✔
570
            return std::nullopt;
12,767✔
571
        leaf->push_back(Node(Keyword::If));
51✔
572

573
        std::string comment = newlineOrComment();
51✔
574
        leaf->attachNearestCommentBefore(comment);
51✔
575

576
        if (const auto cond_expr = nodeOrValue(); cond_expr.has_value())
102✔
577
            leaf->push_back(cond_expr.value());
50✔
578
        else
579
            errorWithNextToken("$if need a valid condition");
1✔
580

581
        comment = newlineOrComment();
50✔
582
        if (auto value_if_true = nodeOrValue(); value_if_true.has_value())
100✔
583
            leaf->push_back(value_if_true.value().attachNearestCommentBefore(comment));
49✔
584
        else
585
            errorWithNextToken("Expected a node or value after condition");
1✔
586

587
        comment = newlineOrComment();
49✔
588
        if (auto value_if_false = nodeOrValue(); value_if_false.has_value())
88✔
589
        {
590
            leaf->push_back(value_if_false.value().attachNearestCommentBefore(comment));
39✔
591
            comment = newlineOrComment();
39✔
592
            leaf->list().back().attachCommentAfter(comment);
39✔
593
        }
39✔
594

595
        return positioned(leaf, filepos);
49✔
596
    }
12,820✔
597

598
    std::optional<Node> Parser::macroArgs(const FilePosition filepos)
152✔
599
    {
152✔
600
        if (!accept(IsChar('(')))
156✔
601
            return std::nullopt;
24✔
602

603
        std::optional<Node> args { NodeType::List };
128✔
604

605
        std::string comment = newlineOrComment();
128✔
606
        args->attachNearestCommentBefore(comment);
128✔
607

608
        std::vector<std::string> names;
128✔
609
        while (!isEOF())
311✔
610
        {
611
            const auto pos = getCount();
310✔
612

613
            std::string arg_name;
310✔
614
            if (!name(&arg_name))
310✔
615
                break;
126✔
616

617
            comment = newlineOrComment();
184✔
618
            args->push_back(Node(NodeType::Symbol, arg_name).attachNearestCommentBefore(comment));
184✔
619

620
            if (std::ranges::find(names, arg_name) != names.end())
184✔
621
            {
622
                backtrack(pos);
1✔
623
                errorWithNextToken(fmt::format("Argument names must be unique, can not reuse `{}'", arg_name));
1✔
UNCOV
624
            }
×
625
            names.push_back(arg_name);
183✔
626
        }
310✔
627

628
        const auto pos = getCount();
127✔
629
        if (sequence("..."))
127✔
630
        {
631
            std::string spread_name;
46✔
632
            if (!name(&spread_name))
46✔
633
                errorWithNextToken("Expected a name for the variadic arguments list");
2✔
634

635
            args->push_back(Node(NodeType::Spread, spread_name));
44✔
636
            args->list().back().attachCommentAfter(newlineOrComment());
44✔
637

638
            if (std::ranges::find(names, spread_name) != names.end())
44✔
639
            {
640
                backtrack(pos);
1✔
641
                errorWithNextToken(fmt::format("Argument names must be unique, can not reuse `{}'", spread_name));
1✔
UNCOV
642
            }
×
643
        }
46✔
644

645
        if (!accept(IsChar(')')))
124✔
646
            return std::nullopt;
32✔
647

648
        comment = newlineOrComment();
92✔
649
        if (!comment.empty())
92✔
650
            args->attachCommentAfter(comment);
4✔
651

652
        return positioned(args, filepos);
92✔
653
    }
156✔
654

655
    std::optional<Node> Parser::macro(const FilePosition filepos)
28,404✔
656
    {
28,404✔
657
        std::optional<Node> leaf { NodeType::Macro };
28,404✔
658

659
        auto context = generateErrorContextAtCurrentPosition();
28,404✔
660
        if (!accept(IsChar('(')))
28,404✔
661
            return std::nullopt;
15,637✔
662

663
        if (!oneOf({ "macro" }))
12,767✔
664
            return std::nullopt;
12,614✔
665
        std::string comment = newlineOrComment();
153✔
666
        leaf->attachNearestCommentBefore(comment);
153✔
667

668
        std::string symbol_name;
153✔
669
        if (!name(&symbol_name))
153✔
670
            errorWithNextToken("Expected a symbol to declare a macro");
1✔
671

672
        comment = newlineOrComment();
152✔
673
        leaf->push_back(Node(NodeType::Symbol, symbol_name).attachNearestCommentBefore(comment));
152✔
674

675
        const auto position = getCount();
152✔
676
        const auto args_file_pos = getCursor();
152✔
677
        if (const auto args = macroArgs(args_file_pos); args.has_value())
304✔
678
            leaf->push_back(args.value());
92✔
679
        else
680
        {
681
            // if we couldn't parse arguments, then we have a value
682
            backtrack(position);
56✔
683

684
            ++m_allow_macro_behavior;
56✔
685
            const auto value = nodeOrValue();
56✔
686
            --m_allow_macro_behavior;
54✔
687

688
            if (value.has_value())
54✔
689
                leaf->push_back(value.value());
53✔
690
            else
691
                errorWithNextToken(fmt::format("Expected an argument list, atom or node while defining macro `{}'", symbol_name));
1✔
692

693
            leaf->list().back().attachCommentAfter(newlineOrComment());
53✔
694
            expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
53✔
695
            return positioned(leaf, filepos);
52✔
696
        }
56✔
697

698
        ++m_allow_macro_behavior;
92✔
699
        const auto value = nodeOrValue();
92✔
700
        --m_allow_macro_behavior;
91✔
701

702
        if (value.has_value())
91✔
703
            leaf->push_back(value.value());
89✔
704
        else if (leaf->list().size() == 2)  // the argument list is actually a function call and it's okay
2✔
705
        {
706
            leaf->list().back().attachCommentAfter(newlineOrComment());
2✔
707

708
            expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
2✔
709
            return positioned(leaf, filepos);
2✔
710
        }
711
        else
712
        {
UNCOV
713
            backtrack(position);
×
UNCOV
714
            errorWithNextToken(fmt::format("Expected a value while defining macro `{}'", symbol_name), context);
×
715
        }
716

717
        leaf->list().back().attachCommentAfter(newlineOrComment());
89✔
718

719
        expectSuffixOrError(')', fmt::format("to close macro `{}'", symbol_name), context);
89✔
720
        return positioned(leaf, filepos);
89✔
721
    }
28,413✔
722

723
    std::optional<Node> Parser::functionCall(const FilePosition filepos)
28,240✔
724
    {
28,240✔
725
        auto context = generateErrorContextAtCurrentPosition();
28,240✔
726
        if (!accept(IsChar('(')))
28,240✔
727
            return std::nullopt;
15,637✔
728
        std::string comment = newlineOrComment();
12,603✔
729

730
        const auto func_name_pos = getCursor();
12,603✔
731
        std::optional<Node> func;
12,603✔
732
        if (auto sym_or_field = anyAtomOf({ NodeType::Symbol, NodeType::Field }); sym_or_field.has_value())
25,206✔
733
            func = sym_or_field->attachNearestCommentBefore(comment);
11,556✔
734
        else if (auto nested = node(); nested.has_value())
2,094✔
735
            func = nested->attachNearestCommentBefore(comment);
22✔
736
        else
737
            return std::nullopt;
1✔
738

739
        std::optional<Node> leaf { NodeType::List };
11,578✔
740
        leaf->push_back(positioned(func.value(), func_name_pos));
11,578✔
741

742
        comment = newlineOrComment();
11,578✔
743

744
        while (!isEOF())
97,341✔
745
        {
746
            if (auto arg = nodeOrValue(); arg.has_value())
194,674✔
747
            {
748
                leaf->push_back(arg.value().attachNearestCommentBefore(comment));
85,763✔
749
                comment = newlineOrComment();
85,763✔
750
            }
85,763✔
751
            else
752
                break;
11,568✔
753
        }
754

755
        leaf->list().back().attachCommentAfter(comment);
11,572✔
756
        comment = newlineOrComment();
11,572✔
757
        if (!comment.empty())
11,572✔
UNCOV
758
            leaf->list().back().attachCommentAfter(comment);
×
759

760
        expectSuffixOrError(')', fmt::format("in function call to `{}'", func.value().repr()), context);
11,572✔
761
        return positioned(leaf, filepos);
11,568✔
762
    }
29,274✔
763

764
    std::optional<Node> Parser::list(const FilePosition filepos)
15,638✔
765
    {
15,638✔
766
        std::optional<Node> leaf { NodeType::List };
15,638✔
767

768
        auto context = generateErrorContextAtCurrentPosition();
15,638✔
769
        if (!accept(IsChar('[')))
15,638✔
770
            return std::nullopt;
14,677✔
771
        leaf->setAltSyntax(true);
961✔
772
        leaf->push_back(Node(NodeType::Symbol, "list"));
961✔
773

774
        std::string comment = newlineOrComment();
961✔
775
        leaf->attachNearestCommentBefore(comment);
961✔
776

777
        while (!isEOF())
2,280✔
778
        {
779
            if (auto value = nodeOrValue(); value.has_value())
4,558✔
780
            {
781
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
1,319✔
782
                comment = newlineOrComment();
1,319✔
783
            }
1,319✔
784
            else
785
                break;
960✔
786
        }
787
        leaf->list().back().attachCommentAfter(comment);
961✔
788

789
        expectSuffixOrError(']', "to end list definition", context);
961✔
790
        return positioned(leaf, filepos);
960✔
791
    }
15,639✔
792

793
    std::optional<Node> Parser::number(const FilePosition filepos)
133,379✔
794
    {
133,379✔
795
        std::string res;
133,379✔
796
        if (signedNumber(&res))
133,379✔
797
        {
798
            double output;
70,506✔
799
            if (Utils::isDouble(res, &output))
70,506✔
800
                return positioned(Node(output), filepos);
70,505✔
801

802
            error("Is not a valid number", filepos);
1✔
803
        }
70,506✔
804
        return std::nullopt;
62,873✔
805
    }
133,380✔
806

807
    std::optional<Node> Parser::string(const FilePosition filepos)
62,873✔
808
    {
62,873✔
809
        std::string res;
62,873✔
810
        if (accept(IsChar('"')))
62,873✔
811
        {
812
            while (true)
20,926✔
813
            {
814
                const auto pos = getCursor();
20,926✔
815

816
                if (accept(IsChar('\\')))
20,926✔
817
                {
818
                    if (!m_interpret)
190✔
819
                        res += '\\';
22✔
820

821
                    if (accept(IsChar('"')))
190✔
822
                        res += '"';
23✔
823
                    else if (accept(IsChar('\\')))
167✔
824
                        res += '\\';
27✔
825
                    else if (accept(IsChar('n')))
140✔
826
                        res += m_interpret ? '\n' : 'n';
68✔
827
                    else if (accept(IsChar('t')))
72✔
828
                        res += m_interpret ? '\t' : 't';
31✔
829
                    else if (accept(IsChar('v')))
41✔
830
                        res += m_interpret ? '\v' : 'v';
2✔
831
                    else if (accept(IsChar('r')))
39✔
832
                        res += m_interpret ? '\r' : 'r';
20✔
833
                    else if (accept(IsChar('a')))
19✔
834
                        res += m_interpret ? '\a' : 'a';
2✔
835
                    else if (accept(IsChar('b')))
17✔
836
                        res += m_interpret ? '\b' : 'b';
2✔
837
                    else if (accept(IsChar('f')))
15✔
838
                        res += m_interpret ? '\f' : 'f';
2✔
839
                    else if (accept(IsChar('u')))
13✔
840
                    {
841
                        std::string seq;
5✔
842
                        if (hexNumber(4, &seq))
5✔
843
                        {
844
                            if (m_interpret)
4✔
845
                            {
846
                                char utf8_str[5];
2✔
847
                                utf8::decode(seq.c_str(), utf8_str);
2✔
848
                                if (*utf8_str == '\0')
2✔
UNCOV
849
                                    error("Invalid escape sequence", pos);
×
850
                                res += utf8_str;
2✔
851
                            }
2✔
852
                            else
853
                                res += "u" + seq;
2✔
854
                        }
4✔
855
                        else
856
                            error("Invalid escape sequence, expected 4 hex digits: \\uabcd", pos);
1✔
857
                    }
5✔
858
                    else if (accept(IsChar('U')))
8✔
859
                    {
860
                        std::string seq;
6✔
861
                        if (hexNumber(8, &seq))
6✔
862
                        {
863
                            if (m_interpret)
5✔
864
                            {
865
                                std::size_t begin = 0;
3✔
866
                                for (; seq[begin] == '0'; ++begin)
9✔
867
                                    ;
868
                                char utf8_str[5];
3✔
869
                                utf8::decode(seq.c_str() + begin, utf8_str);
3✔
870
                                if (*utf8_str == '\0')
3✔
871
                                    error("Invalid escape sequence", pos);
1✔
872
                                res += utf8_str;
2✔
873
                            }
3✔
874
                            else
875
                                res += "U" + seq;
2✔
876
                        }
4✔
877
                        else
878
                            error("Invalid escape sequence, expected 8 hex digits: \\UABCDEF78", pos);
1✔
879
                    }
6✔
880
                    else
881
                    {
882
                        backtrack(getCount() - 1);
2✔
883
                        error("Unknown escape sequence", pos);
2✔
884
                    }
885
                }
185✔
886
                else
887
                    accept(IsNot(IsEither(IsChar('\\'), IsChar('"'))), &res);
20,736✔
888

889
                if (accept(IsChar('"')))
20,921✔
890
                    break;
1,614✔
891
                if (isEOF())
19,307✔
892
                    expectSuffixOrError('"', "after string");
1✔
893
            }
20,926✔
894

895
            return positioned(Node(NodeType::String, res), filepos);
1,614✔
896
        }
897
        return std::nullopt;
61,253✔
898
    }
62,879✔
899

900
    std::optional<Node> Parser::field(const FilePosition filepos)
61,225✔
901
    {
61,225✔
902
        std::string sym;
61,225✔
903
        if (!name(&sym))
61,225✔
904
            return std::nullopt;
37,141✔
905

906
        std::optional<Node> leaf { Node(NodeType::Field) };
24,084✔
907
        leaf->push_back(Node(NodeType::Symbol, sym));
24,084✔
908

909
        while (true)
24,989✔
910
        {
911
            if (leaf->list().size() == 1 && !accept(IsChar('.')))  // Symbol:abc
24,989✔
912
                return std::nullopt;
23,207✔
913

914
            if (leaf->list().size() > 1 && !accept(IsChar('.')))
1,782✔
915
                break;
876✔
916

917
            const auto filepos_inner = getCursor();
906✔
918
            std::string res;
906✔
919
            if (!name(&res))
906✔
920
                errorWithNextToken("Expected a field name: <symbol>.<field>");
1✔
921
            leaf->push_back(positioned(Node(NodeType::Symbol, res), filepos_inner));
905✔
922
        }
906✔
923

924
        return positioned(leaf, filepos);
876✔
925
    }
61,226✔
926

927
    std::optional<Node> Parser::symbol(const FilePosition filepos)
60,348✔
928
    {
60,348✔
929
        std::string res;
60,348✔
930
        if (!name(&res))
60,348✔
931
            return std::nullopt;
37,141✔
932
        return positioned(Node(NodeType::Symbol, res), filepos);
23,207✔
933
    }
60,348✔
934

935
    std::optional<Node> Parser::spread(const FilePosition filepos)
61,253✔
936
    {
61,253✔
937
        std::string res;
61,253✔
938
        if (sequence("..."))
61,253✔
939
        {
940
            if (!name(&res))
28✔
UNCOV
941
                errorWithNextToken("Expected a name for the variadic");
×
942
            return positioned(Node(NodeType::Spread, res), filepos);
28✔
943
        }
944
        return std::nullopt;
61,225✔
945
    }
61,253✔
946

947
    std::optional<Node> Parser::nil(const FilePosition filepos)
37,141✔
948
    {
37,141✔
949
        if (!accept(IsChar('(')))
37,141✔
950
            return std::nullopt;
17,403✔
951

952
        const std::string comment = newlineOrComment();
19,738✔
953
        if (!accept(IsChar(')')))
19,738✔
954
            return std::nullopt;
19,657✔
955

956
        if (m_interpret)
81✔
957
            return positioned(Node(NodeType::Symbol, "nil").attachNearestCommentBefore(comment), filepos);
73✔
958
        return positioned(Node(NodeType::List).attachNearestCommentBefore(comment), filepos);
8✔
959
    }
37,141✔
960

961
    std::optional<Node> Parser::atom()
133,378✔
962
    {
133,378✔
963
        const auto pos = getCount();
133,378✔
964
        const auto filepos = getCursor();
133,378✔
965

966
        if (auto res = Parser::number(filepos); res.has_value())
133,378✔
967
            return res;
70,505✔
968
        backtrack(pos);
62,867✔
969

970
        if (auto res = Parser::string(filepos); res.has_value())
62,867✔
971
            return res;
1,614✔
972
        backtrack(pos);
61,253✔
973

974
        if (auto res = Parser::spread(filepos); m_allow_macro_behavior > 0 && res.has_value())
61,253✔
975
            return res;
28✔
976
        backtrack(pos);
61,224✔
977

978
        if (auto res = Parser::field(filepos); res.has_value())
61,224✔
979
            return res;
876✔
980
        backtrack(pos);
60,348✔
981

982
        if (auto res = Parser::symbol(filepos); res.has_value())
60,348✔
983
            return res;
23,207✔
984
        backtrack(pos);
37,141✔
985

986
        if (auto res = Parser::nil(filepos); res.has_value())
37,141✔
987
            return res;
81✔
988
        backtrack(pos);
37,060✔
989

990
        return std::nullopt;
37,060✔
991
    }
133,378✔
992

993
    std::optional<Node> Parser::anyAtomOf(const std::initializer_list<NodeType> types)
12,603✔
994
    {
12,603✔
995
        if (auto value = atom(); value.has_value())
24,160✔
996
        {
997
            for (const auto type : types)
23,201✔
998
            {
999
                if (value->nodeType() == type)
11,644✔
1000
                    return value;
11,556✔
1001
            }
11,644✔
1002
        }
1✔
1003
        return std::nullopt;
1,047✔
1004
    }
12,603✔
1005

1006
    std::optional<Node> Parser::nodeOrValue()
120,768✔
1007
    {
120,768✔
1008
        if (auto value = atom(); value.has_value())
120,768✔
1009
            return value;
84,754✔
1010
        if (auto sub_node = node(); sub_node.has_value())
36,007✔
1011
            return sub_node;
21,337✔
1012

1013
        return std::nullopt;
14,670✔
1014
    }
120,768✔
1015

1016
    std::optional<Node> Parser::wrapped(std::optional<Node> (Parser::*parser)(FilePosition), const std::string& name)
194,873✔
1017
    {
194,873✔
1018
        const auto cursor = getCursor();
194,873✔
1019
        auto context = generateErrorContextAtCurrentPosition();
194,873✔
1020
        if (!prefix('('))
194,873✔
1021
            return std::nullopt;
100,986✔
1022

1023
        const std::string comment = newlineOrComment();
93,887✔
1024

1025
        if (auto result = (this->*parser)(cursor); result.has_value())
103,920✔
1026
        {
1027
            result->attachNearestCommentBefore(result->comment() + comment);
10,033✔
1028
            result.value().attachCommentAfter(newlineOrComment());
10,033✔
1029

1030
            expectSuffixOrError(')', "after " + name, context);
10,033✔
1031

1032
            result.value().attachCommentAfter(spaceComment());
10,032✔
1033
            return result;
10,032✔
1034
        }
1035

1036
        return std::nullopt;
83,834✔
1037
    }
194,874✔
1038
}
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