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

ArkScript-lang / Ark / 22157739607

18 Feb 2026 09:10PM UTC coverage: 93.312% (-0.2%) from 93.464%
22157739607

Pull #641

github

web-flow
Merge 832fcd44e into 672fb743f
Pull Request #641: Feat/various improvements

138 of 167 new or added lines in 7 files covered. (82.63%)

3 existing lines in 2 files now uncovered.

9223 of 9884 relevant lines covered (93.31%)

265167.15 hits per line

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

97.48
/src/arkreactor/Compiler/Macros/Processor.cpp
1
#include <Ark/Compiler/Macros/Processor.hpp>
2

3
#include <utility>
4
#include <algorithm>
5
#include <cassert>
6
#include <ranges>
7
#include <sstream>
8
#include <fmt/core.h>
9

10
#include <Ark/Constants.hpp>
11
#include <Ark/Error/Exceptions.hpp>
12
#include <Ark/Builtins/Builtins.hpp>
13
#include <Ark/Compiler/Macros/Executor.hpp>
14
#include <Ark/Compiler/Macros/Executors/Symbol.hpp>
15
#include <Ark/Compiler/Macros/Executors/Function.hpp>
16
#include <Ark/Compiler/Macros/Executors/Conditional.hpp>
17

18
namespace Ark::internal
19
{
20
    MacroProcessor::MacroProcessor(const unsigned debug) noexcept :
870✔
21
        Pass("MacroProcessor", debug)
435✔
22
    {
870✔
23
        // create executors pipeline
24
        m_conditional_executor = std::make_shared<ConditionalExecutor>(this);
435✔
25
        m_executors.emplace_back(std::make_shared<SymbolExecutor>(this));
435✔
26
        m_executors.emplace_back(m_conditional_executor);
435✔
27
        m_executors.emplace_back(std::make_shared<FunctionExecutor>(this));
435✔
28
    }
435✔
29

30
    void MacroProcessor::process(const Node& ast)
398✔
31
    {
398✔
32
        m_logger.debug("Processing macros...");
398✔
33
        m_logger.traceStart("process");
398✔
34

35
        // to be able to modify it
36
        m_ast = ast;
398✔
37
        processNode(m_ast, 0);
398✔
38

39
        m_logger.traceEnd();
398✔
40
        m_logger.trace("AST after processing macros");
398✔
41
        if (m_logger.shouldTrace())
398✔
42
            m_ast.debugPrint(std::cout) << '\n';
1✔
43
    }
398✔
44

45
    const Node& MacroProcessor::ast() const noexcept
363✔
46
    {
363✔
47
        return m_ast;
363✔
48
    }
49

50
    void MacroProcessor::handleMacroNode(Node& node, const unsigned depth)
260✔
51
    {
260✔
52
        // a macro needs at least 2 nodes, name + value is the minimal form
53
        // this is guaranteed by the parser
54
        assert(node.constList().size() >= 2 && "Invalid macro, missing value");
260✔
55

56
        const Node& first_node = node.list()[0];
260✔
57

58
        // (macro name value)
59
        if (node.constList().size() == 2)
260✔
60
        {
61
            assert(first_node.nodeType() == NodeType::Symbol && "Can not define a macro without a symbol");
69✔
62
            // processNode is needed here to correctly expand macros when we have something like
63
            //   (macro arg_bloc (__suffix-dup arg length))                                    /
64
            //   (macro all_args (__replace_placeholders [arg_bloc] ...args))  <--------------*
65
            // otherwise, 'arg_bloc' isn't expanded here, and we get in trouble
66
            processNode(node.list()[1], depth + 1);
45✔
67
            applyMacro(node.list()[1], depth + 1);
45✔
68
            node.list()[1] = evaluate(node.list()[1], depth + 1, true);
45✔
69
            m_macros.back().add(first_node.string(), node);
45✔
70
        }
45✔
71
        // (macro name (args) body)
72
        else if (node.constList().size() == 3 && first_node.nodeType() == NodeType::Symbol)
191✔
73
        {
74
            assert(node.constList()[1].nodeType() == NodeType::List && "Invalid macro argument's list");
67✔
75
            m_macros.back().add(first_node.string(), node);
67✔
76
        }
67✔
77
        // in case we had a conditional, we need to evaluate and expand it
78
        else if (m_conditional_executor->canHandle(node))
124✔
79
            m_conditional_executor->applyMacro(node, depth + 1);
121✔
80
    }
230✔
81

82
    // todo find a better way to do this
83
    void MacroProcessor::registerFuncDef(const Node& node)
92,347✔
84
    {
92,347✔
85
        if (node.nodeType() == NodeType::List && node.constList().size() == 3 && node.constList()[0].nodeType() == NodeType::Keyword)
92,347✔
86
        {
87
            const Keyword kw = node.constList()[0].keyword();
27,422✔
88
            // checking for function definition, which can occur only inside an assignment node
89
            if (kw != Keyword::Let && kw != Keyword::Mut && kw != Keyword::Set)
27,422✔
90
                return;
8,211✔
91

92
            const Node inner = node.constList()[2];
19,211✔
93
            if (inner.nodeType() != NodeType::List)
19,211✔
94
                return;
6,608✔
95

96
            if (!inner.constList().empty() && inner.constList()[0].nodeType() == NodeType::Keyword && inner.constList()[0].keyword() == Keyword::Fun)
12,603✔
97
            {
98
                const Node symbol = node.constList()[1];
3,392✔
99
                if (symbol.nodeType() == NodeType::Symbol)
3,392✔
100
                    m_defined_functions.emplace(symbol.string(), inner.constList()[1]);
3,391✔
101
                else
102
                    throwMacroProcessingError(fmt::format("Can not use a {} to define a variable", typeToString(symbol)), symbol);
1✔
103
            }
3,392✔
104
        }
27,422✔
105
    }
92,347✔
106

107
    void MacroProcessor::processNode(Node& node, unsigned depth, const bool is_processing_namespace)
350,841✔
108
    {
350,841✔
109
        if (depth >= MaxMacroProcessingDepth)
350,841✔
110
            throwMacroProcessingError(
1✔
111
                fmt::format(
2✔
112
                    "Max recursion depth reached ({}). You most likely have a badly defined recursive macro calling itself without a proper exit condition",
1✔
113
                    MaxMacroProcessingDepth),
114
                node);
1✔
115

116
        if (node.nodeType() == NodeType::List)
350,840✔
117
        {
118
            bool has_created = false;
92,895✔
119
            std::size_t i = 0;
92,895✔
120
            while (i < node.list().size())
443,655✔
121
            {
122
                const std::size_t pos = i;
350,760✔
123
                Node& child = node.list()[pos];
350,760✔
124
                const bool had_begin = isBeginNode(child);
350,760✔
125
                bool added_begin = false;
350,760✔
126

127
                if (child.nodeType() == NodeType::Macro)
350,760✔
128
                {
129
                    // create a scope only if needed
130
                    if ((!m_macros.empty() && !m_macros.back().empty() && m_macros.back().depth() < depth && !is_processing_namespace) ||
343✔
131
                        (!has_created && !is_processing_namespace) ||
136✔
132
                        (m_macros.empty() && is_processing_namespace))
58✔
133
                    {
134
                        has_created = true;
149✔
135
                        m_macros.emplace_back(depth);
149✔
136
                    }
149✔
137

138
                    handleMacroNode(child, depth);
204✔
139
                    if (child.nodeType() != NodeType::Macro)
204✔
140
                        evaluate(child, depth);
92✔
141
                    added_begin = isBeginNode(child) && !had_begin;
204✔
142
                }
204✔
143
                else  // running on non-macros
144
                {
145
                    applyMacro(child, 0);
350,556✔
146
                    added_begin = isBeginNode(child) && !had_begin;
350,556✔
147

148
                    if (child.nodeType() == NodeType::Unused)
350,556✔
149
                        node.list().erase(node.constList().begin() + static_cast<std::vector<Node>::difference_type>(pos));
9✔
150
                    else if (!added_begin)
350,547✔
151
                        // Go forward only if it isn't a macro, because we delete macros
152
                        // while running on the AST. Also, applying a macro can result in
153
                        // nodes being marked unused, and delete them immediately. When
154
                        // that happens, we can't increment i, otherwise we delete a node,
155
                        // advance, resulting in a node being skipped!
156
                        ++i;
349,116✔
157

158
                    // process subnodes if any
159
                    if (node.nodeType() == NodeType::List && pos < node.constList().size())
350,556✔
160
                    {
161
                        processNode(child, depth + 1);
350,280✔
162
                        // needed if we created a function node from a macro
163
                        registerFuncDef(child);
350,280✔
164
                    }
350,280✔
165
                }
166

167
                if (pos < node.constList().size())
350,760✔
168
                {
169
                    // if we now have a surrounding (begin ...) and didn't have one before, remove it
170
                    if (added_begin)
350,483✔
171
                        removeBegin(node, pos);
1,439✔
172
                    // if there is an unused node or a leftover macro need, we need to get rid of it in the final ast
173
                    else if (child.nodeType() == NodeType::Macro || child.nodeType() == NodeType::Unused)
349,044✔
174
                        node.list().erase(node.constList().begin() + static_cast<std::vector<Node>::difference_type>(pos));
135✔
175
                }
350,483✔
176
            }
350,760✔
177

178
            // delete a scope only if needed
179
            if (!m_macros.empty() && m_macros.back().depth() == depth && !is_processing_namespace)
92,895✔
180
                m_macros.pop_back();
118✔
181
        }
92,895✔
182
        else if (node.nodeType() == NodeType::Namespace)
257,945✔
183
        {
184
            Node& namespace_ast = *node.arkNamespace().ast;
142✔
185
            // We have to use depth - 1 because it was incremented previously, as a namespace node
186
            // must be in a list node. Then depth - 1 is safe as depth is at least 1.
187
            // Using a decreased value of depth ensures that macros are stored in the correct scope,
188
            // and not deleted when the namespace traversal ends.
189
            processNode(namespace_ast, depth - 1, /* is_processing_namespace= */ true);
142✔
190
        }
142✔
191
    }
350,841✔
192

193
    bool MacroProcessor::applyMacro(Node& node, const unsigned depth)
353,560✔
194
    {
353,560✔
195
        if (depth > MaxMacroProcessingDepth)
353,560✔
196
            throwMacroProcessingError(
1✔
197
                fmt::format(
2✔
198
                    "Max macro processing depth reached ({}). You may have a macro trying to evaluate itself, try splitting your code in multiple nodes.",
1✔
199
                    MaxMacroProcessingDepth),
200
                node);
1✔
201

202
        for (const auto& executor : m_executors)
1,413,603✔
203
        {
204
            if (executor->canHandle(node))
1,060,262✔
205
            {
206
                m_macros_being_applied.push_back(executor->macroNode(node));
164,214✔
207
                const bool applied = executor->applyMacro(node, depth);
164,214✔
208
                m_macros_being_applied.pop_back();
163,996✔
209

210
                if (applied)
163,996✔
211
                    return true;
1,675✔
212
            }
163,996✔
213
        }
1,060,044✔
214
        return false;
351,775✔
215
    }
353,342✔
216

217
    void MacroProcessor::checkMacroArgCountEq(const Node& node, std::size_t expected, const std::string& name, const bool is_expansion, const std::string& kind) const
2,351✔
218
    {
2,351✔
219
        const std::size_t argcount = node.constList().size();
2,351✔
220
        if (argcount != expected + 1)
2,351✔
221
        {
222
            if (is_expansion)
7✔
223
                throwMacroProcessingError(
12✔
224
                    fmt::format(
12✔
225
                        "When expanding `{}' inside a macro, got {} argument{}, expected {}",
6✔
226
                        name,
6✔
227
                        argcount - 1,
6✔
228
                        argcount > 2 ? "s" : "",
6✔
229
                        expected),
230
                    node);
6✔
231
            else
232
                throwMacroProcessingError(
2✔
233
                    fmt::format(
3✔
234
                        "Interpreting a `{}'{} with {} argument{}, expected {}.",
1✔
235
                        name,
1✔
236
                        kind.empty() ? kind : " " + kind,
1✔
237
                        argcount - 1,
1✔
238
                        argcount > 2 ? "s" : "",
1✔
239
                        expected),
240
                    node);
1✔
241
        }
242
    }
2,352✔
243

244
    void MacroProcessor::checkMacroArgCountGe(const Node& node, std::size_t expected, const std::string& name, const std::string& kind) const
76✔
245
    {
76✔
246
        const std::size_t argcount = node.constList().size();
76✔
247
        if (argcount < expected + 1)
76✔
248
            throwMacroProcessingError(
2✔
249
                fmt::format(
3✔
250
                    "Interpreting a `{}'{} with {} argument{}, expected at least {}.",
1✔
251
                    name,
1✔
252
                    kind.empty() ? kind : " " + kind,
1✔
253
                    argcount - 1,
1✔
254
                    argcount > 2 ? "s" : "",
1✔
255
                    expected),
256
                node);
1✔
257
    }
76✔
258

259
    Node MacroProcessor::evaluate(Node& node, const unsigned depth, const bool is_not_body)
48,834✔
260
    {
48,834✔
261
        if (node.nodeType() == NodeType::Symbol)
48,834✔
262
        {
263
            const Node* macro = findNearestMacro(node.string());
16,677✔
264
            if (macro != nullptr && macro->constList().size() == 2)
16,677✔
265
                return macro->constList()[1];
146✔
266
            return node;
16,531✔
267
        }
16,677✔
268
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.list()[0].nodeType() == NodeType::Symbol)
32,157✔
269
        {
270
            const std::string& name = node.list()[0].string();
12,821✔
271
            const std::size_t argcount = node.list().size() - 1;
12,821✔
272

273
            if (const Node* macro = findNearestMacro(name); macro != nullptr)
25,642✔
274
            {
275
                applyMacro(node.list()[0], depth + 1);
1,161✔
276
                if (node.list()[0].nodeType() == NodeType::Unused)
1,161✔
277
                    node.list().erase(node.constList().begin());
×
278
            }
1,161✔
279
            else if (name == "=" && is_not_body)
11,660✔
280
            {
281
                checkMacroArgCountEq(node, 2, "=", /* is_expansion= */ false, "condition");
43✔
282
                const Node one = evaluate(node.list()[1], depth + 1, is_not_body);
42✔
283
                const Node two = evaluate(node.list()[2], depth + 1, is_not_body);
42✔
284
                return (one == two) ? getTrueNode() : getFalseNode();
42✔
285
            }
42✔
286
            else if (name == "!=" && is_not_body)
11,617✔
287
            {
288
                checkMacroArgCountEq(node, 2, "!=", /* is_expansion= */ false, "condition");
1✔
289
                const Node one = evaluate(node.list()[1], depth + 1, is_not_body);
1✔
290
                const Node two = evaluate(node.list()[2], depth + 1, is_not_body);
1✔
291
                return (one != two) ? getTrueNode() : getFalseNode();
1✔
292
            }
1✔
293
            else if (name == "<" && is_not_body)
11,616✔
294
            {
295
                checkMacroArgCountEq(node, 2, "<", /* is_expansion= */ false, "condition");
1✔
296
                const Node one = evaluate(node.list()[1], depth + 1, is_not_body);
1✔
297
                const Node two = evaluate(node.list()[2], depth + 1, is_not_body);
1✔
298
                return (one < two) ? getTrueNode() : getFalseNode();
1✔
299
            }
1✔
300
            else if (name == ">" && is_not_body)
11,615✔
301
            {
302
                checkMacroArgCountEq(node, 2, ">", /* is_expansion= */ false, "condition");
41✔
303
                const Node one = evaluate(node.list()[1], depth + 1, is_not_body);
41✔
304
                const Node two = evaluate(node.list()[2], depth + 1, is_not_body);
41✔
305
                return !(one < two) && (one != two) ? getTrueNode() : getFalseNode();
41✔
306
            }
41✔
307
            else if (name == "<=" && is_not_body)
11,574✔
308
            {
309
                checkMacroArgCountEq(node, 2, "<=", /* is_expansion= */ false, "condition");
1✔
310
                const Node one = evaluate(node.list()[1], depth + 1, is_not_body);
1✔
311
                const Node two = evaluate(node.list()[2], depth + 1, is_not_body);
1✔
312
                return one < two || one == two ? getTrueNode() : getFalseNode();
1✔
313
            }
1✔
314
            else if (name == ">=" && is_not_body)
11,573✔
315
            {
316
                checkMacroArgCountEq(node, 2, ">=", /* is_expansion= */ false, "condition");
10✔
317
                const Node one = evaluate(node.list()[1], depth + 1, is_not_body);
10✔
318
                const Node two = evaluate(node.list()[2], depth + 1, is_not_body);
10✔
319
                return !(one < two) ? getTrueNode() : getFalseNode();
10✔
320
            }
10✔
321
            else if (name == "+" && is_not_body)
11,563✔
322
            {
323
                checkMacroArgCountGe(node, 2, "+", "operator");
9✔
324
                double v = 0.0;
8✔
325
                for (auto& child : node.list() | std::ranges::views::drop(1))
27✔
326
                {
327
                    Node ev = evaluate(child, depth + 1, is_not_body);
19✔
328
                    if (ev.nodeType() != NodeType::Number)
19✔
329
                        return node;
×
330
                    v += ev.number();
19✔
331
                }
19✔
332
                return Node(v);
8✔
333
            }
8✔
334
            else if (name == "-" && is_not_body)
11,554✔
335
            {
336
                checkMacroArgCountGe(node, 2, "-", "operator");
64✔
337
                const Node one = evaluate(node.list()[1], depth + 1, is_not_body);
64✔
338
                if (one.nodeType() != NodeType::Number)
64✔
339
                    return node;
×
340

341
                double v = one.number();
64✔
342
                for (auto& child : node.list() | std::ranges::views::drop(2))
130✔
343
                {
344
                    Node ev = evaluate(child, depth + 1, is_not_body);
66✔
345
                    if (ev.nodeType() != NodeType::Number)
66✔
346
                        return node;
1✔
347
                    v -= ev.number();
65✔
348
                }
66✔
349
                return Node(v);
63✔
350
            }
64✔
351
            else if (name == "*" && is_not_body)
11,490✔
352
            {
353
                checkMacroArgCountGe(node, 2, "*", "operator");
1✔
354
                double v = 1.0;
1✔
355
                for (auto& child : node.list() | std::ranges::views::drop(1))
4✔
356
                {
357
                    Node ev = evaluate(child, depth + 1, is_not_body);
3✔
358
                    if (ev.nodeType() != NodeType::Number)
3✔
359
                        return node;
×
360
                    v *= ev.number();
3✔
361
                }
3✔
362
                return Node(v);
1✔
363
            }
1✔
364
            else if (name == "/" && is_not_body)
11,489✔
365
            {
366
                checkMacroArgCountGe(node, 2, "/", "operator");
2✔
367
                const Node one = evaluate(node.list()[1], depth + 1, is_not_body);
2✔
368
                if (one.nodeType() != NodeType::Number)
2✔
369
                    return node;
1✔
370

371
                double v = one.number();
1✔
372
                for (auto& child : node.list() | std::ranges::views::drop(2))
3✔
373
                {
374
                    Node ev = evaluate(child, depth + 1, is_not_body);
2✔
375
                    if (ev.nodeType() != NodeType::Number)
2✔
376
                        return node;
×
377
                    v /= ev.number();
2✔
378
                }
2✔
379
                return Node(v);
1✔
380
            }
2✔
381
            else if (name == "not" && is_not_body)
11,487✔
382
            {
383
                checkMacroArgCountEq(node, 1, "not", /* is_expansion= */ false, "condition");
1✔
384
                return (!isTruthy(evaluate(node.list()[1], depth + 1, is_not_body))) ? getTrueNode() : getFalseNode();
1✔
385
            }
386
            else if (name == Language::And && is_not_body)
11,486✔
387
            {
388
                if (node.list().size() < 3)
5✔
389
                    throwMacroProcessingError(fmt::format("Interpreting a `{}' chain with {} arguments, expected at least 2.", Language::And, argcount), node);
1✔
390

391
                for (std::size_t i = 1, end = node.list().size(); i < end; ++i)
11✔
392
                {
393
                    if (!isTruthy(evaluate(node.list()[i], depth + 1, is_not_body)))
7✔
394
                        return getFalseNode();
1✔
395
                }
6✔
396
                return getTrueNode();
3✔
397
            }
398
            else if (name == Language::Or && is_not_body)
11,481✔
399
            {
400
                if (node.list().size() < 3)
3✔
401
                    throwMacroProcessingError(fmt::format("Interpreting an `{}' chain with {} arguments, expected at least 2.", Language::Or, argcount), node);
1✔
402

403
                for (std::size_t i = 1, end = node.list().size(); i < end; ++i)
6✔
404
                {
405
                    if (isTruthy(evaluate(node.list()[i], depth + 1, is_not_body)))
4✔
406
                        return getTrueNode();
1✔
407
                }
3✔
408
                return getFalseNode();
1✔
409
            }
410
            else if (name == "$len")
11,478✔
411
            {
412
                checkMacroArgCountEq(node, 1, "$len", true);
56✔
413
                Node& lst = node.list()[1];
55✔
414
                checkMacroTypeError("$len", "node", NodeType::List, lst);
55✔
415

416
                if (!lst.list().empty() && lst.list()[0] == getListNode())
54✔
417
                    node.updateValueAndType(Node(static_cast<long>(lst.list().size()) - 1));
53✔
418
                else
419
                    node.updateValueAndType(Node(static_cast<long>(lst.list().size())));
1✔
420
            }
55✔
421
            else if (name == "$empty?")
11,422✔
422
            {
423
                checkMacroArgCountEq(node, 1, "$empty?", true);
22✔
424

425
                if (Node& lst = node.list()[1]; lst.nodeType() == NodeType::List)
42✔
426
                {
427
                    // only apply len at compile time if we can
428
                    if (!lst.list().empty() && lst.list()[0] == getListNode())
20✔
429
                        node.updateValueAndType(lst.list().size() - 1 == 0 ? getTrueNode() : getFalseNode());
19✔
430
                    else
431
                        node.updateValueAndType(lst.list().empty() ? getTrueNode() : getFalseNode());
1✔
432
                }
20✔
433
                else if (lst == getNilNode())
1✔
434
                    node.updateValueAndType(getTrueNode());
1✔
435
            }
21✔
436
            else if (name == "$at")
11,400✔
437
            {
438
                checkMacroArgCountEq(node, 2, "$at");
17✔
439

440
                Node sublist = evaluate(node.list()[1], depth + 1, is_not_body);
17✔
441
                const Node idx = evaluate(node.list()[2], depth + 1, is_not_body);
17✔
442

443
                checkMacroTypeError("$at", "list", NodeType::List, sublist);
17✔
444
                checkMacroTypeError("$at", "index", NodeType::Number, idx);
16✔
445

446
                const std::size_t size = sublist.list().size();
15✔
447
                std::size_t real_size = size;
15✔
448
                long num_idx = static_cast<long>(idx.number());
15✔
449

450
                // if the first node is the function call to "list", don't count it
451
                if (size > 0 && sublist.list()[0] == getListNode())
15✔
452
                {
453
                    real_size--;
9✔
454
                    if (num_idx >= 0)
9✔
455
                        ++num_idx;
4✔
456
                }
9✔
457

458
                Node output;
15✔
459
                if (num_idx >= 0 && std::cmp_less(num_idx, size))
15✔
460
                    output = sublist.list()[static_cast<std::size_t>(num_idx)];
10✔
461
                else if (const auto c = static_cast<long>(size) + num_idx; num_idx < 0 && std::cmp_less(c, size) && c >= 0)
10✔
462
                    output = sublist.list()[static_cast<std::size_t>(c)];
4✔
463
                else
464
                    throwMacroProcessingError(fmt::format("Index ({}) out of range (list size: {})", num_idx, real_size), node);
1✔
465

466
                output.setPositionFrom(node);
14✔
467
                return output;
14✔
468
            }
17✔
469
            else if (name == "$head")
11,383✔
470
            {
471
                checkMacroArgCountEq(node, 1, "$head", true);
12✔
472
                Node sublist = node.list()[1];
11✔
473
                checkMacroTypeError("$head", "node", NodeType::List, sublist);
11✔
474

475
                if (!sublist.constList().empty() && sublist.constList()[0] == getListNode())
10✔
476
                {
477
                    if (sublist.constList().size() > 1)
6✔
478
                    {
479
                        const Node sublistCopy = sublist.constList()[1];
5✔
480
                        node.updateValueAndType(sublistCopy);
5✔
481
                    }
5✔
482
                    else
483
                        node.updateValueAndType(getNilNode());
1✔
484
                }
6✔
485
                else if (!sublist.list().empty())
4✔
486
                    node.updateValueAndType(sublist.constList()[0]);
4✔
487
                else
NEW
488
                    node.updateValueAndType(getNilNode());
×
489
            }
11✔
490
            else if (name == "$tail")
11,371✔
491
            {
492
                checkMacroArgCountEq(node, 1, "$tail", true);
9✔
493
                Node sublist = node.list()[1];
8✔
494
                checkMacroTypeError("$tail", "node", NodeType::List, sublist);
8✔
495

496
                if (!sublist.list().empty() && sublist.list()[0] == getListNode())
7✔
497
                {
498
                    if (sublist.list().size() > 1)
6✔
499
                    {
500
                        sublist.list().erase(sublist.constList().begin() + 1);
5✔
501
                        node.updateValueAndType(sublist);
5✔
502
                    }
5✔
503
                    else
504
                    {
505
                        node.updateValueAndType(Node(NodeType::List));
1✔
506
                        node.push_back(getListNode());
1✔
507
                    }
508
                }
6✔
509
                else if (!sublist.list().empty())
1✔
510
                {
511
                    sublist.list().erase(sublist.constList().begin());
1✔
512
                    sublist.list().insert(sublist.list().begin(), getListNode());
1✔
513
                    node.updateValueAndType(sublist);
1✔
514
                }
1✔
515
                else
516
                {
NEW
517
                    node.updateValueAndType(Node(NodeType::List));
×
NEW
518
                    node.push_back(getListNode());
×
519
                }
520
            }
8✔
521
            else if (name == Language::Symcat)
11,362✔
522
            {
523
                if (node.list().size() <= 2)
100✔
524
                    throwMacroProcessingError(fmt::format("When expanding `{}', expected at least 2 arguments, got {} arguments", Language::Symcat, argcount), node);
1✔
525
                checkMacroTypeError(Language::Symcat.data(), "symbol", NodeType::Symbol, node.list()[1]);
99✔
526

527
                std::string sym = node.list()[1].string();
98✔
528

529
                for (std::size_t i = 2, end = node.list().size(); i < end; ++i)
196✔
530
                {
531
                    const Node ev = evaluate(node.list()[i], depth + 1, /* is_not_body */ true);
98✔
532

533
                    switch (ev.nodeType())
98✔
534
                    {
25✔
535
                        case NodeType::Number:
536
                            // we don't want '.' in identifiers
537
                            sym += std::to_string(static_cast<long int>(ev.number()));
25✔
538
                            break;
97✔
539

540
                        case NodeType::String:
541
                        case NodeType::Symbol:
542
                            sym += ev.string();
72✔
543
                            break;
73✔
544

545
                        default:
546
                            throwMacroProcessingError(
2✔
547
                                fmt::format(
2✔
548
                                    "When expanding `{}', expected either a Number, String or Symbol, got a {}: {}",
1✔
549
                                    Language::Symcat,
550
                                    typeToString(ev),
1✔
551
                                    ev.repr()),
1✔
552
                                ev);
553
                    }
97✔
554
                }
98✔
555

556
                node.setNodeType(NodeType::Symbol);
97✔
557
                node.setString(sym);
97✔
558
            }
98✔
559
            else if (name == Language::Argcount)
11,262✔
560
            {
561
                checkMacroArgCountEq(node, 1, Language::Argcount.data(), true);
21✔
562

563
                const Node sym = node.constList()[1];
20✔
564
                if (sym.nodeType() == NodeType::Symbol)
20✔
565
                {
566
                    if (const auto maybe_func = lookupDefinedFunction(sym.string()); maybe_func.has_value())
32✔
567
                        node.updateValueAndType(Node(static_cast<long>(maybe_func->constList().size())));
15✔
568
                    else
569
                        throwMacroProcessingError(fmt::format("When expanding `{}', expected a known function name, got unbound variable {}", Language::Argcount, sym.string()), sym);
1✔
570
                }
15✔
571
                else if (sym.nodeType() == NodeType::List && sym.constList().size() == 3 && sym.constList()[0].nodeType() == NodeType::Keyword && sym.constList()[0].keyword() == Keyword::Fun)
4✔
572
                    node.updateValueAndType(Node(static_cast<long>(sym.constList()[1].constList().size())));
3✔
573
                else
574
                    throwMacroProcessingError(fmt::format("When trying to apply `{}', got a {} instead of a Symbol or Function", Language::Argcount, typeToString(sym)), sym);
1✔
575
            }
20✔
576
            else if (name == Language::Repr)
11,241✔
577
            {
578
                checkMacroArgCountEq(node, 1, Language::Repr.data(), true);
2,116✔
579

580
                const Node arg = node.constList()[1];
2,115✔
581
                node.updateValueAndType(Node(NodeType::String, arg.repr()));
2,115✔
582
            }
2,115✔
583
            else if (name == Language::AsIs)
9,125✔
584
            {
585
                if (node.list().size() != 2)
2,062✔
586
                    throwMacroProcessingError(fmt::format("When expanding `{}', expected one argument, got {} arguments", Language::AsIs, argcount), node);
1✔
587
                return node.constList()[1];
2,061✔
588
            }
589
            else if (name == Language::Undef)
7,063✔
590
            {
591
                if (node.list().size() != 2)
13✔
592
                    throwMacroProcessingError(fmt::format("When expanding `{}', expected one argument, got {} arguments", Language::Undef, argcount), node);
1✔
593

594
                const Node sym = node.constList()[1];
12✔
595
                if (sym.nodeType() == NodeType::Symbol)
12✔
596
                {
597
                    deleteNearestMacro(sym.string());
11✔
598
                    node.setNodeType(NodeType::Unused);
11✔
599
                    return node;
11✔
600
                }
601

602
                throwMacroProcessingError(
2✔
603
                    fmt::format(
2✔
604
                        "When expanding `{}', got a {}. Can not un-define a macro without a valid name",
1✔
605
                        Language::Undef, typeToString(sym)),
1✔
606
                    sym);
607
            }
12✔
608
            else if (name == Language::Type)
7,050✔
609
            {
610
                if (node.list().size() != 2)
10✔
611
                    throwMacroProcessingError(fmt::format("When expanding `{}', expected one argument, got {} arguments", Language::Type, argcount), node);
1✔
612

613
                const Node arg = node.constList()[1];
9✔
614
                node.updateValueAndType(Node(NodeType::String, typeToString(arg)));
9✔
615
            }
9✔
616
        }
12,822✔
617

618
        if (node.nodeType() == NodeType::List && !node.constList().empty())
29,868✔
619
        {
620
            for (auto& child : node.list())
62,110✔
621
                child.updateValueAndType(evaluate(child, depth + 1, is_not_body));
46,476✔
622
        }
15,634✔
623

624
        if (node.nodeType() == NodeType::Spread)
29,868✔
625
            throwMacroProcessingError(fmt::format("Found an unevaluated spread: `{}'", node.string()), node);
1✔
626

627
        return node;
29,867✔
628
    }
48,834✔
629

630
    bool MacroProcessor::isTruthy(const Node& node) const
136✔
631
    {
136✔
632
        if (node.nodeType() == NodeType::Symbol)
136✔
633
        {
634
            if (node.string() == "true")
135✔
635
                return true;
68✔
636
            if (node.string() == "false" || node.string() == "nil")
67✔
637
                return false;
67✔
638
        }
×
639
        else if ((node.nodeType() == NodeType::Number && node.number() != 0.0) || (node.nodeType() == NodeType::String && !node.string().empty()))
1✔
640
            return true;
1✔
UNCOV
641
        else if (node.nodeType() == NodeType::Spread)
×
642
            throwMacroProcessingError("Can not determine the truth value of a spread symbol", node);
×
UNCOV
643
        return false;
×
644
    }
136✔
645

646
    std::optional<Node> MacroProcessor::lookupDefinedFunction(const std::string& name) const
16✔
647
    {
16✔
648
        if (m_defined_functions.contains(name))
16✔
649
            return m_defined_functions.at(name);
15✔
650
        return std::nullopt;
1✔
651
    }
16✔
652

653
    const Node* MacroProcessor::findNearestMacro(const std::string& name) const
357,782✔
654
    {
357,782✔
655
        if (m_macros.empty())
357,782✔
656
            return nullptr;
143,211✔
657

658
        for (const auto& m_macro : std::ranges::reverse_view(m_macros))
457,779✔
659
        {
660
            if (const auto res = m_macro.has(name); res != nullptr)
243,208✔
661
                return res;
8,430✔
662
        }
243,208✔
663
        return nullptr;
206,141✔
664
    }
357,782✔
665

666
    void MacroProcessor::deleteNearestMacro(const std::string& name)
11✔
667
    {
11✔
668
        if (m_macros.empty())
11✔
669
            return;
×
670

671
        for (auto& m_macro : std::ranges::reverse_view(m_macros))
27✔
672
        {
673
            if (m_macro.remove(name))
16✔
674
            {
675
                // stop right here because we found one matching macro
676
                return;
1✔
677
            }
678
        }
16✔
679
    }
11✔
680

681
    bool MacroProcessor::isBeginNode(const Node& node)
701,555✔
682
    {
701,555✔
683
        return node.nodeType() == NodeType::List &&
886,727✔
684
            !node.constList().empty() &&
185,172✔
685
            node.constList()[0].nodeType() == NodeType::Keyword &&
265,696✔
686
            node.constList()[0].keyword() == Keyword::Begin;
80,926✔
687
    }
688

689
    void MacroProcessor::removeBegin(Node& node, const std::size_t i)
1,439✔
690
    {
1,439✔
691
        if (node.isListLike() && node.list()[i].nodeType() == NodeType::List && !node.list()[i].list().empty())
1,439✔
692
        {
693
            Node lst = node.constList()[i];
1,439✔
694
            const Node first = lst.constList()[0];
1,439✔
695

696
            if (first.nodeType() == NodeType::Keyword && first.keyword() == Keyword::Begin)
1,439✔
697
            {
698
                const std::size_t previous = i;
1,439✔
699

700
                for (std::size_t block_idx = 1, end = lst.constList().size(); block_idx < end; ++block_idx)
3,847✔
701
                    node.list().insert(
4,816✔
702
                        node.constList().begin() + static_cast<std::vector<Node>::difference_type>(i + block_idx),
2,408✔
703
                        lst.list()[block_idx]);
2,408✔
704

705
                node.list().erase(node.constList().begin() + static_cast<std::vector<Node>::difference_type>(previous));
1,439✔
706
            }
1,439✔
707
        }
1,439✔
708
    }
1,439✔
709

710
    void MacroProcessor::throwMacroProcessingError(const std::string& message, const Node& node) const
35✔
711
    {
35✔
712
        const std::optional<CodeErrorContext> maybe_context = [this]() -> std::optional<CodeErrorContext> {
70✔
713
            if (!m_macros_being_applied.empty())
35✔
714
            {
715
                const Node& origin = m_macros_being_applied.front();
24✔
716
                return CodeErrorContext(
48✔
717
                    origin.filename(),
24✔
718
                    origin.position(),
24✔
719
                    /* from_macro_expansion= */ true);
720
            }
24✔
721
            return std::nullopt;
11✔
722
        }();
35✔
723

724
        throw CodeError(message, CodeErrorContext(node.filename(), node.position()), maybe_context);
35✔
725
    }
70✔
726

727
    void MacroProcessor::checkMacroTypeError(const std::string& macro, const std::string& arg, const NodeType expected, const Node& actual) const
206✔
728
    {
206✔
729
        if (actual.nodeType() != expected)
206✔
730
            throwMacroProcessingError(
12✔
731
                fmt::format(
18✔
732
                    "When expanding `{}', expected '{}' to be a {}, got a {}: {}",
6✔
733
                    macro,
6✔
734
                    arg,
6✔
735
                    std::string(nodeTypes[static_cast<std::size_t>(expected)]),
6✔
736
                    typeToString(actual),
6✔
737
                    actual.repr()),
6✔
738
                actual);
6✔
739
    }
206✔
740
}
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