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

ArkScript-lang / Ark / 14956639333

11 May 2025 02:13PM UTC coverage: 86.495% (+0.02%) from 86.474%
14956639333

Pull #535

github

web-flow
Merge fa81c887e into 3f1c6e9e3
Pull Request #535: Feat/parsing errors context

208 of 230 new or added lines in 10 files covered. (90.43%)

3 existing lines in 2 files now uncovered.

6949 of 8034 relevant lines covered (86.49%)

79310.9 hits per line

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

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

3
#include <fmt/core.h>
4

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

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

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

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

55
        m_logger.traceEnd();
403✔
56
    }
444✔
57

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

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

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

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

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

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

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

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

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

100
        if (auto result = import_(); result.has_value())
20,296✔
101
            return result;
151✔
102
        backtrack(position);
20,144✔
103

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

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

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

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

120
        if (auto result = functionCall(); result.has_value())
18,619✔
121
            return result;
7,830✔
122
        backtrack(position);
10,788✔
123

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

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

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

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

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

150
        if (m_allow_macro_behavior > 0)
3,701✔
151
        {
152
            const auto position = getCount();
22✔
153
            if (const auto value = nodeOrValue(); value.has_value())
44✔
154
            {
155
                const auto sym = value.value();
22✔
156
                if (sym.nodeType() == NodeType::List || sym.nodeType() == NodeType::Symbol || sym.nodeType() == NodeType::Macro || sym.nodeType() == NodeType::Spread)
22✔
157
                    leaf->push_back(sym);
21✔
158
                else
159
                    error(fmt::format("Can not use a {} as a symbol name, even in a macro", nodeTypes[static_cast<std::size_t>(sym.nodeType())]), sym.repr());
1✔
160
            }
22✔
161
            else
162
                backtrack(position);
×
163
        }
22✔
164

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

301
        Import import_data;
159✔
302

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

473
        bool has_captures = false;
1,295✔
474

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

687
        return args;
90✔
688
    }
150✔
689

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

872
        return std::nullopt;
24,387✔
873
    }
110,826✔
874

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

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

904
        return std::nullopt;
10,064✔
905
    }
102,987✔
906

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

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

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

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

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

933
            return result;
6,148✔
934
        }
935

936
        return std::nullopt;
52,690✔
937
    }
128,798✔
938
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc