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

ArkScript-lang / Ark / 20377988123

19 Dec 2025 05:45PM UTC coverage: 92.242% (+1.6%) from 90.661%
20377988123

push

github

SuperFola
chore: addressing cppcheck recommandations

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

137 existing lines in 6 files now uncovered.

8430 of 9139 relevant lines covered (92.24%)

245122.08 hits per line

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

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

14
        m_parsers = {
1,206✔
15
            [this](FilePosition) {
66,007✔
16
                return wrapped(&Parser::letMutSet, "variable assignment or declaration");
65,404✔
17
            },
8✔
18
            [this](FilePosition) {
56,346✔
19
                return wrapped(&Parser::function, "function");
55,743✔
20
            },
7✔
21
            [this](FilePosition) {
52,640✔
22
                return wrapped(&Parser::condition, "condition");
52,037✔
23
            },
3✔
24
            [this](FilePosition) {
51,037✔
25
                return wrapped(&Parser::loop, "loop");
50,434✔
26
            },
2✔
27
            [this](const FilePosition filepos) {
49,904✔
28
                return import_(filepos);
49,301✔
29
            },
30
            [this](const FilePosition filepos) {
49,675✔
31
                return block(filepos);
49,072✔
32
            },
33
            [this](FilePosition) {
46,697✔
34
                return wrapped(&Parser::macroCondition, "$if");
46,094✔
35
            },
2✔
36
            [this](const FilePosition filepos) {
46,642✔
37
                return macro(filepos);
46,039✔
38
            },
39
            [this](FilePosition) {
46,478✔
40
                return wrapped(&Parser::del, "del");
45,875✔
41
            },
1✔
42
            [this](const FilePosition filepos) {
46,466✔
43
                return functionCall(filepos);
45,863✔
44
            },
45
            [this](const FilePosition filepos) {
26,106✔
46
                return list(filepos);
25,503✔
47
            }
48
        };
49
    }
603✔
50

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

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

66
            const auto pos = getCount();
4,843✔
67
            if (auto n = node())
9,686✔
68
            {
69
                m_ast.push_back(n->attachNearestCommentBefore(n->comment() + comment));
4,788✔
70
                m_ast.list().back().attachCommentAfter(spaceComment());
4,788✔
71
            }
4,788✔
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
        }
5,351✔
88

89
        m_logger.traceEnd();
548✔
90
    }
603✔
91

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

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

102
    Node Parser::positioned(Node node, const FilePosition cursor) const
146,202✔
103
    {
146,202✔
104
        const auto [row, col] = cursor;
438,606✔
105
        const auto [end_row, end_col] = getCursor();
438,606✔
106

107
        node.m_filename = m_filename;
146,202✔
108
        node.m_pos = FileSpan {
292,404✔
109
            .start = FilePos { .line = row, .column = col },
438,606✔
110
            .end = FilePos { .line = end_row, .column = end_col }
438,606✔
111
        };
112
        return node;
146,202✔
113
    }
146,202✔
114

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

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

123
        node->m_filename = m_filename;
44,891✔
124
        node->m_pos = FileSpan {
89,782✔
125
            .start = FilePos { .line = row, .column = col },
134,673✔
126
            .end = FilePos { .line = end_row, .column = end_col }
134,673✔
127
        };
128
        return node;
44,891✔
129
    }
44,891✔
130

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

135
        if (m_nested_nodes > MaxNestedNodes)
65,405✔
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,082✔
137

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

143
        for (auto&& parser : m_parsers)
596,769✔
144
        {
145
            result = parser(filepos);
531,365✔
146

147
            if (result)
530,284✔
148
                break;
40,142✔
149
            backtrack(position);
490,142✔
150
        }
531,365✔
151

152
        // return std::nullopt only on parsing error, nothing matched, the user provided terrible code
153
        --m_nested_nodes;
64,323✔
154
        return result;
64,323✔
155
    }
65,405✔
156

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

161
        std::string token;
36,933✔
162
        if (!oneOf({ "let", "mut", "set" }, &token))
36,933✔
163
            return std::nullopt;
27,272✔
164

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

168
        if (token == "let")
9,661✔
169
            leaf->push_back(Node(Keyword::Let));
4,905✔
170
        else if (token == "mut")
4,756✔
171
            leaf->push_back(Node(Keyword::Mut));
2,567✔
172
        else  // "set"
173
            leaf->push_back(Node(Keyword::Set));
2,189✔
174

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

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

198
            leaf->push_back(Node(NodeType::Symbol, symbol_name));
9,604✔
199
        }
9,606✔
200

201
        comment = newlineOrComment();
9,658✔
202
        if (auto value = nodeOrValue(); value.has_value())
19,316✔
203
            leaf->push_back(value.value().attachNearestCommentBefore(comment));
9,653✔
204
        else
205
            errorWithNextToken("Expected a value");
×
206

207
        return positioned(leaf, filepos);
9,653✔
208
    }
36,941✔
209

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

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

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

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

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

227
        return positioned(leaf, filepos);
11✔
228
    }
20,374✔
229

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

234
        if (!oneOf({ "if" }))
23,566✔
235
            return std::nullopt;
21,963✔
236

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

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

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

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

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

261
        return positioned(leaf, filepos);
1,601✔
262
    }
23,568✔
263

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

268
        if (!oneOf({ "while" }))
21,963✔
269
            return std::nullopt;
20,830✔
270

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

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

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

285
        return positioned(leaf, filepos);
1,131✔
286
    }
21,965✔
287

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

292
        auto context = generateErrorContextAtCurrentPosition();
49,301✔
293
        if (!accept(IsChar('(')))
49,301✔
294
            return std::nullopt;
28,471✔
295

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

299
        if (!oneOf({ "import" }))
20,830✔
300
            return std::nullopt;
20,601✔
301

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

422
        auto context = generateErrorContextAtCurrentPosition();
49,072✔
423
        bool alt_syntax = false;
49,072✔
424
        std::string comment;
49,072✔
425
        if (accept(IsChar('(')))
49,072✔
426
        {
427
            comment = newlineOrComment();
20,601✔
428
            if (!oneOf({ "begin" }))
20,601✔
429
                return std::nullopt;
20,592✔
430
        }
9✔
431
        else if (accept(IsChar('{')))
28,471✔
432
            alt_syntax = true;
2,969✔
433
        else
434
            return std::nullopt;
25,502✔
435

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

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

441
        while (!isEOF())
13,552✔
442
        {
443
            if (auto value = nodeOrValue(); value.has_value())
27,102✔
444
            {
445
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
10,574✔
446
                comment = newlineOrComment();
10,574✔
447
            }
10,574✔
448
            else
449
                break;
2,976✔
450
        }
451

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

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

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

466
        bool has_captures = false;
3,683✔
467

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

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

491
                NodeType type = NodeType::Unused;
878✔
492
                if (modifier == "mut")
878✔
493
                    type = NodeType::MutArg;
104✔
494
                else if (modifier == "ref")
774✔
495
                    type = NodeType::RefArg;
774✔
496

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

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

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

518
                args->push_back(positioned(Node(NodeType::Symbol, symbol_name), pos));
4,754✔
519
            }
8,433✔
520

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

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

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

535
        if (!oneOf({ "fun" }))
27,272✔
536
            return std::nullopt;
23,566✔
537
        leaf->push_back(Node(Keyword::Fun));
3,706✔
538

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

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

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

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

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

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

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

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

592
        return positioned(leaf, filepos);
3,677✔
593
    }
27,279✔
594

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

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

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

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

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

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

625
        return positioned(leaf, filepos);
53✔
626
    }
20,594✔
627

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

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

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

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

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

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

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

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

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

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

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

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

682
        return positioned(args, filepos);
100✔
683
    }
167✔
684

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

689
        auto context = generateErrorContextAtCurrentPosition();
46,039✔
690
        if (!accept(IsChar('(')))
46,039✔
691
            return std::nullopt;
25,502✔
692

693
        if (!oneOf({ "macro" }))
20,537✔
694
            return std::nullopt;
20,373✔
695
        std::string comment = newlineOrComment();
164✔
696
        leaf->attachNearestCommentBefore(comment);
164✔
697

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

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

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

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

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

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

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

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

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

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

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

753
    std::optional<Node> Parser::functionCall(const FilePosition filepos)
45,863✔
754
    {
45,863✔
755
        auto context = generateErrorContextAtCurrentPosition();
45,863✔
756
        if (!accept(IsChar('(')))
45,863✔
757
            return std::nullopt;
25,502✔
758
        std::string comment = newlineOrComment();
20,361✔
759

760
        const auto func_name_pos = getCursor();
20,361✔
761
        std::optional<Node> func;
20,361✔
762
        if (auto sym_or_field = anyAtomOf({ NodeType::Symbol, NodeType::Field }); sym_or_field.has_value())
40,722✔
763
            func = sym_or_field->attachNearestCommentBefore(comment);
19,294✔
764
        else if (auto nested = node(); nested.has_value())
2,134✔
765
            func = nested->attachNearestCommentBefore(comment);
42✔
766
        else
767
            return std::nullopt;
1✔
768

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

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

775
        comment = newlineOrComment();
19,335✔
776

777
        while (!isEOF())
122,895✔
778
        {
779
            if (auto arg = nodeOrValue(); arg.has_value())
245,782✔
780
            {
781
                leaf->push_back(arg.value().attachNearestCommentBefore(comment));
103,560✔
782
                comment = newlineOrComment();
103,560✔
783
            }
103,560✔
784
            else
785
                break;
19,324✔
786
        }
787

788
        leaf->list().back().attachCommentAfter(comment);
19,328✔
789
        comment = newlineOrComment();
19,328✔
790
        if (!comment.empty())
19,328✔
UNCOV
791
            leaf->list().back().attachCommentAfter(comment);
×
792

793
        expectSuffixOrError(')', fmt::format("in function call to `{}'", func.value().repr()), context);
19,328✔
794
        return positioned(leaf, filepos);
19,324✔
795
    }
46,899✔
796

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

801
        auto context = generateErrorContextAtCurrentPosition();
25,503✔
802
        if (!accept(IsChar('[')))
25,503✔
803
            return std::nullopt;
24,181✔
804
        leaf->setAltSyntax(true);
1,322✔
805
        leaf->push_back(Node(NodeType::Symbol, "list"));
1,322✔
806

807
        std::string comment = newlineOrComment();
1,322✔
808
        leaf->attachNearestCommentBefore(comment);
1,322✔
809

810
        while (!isEOF())
3,084✔
811
        {
812
            if (auto value = nodeOrValue(); value.has_value())
6,166✔
813
            {
814
                leaf->push_back(value.value().attachNearestCommentBefore(comment));
1,762✔
815
                comment = newlineOrComment();
1,762✔
816
            }
1,762✔
817
            else
818
                break;
1,321✔
819
        }
820
        leaf->list().back().attachCommentAfter(comment);
1,322✔
821

822
        expectSuffixOrError(']', "to end list definition", context);
1,322✔
823
        return positioned(leaf, filepos);
1,321✔
824
    }
25,504✔
825

826
    std::optional<Node> Parser::number(const FilePosition filepos)
180,718✔
827
    {
180,718✔
828
        std::string res;
180,718✔
829
        if (signedNumber(&res))
180,718✔
830
        {
831
            double output;
77,210✔
832
            if (Utils::isDouble(res, &output))
77,210✔
833
                return positioned(Node(output), filepos);
77,209✔
834

835
            error("Is not a valid number", filepos);
1✔
836
        }
77,210✔
837
        return std::nullopt;
103,508✔
838
    }
180,719✔
839

840
    std::optional<Node> Parser::string(const FilePosition filepos)
103,508✔
841
    {
103,508✔
842
        std::string res;
103,508✔
843
        if (accept(IsChar('"')))
103,508✔
844
        {
845
            while (true)
29,186✔
846
            {
847
                const auto pos = getCursor();
29,186✔
848

849
                if (accept(IsChar('\\')))
29,186✔
850
                {
851
                    if (m_mode != ParserMode::Interpret)
408✔
852
                        res += '\\';
28✔
853

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

922
                if (accept(IsChar('"')))
29,181✔
923
                    break;
2,410✔
924
                if (isEOF())
26,771✔
925
                    expectSuffixOrError('"', "after string");
1✔
926
            }
29,186✔
927

928
            return positioned(Node(NodeType::String, res), filepos);
2,410✔
929
        }
930
        return std::nullopt;
101,092✔
931
    }
103,514✔
932

933
    std::optional<Node> Parser::field(const FilePosition filepos)
101,063✔
934
    {
101,063✔
935
        std::string sym;
101,063✔
936
        if (!name(&sym))
101,063✔
937
            return std::nullopt;
60,659✔
938

939
        std::optional<Node> leaf { Node(NodeType::Field) };
40,404✔
940
        leaf->push_back(Node(NodeType::Symbol, sym));
40,404✔
941

942
        while (true)
41,403✔
943
        {
944
            if (leaf->list().size() == 1 && !accept(IsChar('.')))  // Symbol:abc
41,403✔
945
                return std::nullopt;
39,433✔
946

947
            if (leaf->list().size() > 1 && !accept(IsChar('.')))
1,970✔
948
                break;
970✔
949

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

957
        return positioned(leaf, filepos);
970✔
958
    }
101,064✔
959

960
    std::optional<Node> Parser::symbol(const FilePosition filepos)
100,092✔
961
    {
100,092✔
962
        std::string res;
100,092✔
963
        if (!name(&res))
100,092✔
964
            return std::nullopt;
60,659✔
965
        return positioned(Node(NodeType::Symbol, res), filepos);
39,433✔
966
    }
100,092✔
967

968
    std::optional<Node> Parser::spread(const FilePosition filepos)
101,092✔
969
    {
101,092✔
970
        std::string res;
101,092✔
971
        if (sequence("..."))
101,092✔
972
        {
973
            if (!name(&res))
29✔
974
                errorWithNextToken("Expected a name for the variadic");
1✔
975
            return positioned(Node(NodeType::Spread, res), filepos);
28✔
976
        }
977
        return std::nullopt;
101,063✔
978
    }
101,093✔
979

980
    std::optional<Node> Parser::nil(const FilePosition filepos)
60,659✔
981
    {
60,659✔
982
        if (!accept(IsChar('(')))
60,659✔
983
            return std::nullopt;
28,444✔
984

985
        const std::string comment = newlineOrComment();
32,215✔
986
        if (!accept(IsChar(')')))
32,215✔
987
            return std::nullopt;
32,117✔
988

989
        if (m_mode == ParserMode::Interpret)
98✔
990
            return positioned(Node(NodeType::Symbol, "nil").attachNearestCommentBefore(comment), filepos);
88✔
991
        return positioned(Node(NodeType::List).attachNearestCommentBefore(comment), filepos);
10✔
992
    }
60,659✔
993

994
    std::optional<Node> Parser::atom()
180,717✔
995
    {
180,717✔
996
        const auto pos = getCount();
180,717✔
997
        const auto filepos = getCursor();
180,717✔
998

999
        if (auto res = Parser::number(filepos); res.has_value())
180,717✔
1000
            return res;
77,209✔
1001
        backtrack(pos);
103,502✔
1002

1003
        if (auto res = Parser::string(filepos); res.has_value())
103,502✔
1004
            return res;
2,410✔
1005
        backtrack(pos);
101,091✔
1006

1007
        if (auto res = Parser::spread(filepos); m_allow_macro_behavior > 0 && res.has_value())
101,091✔
1008
            return res;
28✔
1009
        backtrack(pos);
101,062✔
1010

1011
        if (auto res = Parser::field(filepos); res.has_value())
101,062✔
1012
            return res;
970✔
1013
        backtrack(pos);
100,092✔
1014

1015
        if (auto res = Parser::symbol(filepos); res.has_value())
100,092✔
1016
            return res;
39,433✔
1017
        backtrack(pos);
60,659✔
1018

1019
        if (auto res = Parser::nil(filepos); res.has_value())
60,659✔
1020
            return res;
98✔
1021
        backtrack(pos);
60,561✔
1022

1023
        return std::nullopt;
60,561✔
1024
    }
180,717✔
1025

1026
    std::optional<Node> Parser::anyAtomOf(const std::initializer_list<NodeType> types)
20,361✔
1027
    {
20,361✔
1028
        if (auto value = atom(); value.has_value())
39,656✔
1029
        {
1030
            for (const auto type : types)
38,685✔
1031
            {
1032
                if (value->nodeType() == type)
19,390✔
1033
                    return value;
19,294✔
1034
            }
19,390✔
1035
        }
1✔
1036
        return std::nullopt;
1,067✔
1037
    }
20,361✔
1038

1039
    std::optional<Node> Parser::nodeOrValue()
160,348✔
1040
    {
160,348✔
1041
        if (auto value = atom(); value.has_value())
160,348✔
1042
            return value;
100,853✔
1043
        if (auto sub_node = node(); sub_node.has_value())
59,486✔
1044
            return sub_node;
35,312✔
1045

1046
        return std::nullopt;
24,174✔
1047
    }
160,348✔
1048

1049
    std::optional<Node> Parser::wrapped(std::optional<Node> (Parser::*parser)(FilePosition), const std::string& name)
315,587✔
1050
    {
315,587✔
1051
        const auto cursor = getCursor();
315,587✔
1052
        auto context = generateErrorContextAtCurrentPosition();
315,587✔
1053
        if (!prefix('('))
315,587✔
1054
            return std::nullopt;
164,888✔
1055

1056
        const std::string comment = newlineOrComment();
150,699✔
1057

1058
        if (auto result = (this->*parser)(cursor); result.has_value())
166,847✔
1059
        {
1060
            result->attachNearestCommentBefore(result->comment() + comment);
16,148✔
1061
            result.value().attachCommentAfter(newlineOrComment());
16,148✔
1062

1063
            expectSuffixOrError(')', "after " + name, context);
16,148✔
1064

1065
            result.value().attachCommentAfter(spaceComment());
16,147✔
1066
            return result;
16,147✔
1067
        }
1068

1069
        return std::nullopt;
134,529✔
1070
    }
315,588✔
1071
}
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