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

ArkScript-lang / Ark / 23025455915

12 Mar 2026 09:46PM UTC coverage: 93.657% (-0.1%) from 93.778%
23025455915

Pull #655

github

web-flow
Merge 601ead9d8 into 1ba43e058
Pull Request #655: Feat/everything is an expr

41 of 43 new or added lines in 3 files covered. (95.35%)

12 existing lines in 2 files now uncovered.

9523 of 10168 relevant lines covered (93.66%)

270525.39 hits per line

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

95.48
/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp
1
#include <Ark/Compiler/Lowerer/ASTLowerer.hpp>
2

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

8
#include <Ark/Error/Exceptions.hpp>
9
#include <Ark/Error/Diagnostics.hpp>
10
#include <Ark/Utils/Literals.hpp>
11
#include <Ark/Builtins/Builtins.hpp>
12

13
namespace Ark::internal
14
{
15
    using namespace literals;
16

17
    ASTLowerer::ASTLowerer(const unsigned debug) :
914✔
18
        Pass("ASTLowerer", debug)
457✔
19
    {}
1,371✔
20

21
    void ASTLowerer::addToTables(const std::vector<std::string>& symbols, const std::vector<ValTableElem>& constants)
15✔
22
    {
15✔
23
        std::ranges::copy(symbols, std::back_inserter(m_symbols));
15✔
24
        std::ranges::copy(constants, std::back_inserter(m_values));
15✔
25
    }
15✔
26

27
    void ASTLowerer::offsetPagesBy(const std::size_t offset)
15✔
28
    {
15✔
29
        m_start_page_at_offset = offset;
15✔
30
    }
15✔
31

32
    void ASTLowerer::process(Node& ast)
364✔
33
    {
364✔
34
        m_logger.traceStart("process");
364✔
35
        const Page global = createNewCodePage();
364✔
36

37
        // gather symbols, values, and start to create code segments
38
        compileExpression(
728✔
39
            ast,
364✔
40
            /* current_page= */ global,
364✔
41
            // the offset is non-zero when coming from the debugger, and setting is_result_unused to
42
            // true will avoid adding a LOAD_FAST/LOAD_FAST_FROM_INDEX after a let/mut/set
43
            /* is_result_unused= */ m_start_page_at_offset != 0,
364✔
44
            /* is_terminal= */ false);
45
        m_logger.traceEnd();
364✔
46
    }
364✔
47

48
    const std::vector<IR::Block>& ASTLowerer::intermediateRepresentation() const noexcept
322✔
49
    {
322✔
50
        return m_code_pages;
322✔
51
    }
52

53
    const std::vector<std::string>& ASTLowerer::symbols() const noexcept
633✔
54
    {
633✔
55
        return m_symbols;
633✔
56
    }
57

58
    const std::vector<ValTableElem>& ASTLowerer::values() const noexcept
633✔
59
    {
633✔
60
        return m_values;
633✔
61
    }
62

63
    std::optional<Instruction> ASTLowerer::getOperator(const std::string& name) noexcept
56,266✔
64
    {
56,266✔
65
        const auto it = std::ranges::find(Language::operators, name);
56,266✔
66
        if (it != Language::operators.end())
56,266✔
67
            return static_cast<Instruction>(std::distance(Language::operators.begin(), it) + FIRST_OPERATOR);
14,183✔
68
        return std::nullopt;
42,083✔
69
    }
56,266✔
70

71
    std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
37,320✔
72
    {
37,320✔
73
        const auto it = std::ranges::find_if(Builtins::builtins,
37,320✔
74
                                             [&name](const std::pair<std::string, Value>& element) -> bool {
3,175,779✔
75
                                                 return name == element.first;
3,138,459✔
76
                                             });
77
        if (it != Builtins::builtins.end())
37,320✔
78
            return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
3,777✔
79
        return std::nullopt;
33,543✔
80
    }
37,320✔
81

82
    std::optional<Instruction> ASTLowerer::getListInstruction(const std::string& name) noexcept
30,595✔
83
    {
30,595✔
84
        const auto it = std::ranges::find(Language::listInstructions, name);
30,595✔
85
        if (it != Language::listInstructions.end())
30,595✔
86
            return static_cast<Instruction>(std::distance(Language::listInstructions.begin(), it) + LIST);
7,680✔
87
        return std::nullopt;
22,915✔
88
    }
30,595✔
89

90
    bool ASTLowerer::isBreakpoint(const Node& node)
55,325✔
91
    {
55,325✔
92
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
55,325✔
93
            return node.constList().front().string() == "breakpoint";
22,061✔
94
        return false;
33,264✔
95
    }
55,325✔
96

97
    bool ASTLowerer::nodeProducesOutput(const Node& node)
60,665✔
98
    {
60,665✔
99
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
60,665✔
100
            // a 'begin' node produces a value if the last node in it produces a value
101
            return (node.constList()[0].keyword() == Keyword::Begin && node.constList().size() > 1 && nodeProducesOutput(node.constList().back())) ||
954✔
102
                // a function always produces a value ; even if it ends with a node not producing one, the VM returns nil
103
                node.constList()[0].keyword() == Keyword::Fun ||
477✔
104
                // a let/mut/set pushes the value that was assigned
105
                node.constList()[0].keyword() == Keyword::Let ||
137✔
106
                node.constList()[0].keyword() == Keyword::Mut ||
594✔
107
                node.constList()[0].keyword() == Keyword::Set ||
274✔
108
                // a condition produces a value if all its branches produce a value
109
                (node.constList()[0].keyword() == Keyword::If &&
594✔
110
                 nodeProducesOutput(node.constList()[2]) &&
133✔
111
                 (node.constList().size() == 3 || nodeProducesOutput(node.constList()[3])));
133✔
112
        // breakpoint do not produce values
113
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
60,188✔
114
        {
115
            const std::string& name = node.constList().front().string();
10,266✔
116
            return name != "breakpoint";
10,266✔
117
        }
10,266✔
118
        return true;  // any other node, function call, symbol, number...
49,922✔
119
    }
60,665✔
120

121
    bool ASTLowerer::isUnaryInst(const Instruction inst) noexcept
14,163✔
122
    {
14,163✔
123
        switch (inst)
14,163✔
124
        {
2,749✔
125
            case NOT: [[fallthrough]];
126
            case LEN: [[fallthrough]];
127
            case IS_EMPTY: [[fallthrough]];
128
            case TAIL: [[fallthrough]];
129
            case HEAD: [[fallthrough]];
130
            case IS_NIL: [[fallthrough]];
131
            case TO_NUM: [[fallthrough]];
132
            case TO_STR: [[fallthrough]];
133
            case TYPE:
134
                return true;
14,163✔
135

136
            default:
137
                return false;
11,414✔
138
        }
139
    }
14,163✔
140

141
    bool ASTLowerer::isTernaryInst(const Instruction inst) noexcept
23,187✔
142
    {
23,187✔
143
        switch (inst)
23,187✔
144
        {
426✔
145
            case AT_AT:
146
                return true;
23,187✔
147

148
            default:
149
                return false;
22,761✔
150
        }
151
    }
23,187✔
152

153
    bool ASTLowerer::isRepeatableOperation(const Instruction inst) noexcept
231✔
154
    {
231✔
155
        switch (inst)
231✔
156
        {
117✔
157
            case ADD: [[fallthrough]];
158
            case SUB: [[fallthrough]];
159
            case MUL: [[fallthrough]];
160
            case DIV:
161
                return true;
231✔
162

163
            default:
164
                return false;
114✔
165
        }
166
    }
231✔
167

168
    void ASTLowerer::warning(const std::string& message, const Node& node)
2✔
169
    {
2✔
170
        m_logger.warn("{}", Diagnostics::makeContextWithNode(message, node, m_logger.colorize()));
2✔
171
    }
2✔
172

173
    void ASTLowerer::buildAndThrowError(const std::string& message, const Node& node)
42✔
174
    {
42✔
175
        throw CodeError(message, CodeErrorContext(node.filename(), node.position()));
42✔
176
    }
42✔
177

178
    void ASTLowerer::makeError(const ErrorKind kind, const Node& node, const std::string& additional_ctx)
8✔
179
    {
8✔
180
        const std::string invalid_node_msg = "The given node doesn't return a value, and thus can't be used as an expression.";
8✔
181

182
        switch (kind)
8✔
183
        {
3✔
184
            case ErrorKind::InvalidNodeMacro:
185
                buildAndThrowError(fmt::format("Invalid node ; if it was computed by a macro, check that a node is returned"), node);
6✔
186
                break;
187

188
            case ErrorKind::InvalidNodeNoReturnValue:
189
                buildAndThrowError(fmt::format("Invalid node inside call to `{}'. {}", additional_ctx, invalid_node_msg), node);
4✔
190
                break;
191

192
            case ErrorKind::InvalidNodeInTailCallNoReturnValue:
193
                buildAndThrowError(fmt::format("Invalid node inside tail call to `{}'. {}", additional_ctx, invalid_node_msg), node);
2✔
194
                break;
195

196
            case ErrorKind::InvalidNodeInOperatorNoReturnValue:
197
                buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'. {}", additional_ctx, invalid_node_msg), node);
1✔
198
                break;
199
        }
×
200
    }
16✔
201

202
    void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
109,159✔
203
    {
109,159✔
204
        // register symbols
205
        if (x.nodeType() == NodeType::Symbol)
109,159✔
206
            compileSymbol(x, p, is_result_unused, /* can_use_ref= */ true);
35,414✔
207
        else if (x.nodeType() == NodeType::Field)
73,745✔
208
        {
209
            // the parser guarantees us that there is at least 2 elements (eg: a.b)
210
            compileSymbol(x.list()[0], p, is_result_unused, /* can_use_ref= */ true);
1,821✔
211
            for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
3,648✔
212
            {
213
                uint16_t i = addSymbol(*it);
1,827✔
214
                page(p).emplace_back(GET_FIELD, i);
1,827✔
215
            }
1,827✔
216
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,821✔
217
        }
1,821✔
218
        // register values
219
        else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
71,924✔
220
        {
221
            uint16_t i = addValue(x);
18,087✔
222

223
            if (!is_result_unused)
18,087✔
224
                page(p).emplace_back(LOAD_CONST, i);
18,087✔
225
        }
18,087✔
226
        // namespace nodes
227
        else if (x.nodeType() == NodeType::Namespace)
53,837✔
228
            compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
136✔
229
        else if (x.nodeType() == NodeType::List)
53,701✔
230
        {
231
            // empty code block should be nil
232
            if (x.constList().empty())
53,696✔
233
            {
234
                if (!is_result_unused)
×
235
                {
236
                    static const std::optional<uint16_t> nil = getBuiltin("nil");
100✔
237
                    page(p).emplace_back(BUILTIN, nil.value());
×
238
                }
×
239
            }
×
240
            // list instructions
241
            else if (const auto head = x.constList()[0]; head.nodeType() == NodeType::Symbol && getListInstruction(head.string()).has_value())
107,392✔
242
                compileListInstruction(x, p, is_result_unused);
3,840✔
243
            else if (head.nodeType() == NodeType::Symbol && head.string() == Language::Apply)
49,856✔
244
                compileApplyInstruction(x, p, is_result_unused);
189✔
245
            // registering structures
246
            else if (head.nodeType() == NodeType::Keyword)
49,667✔
247
            {
248
                switch (const Keyword keyword = head.keyword())
26,180✔
249
                {
3,026✔
250
                    case Keyword::If:
251
                        compileIf(x, p, is_result_unused, is_terminal);
3,026✔
252
                        break;
15,778✔
253

254
                    case Keyword::Set:
255
                        [[fallthrough]];
256
                    case Keyword::Let:
257
                        [[fallthrough]];
258
                    case Keyword::Mut:
259
                        compileLetMutSet(keyword, x, p, is_result_unused);
12,754✔
260
                        break;
16,558✔
261

262
                    case Keyword::Fun:
263
                        compileFunction(x, p, is_result_unused);
3,811✔
264
                        break;
9,105✔
265

266
                    case Keyword::Begin:
267
                    {
268
                        for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
26,199✔
269
                            compileExpression(
62,700✔
270
                                x.list()[i],
20,900✔
271
                                p,
20,900✔
272
                                // All the nodes in a 'begin' (except for the last one) are producing a result that we want to drop.
273
                                /* is_result_unused= */ (i != size - 1) || is_result_unused,
20,900✔
274
                                // If the 'begin' is a terminal node, only its last node is terminal.
275
                                /* is_terminal= */ is_terminal && (i == size - 1));
20,900✔
276
                        break;
5,255✔
277
                    }
1,286✔
278

279
                    case Keyword::While:
280
                        compileWhile(x, p);
1,286✔
281
                        break;
1,287✔
282

283
                    case Keyword::Import:
284
                        compilePluginImport(x, p);
2✔
285
                        break;
4✔
286

287
                    case Keyword::Del:
288
                        page(p).emplace_back(DEL, addSymbol(x.constList()[1]));
2✔
289
                        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
290
                        break;
2✔
291
                }
26,180✔
292
            }
26,121✔
293
            else
294
            {
295
                // If we are here, we should have a function name via the m_opened_vars.
296
                // Push arguments first, then function name, then call it.
297
                handleCalls(x, p, is_result_unused, is_terminal);
23,487✔
298
            }
299
        }
53,596✔
300
        else if (x.nodeType() != NodeType::Unused)
5✔
301
            buildAndThrowError(
×
302
                fmt::format(
×
303
                    "NodeType `{}' not handled in ASTLowerer::compileExpression. Please fill an issue on GitHub: https://github.com/ArkScript-lang/Ark",
×
304
                    typeToString(x)),
×
305
                x);
×
306
    }
109,159✔
307

308
    void ASTLowerer::compileSymbol(const Node& x, const Page p, const bool is_result_unused, const bool can_use_ref)
37,320✔
309
    {
37,320✔
310
        const std::string& name = x.string();
37,320✔
311

312
        if (const auto it_builtin = getBuiltin(name))
74,640✔
313
            page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
3,777✔
314
        else if (getOperator(name).has_value())
33,543✔
315
            buildAndThrowError(fmt::format("Found a freestanding operator: `{}`. It can not be used as value like `+', where (let add +) (add 1 2) would be valid", name), x);
2✔
316
        else
317
        {
318
            if (can_use_ref)
33,541✔
319
            {
320
                const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
33,456✔
321
                if (maybe_local_idx.has_value())
33,456✔
322
                    page(p).emplace_back(LOAD_FAST_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
12,536✔
323
                else
324
                    page(p).emplace_back(LOAD_FAST, addSymbol(x));
20,920✔
325
            }
33,456✔
326
            else
327
                page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
85✔
328
        }
329

330
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
37,318✔
331

332
        if (is_result_unused)
37,318✔
333
        {
334
            warning("Statement has no effect", x);
1✔
335
            page(p).emplace_back(POP);
1✔
336
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1✔
337
        }
1✔
338
    }
37,320✔
339

340
    void ASTLowerer::compileListInstruction(Node& x, const Page p, const bool is_result_unused)
3,840✔
341
    {
3,840✔
342
        const Node head = x.constList()[0];
3,840✔
343
        const std::string& name = head.string();
3,840✔
344
        const Instruction inst = getListInstruction(name).value();
3,840✔
345

346
        // length of at least 1 since we got a symbol name
347
        const auto argc = x.constList().size() - 1u;
3,840✔
348
        // error, can not use append/concat/pop (and their in place versions) with a <2 length argument list
349
        if (argc < 2 && APPEND <= inst && inst <= SET_AT_2_INDEX)
3,840✔
350
            buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), head);
6✔
351
        if (std::cmp_greater(argc, MaxValue16Bits))
3,834✔
352
            buildAndThrowError(fmt::format("Too many arguments ({}), exceeds {}", argc, MaxValue16Bits), x);
1✔
353
        if (argc != 3 && inst == SET_AT_INDEX)
3,833✔
354
            buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), head);
1✔
355
        if (argc != 4 && inst == SET_AT_2_INDEX)
3,832✔
356
            buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), head);
1✔
357

358
        // compile arguments in reverse order
359
        for (std::size_t i = x.constList().size() - 1u; i > 0; --i)
12,602✔
360
        {
361
            Node& node = x.list()[i];
8,771✔
362
            if (nodeProducesOutput(node))
8,771✔
363
                compileExpression(node, p, false, false);
8,770✔
364
            else
365
                makeError(ErrorKind::InvalidNodeNoReturnValue, node, name);
1✔
366
        }
8,771✔
367

368
        // put inst and number of arguments
369
        std::size_t inst_argc = 0;
3,830✔
370
        switch (inst)
3,830✔
371
        {
2,869✔
372
            case LIST:
373
                inst_argc = argc;
2,869✔
374
                break;
3,685✔
375

376
            case APPEND:
377
                [[fallthrough]];
378
            case APPEND_IN_PLACE:
379
                [[fallthrough]];
380
            case CONCAT:
381
                [[fallthrough]];
382
            case CONCAT_IN_PLACE:
383
                inst_argc = argc - 1;
816✔
384
                break;
822✔
385

386
            case POP_LIST:
387
                inst_argc = 0;
6✔
388
                break;
145✔
389

390
            case SET_AT_INDEX:
391
                [[fallthrough]];
392
            case SET_AT_2_INDEX:
393
                [[fallthrough]];
394
            case POP_LIST_IN_PLACE:
395
                inst_argc = is_result_unused ? 0 : 1;
139✔
396
                break;
139✔
397

398
            default:
UNCOV
399
                break;
×
400
        }
3,830✔
401
        page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
3,830✔
402
        page(p).back().setSourceLocation(head.filename(), head.position().start.line);
3,830✔
403

404
        if (!is_result_unused && (inst == APPEND_IN_PLACE || inst == CONCAT_IN_PLACE))
3,830✔
405
        {
406
            // Load the first argument which should be a symbol (or field),
407
            // that append!/concat! write to, so that we have its new value available.
408
            compileExpression(x.list()[1], p, false, false);
20✔
409
        }
20✔
410

411
        // append!, concat!, pop!, @= and @@= can push to the stack, but not using its returned value isn't an error
412
        if (is_result_unused && (inst == LIST || inst == APPEND || inst == CONCAT || inst == POP_LIST))
3,830✔
413
        {
414
            warning("Ignoring return value of function", x);
1✔
415
            page(p).emplace_back(POP);
1✔
416
        }
1✔
417
    }
3,849✔
418

419
    void ASTLowerer::compileApplyInstruction(Node& x, const Page p, const bool is_result_unused)
189✔
420
    {
189✔
421
        const Node head = x.constList()[0];
189✔
422
        const auto argc = x.constList().size() - 1u;
189✔
423

424
        if (argc != 2)
189✔
425
            buildAndThrowError(fmt::format("Expected 2 arguments (function, arguments) for apply, got {}", argc), head);
1✔
426

427
        const auto label_return = IR::Entity::Label(m_current_label++);
188✔
428
        page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
188✔
429

430
        for (Node& node : x.list() | std::ranges::views::drop(1))
564✔
431
        {
432
            if (nodeProducesOutput(node))
376✔
433
                compileExpression(node, p, false, false);
375✔
434
            else
435
                makeError(ErrorKind::InvalidNodeNoReturnValue, node, "apply");
1✔
436
        }
376✔
437
        page(p).emplace_back(APPLY);
187✔
438
        // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
439
        page(p).emplace_back(label_return);
187✔
440

441
        if (is_result_unused)
187✔
442
            page(p).emplace_back(POP);
×
443
    }
191✔
444

445
    void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
3,026✔
446
    {
3,026✔
447
        if (x.constList().size() == 1)
3,026✔
448
            buildAndThrowError("Invalid condition: missing 'cond' and 'then' nodes, expected (if cond then)", x);
2✔
449
        if (x.constList().size() == 2)
3,025✔
450
            buildAndThrowError(fmt::format("Invalid condition: missing 'then' node, expected (if {} then)", x.constList()[1].repr()), x);
1✔
451

452
        // compile condition
453
        compileExpression(x.list()[1], p, false, false);
3,024✔
454
        page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
3,024✔
455

456
        // jump only if needed to the "true" branch
457
        const auto label_then = IR::Entity::Label(m_current_label++);
3,024✔
458
        page(p).emplace_back(IR::Entity::GotoIf(label_then, true));
3,024✔
459

460
        // "false" branch code
461
        if (x.constList().size() == 4)  // we have an else clause
3,024✔
462
        {
463
            m_locals_locator.saveScopeLengthForBranch();
2,311✔
464
            compileExpression(x.list()[3], p, is_result_unused, is_terminal);
2,311✔
465
            page(p).back().setSourceLocation(x.constList()[3].filename(), x.constList()[3].position().start.line);
2,311✔
466
            m_locals_locator.dropVarsForBranch();
2,311✔
467
        }
2,311✔
468

469
        // when else is finished, jump to end
470
        const auto label_end = IR::Entity::Label(m_current_label++);
3,024✔
471
        page(p).emplace_back(IR::Entity::Goto(label_end));
3,024✔
472

473
        // absolute address to jump to if condition is true
474
        page(p).emplace_back(label_then);
3,024✔
475
        // if code
476
        m_locals_locator.saveScopeLengthForBranch();
3,024✔
477
        compileExpression(x.list()[2], p, is_result_unused, is_terminal);
3,024✔
478
        page(p).back().setSourceLocation(x.constList()[2].filename(), x.constList()[2].position().start.line);
3,024✔
479
        m_locals_locator.dropVarsForBranch();
3,024✔
480
        // set jump to end pos
481
        page(p).emplace_back(label_end);
3,024✔
482
    }
3,026✔
483

484
    void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused)
3,811✔
485
    {
3,811✔
486
        if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List)
3,813✔
487
            buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args);
1✔
488
        if (x.constList().size() != 3)
3,810✔
489
            makeError(ErrorKind::InvalidNodeMacro, x, "");
1✔
490

491
        // capture, if needed
492
        std::size_t capture_inst_count = 0;
3,809✔
493
        for (const auto& node : x.constList()[1].constList())
9,907✔
494
        {
495
            if (node.nodeType() == NodeType::Capture)
6,098✔
496
            {
497
                const uint16_t symbol_id = addSymbol(node);
227✔
498

499
                // We have an unqualified name that isn't the captured name
500
                // This means we need to rename the captured value
501
                if (const auto& maybe_nqn = node.getUnqualifiedName(); maybe_nqn.has_value() && maybe_nqn.value() != node.string())
454✔
502
                {
503
                    const uint16_t nqn_id = addSymbol(Node(NodeType::Symbol, maybe_nqn.value()));
14✔
504

505
                    page(p).emplace_back(RENAME_NEXT_CAPTURE, nqn_id);
14✔
506
                    page(p).emplace_back(CAPTURE, symbol_id);
14✔
507
                }
14✔
508
                else
509
                    page(p).emplace_back(CAPTURE, symbol_id);
213✔
510

511
                ++capture_inst_count;
227✔
512
            }
227✔
513
        }
6,098✔
514
        const bool is_closure = capture_inst_count > 0;
3,809✔
515

516
        m_locals_locator.createScope(
7,618✔
517
            is_closure
3,809✔
518
                ? LocalsLocator::ScopeType::Closure
519
                : LocalsLocator::ScopeType::Function);
520

521
        // create new page for function body
522
        const auto function_body_page = createNewCodePage();
3,809✔
523
        // save page_id into the constants table as PageAddr and load the const
524
        page(p).emplace_back(is_closure ? MAKE_CLOSURE : LOAD_CONST, addValue(function_body_page.index, x));
3,809✔
525

526
        // pushing arguments from the stack into variables in the new scope
527
        for (const auto& node : x.constList()[1].constList())
9,907✔
528
        {
529
            if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg)
6,098✔
530
            {
531
                page(function_body_page).emplace_back(STORE, addSymbol(node));
5,016✔
532
                m_locals_locator.addLocal(node.string());
5,016✔
533
            }
5,016✔
534
            else if (node.nodeType() == NodeType::RefArg)
1,082✔
535
            {
536
                page(function_body_page).emplace_back(STORE_REF, addSymbol(node));
855✔
537
                m_locals_locator.addLocal(node.string());
855✔
538
            }
855✔
539
        }
6,098✔
540

541
        // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls.
542
        // This way we can continue to safely apply optimisations on
543
        // (let name (fun (e) (map lst (fun (e) (name e)))))
544
        // Otherwise, `name` would have been optimized to a GET_CURRENT_PAGE_ADDRESS, which would have returned the wrong page.
545
        if (x.isAnonymousFunction())
3,809✔
546
            m_opened_vars.emplace("#anonymous");
402✔
547
        // push body of the function
548
        compileExpression(x.list()[2], function_body_page, false, true);
3,809✔
549
        if (x.isAnonymousFunction())
3,809✔
550
            m_opened_vars.pop();
402✔
551

552
        // return last value on the stack
553
        page(function_body_page).emplace_back(RET);
3,809✔
554
        m_locals_locator.deleteScope();
3,809✔
555

556
        // if the computed function is unused, pop it
557
        if (is_result_unused)
3,809✔
558
        {
559
            warning("Unused declared function", x);
×
560
            page(p).emplace_back(POP);
×
561
        }
×
562
    }
3,811✔
563

564
    void ASTLowerer::compileLetMutSet(const Keyword n, Node& x, const Page p, const bool is_result_unused)
12,754✔
565
    {
12,754✔
566
        if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol)
12,761✔
567
            buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym);
×
568
        if (x.constList().size() != 3)
12,754✔
569
            makeError(ErrorKind::InvalidNodeMacro, x, "");
1✔
570

571
        const std::string name = x.constList()[1].string();
12,753✔
572
        uint16_t i = addSymbol(x.constList()[1]);
12,753✔
573

574
        if (!m_opened_vars.empty() && m_opened_vars.top() == name)
12,753✔
575
            buildAndThrowError("Can not define a variable using the same name as the function it is defined inside. You need to rename the function or the variable", x);
1✔
576

577
        const bool is_function = x.constList()[2].isFunction();
12,752✔
578
        if (is_function)
12,752✔
579
        {
580
            m_opened_vars.push(name);
3,409✔
581
            x.list()[2].setFunctionKind(/* anonymous= */ false);
3,409✔
582
        }
3,409✔
583

584
        // put value before symbol id
585
        // starting at index = 2 because x is a (let|mut|set variable ...) node
586
        compileExpression(x.list()[2], p, false, false);
12,752✔
587

588
        if (n == Keyword::Let || n == Keyword::Mut)
12,747✔
589
        {
590
            page(p).emplace_back(STORE, i);
8,159✔
591
            m_locals_locator.addLocal(name);
8,159✔
592

593
            if (!is_result_unused)
8,159✔
594
                page(p).emplace_back(LOAD_FAST_BY_INDEX, 0);
34✔
595
        }
8,159✔
596
        else
597
        {
598
            page(p).emplace_back(SET_VAL, i);
4,588✔
599

600
            if (!is_result_unused)
4,588✔
601
                page(p).emplace_back(LOAD_FAST, i);
41✔
602
        }
603

604
        if (is_function)
12,747✔
605
            m_opened_vars.pop();
3,404✔
606
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
12,747✔
607
    }
12,755✔
608

609
    void ASTLowerer::compileWhile(Node& x, const Page p)
1,286✔
610
    {
1,286✔
611
        if (x.constList().size() != 3)
1,286✔
612
            makeError(ErrorKind::InvalidNodeMacro, x, "");
1✔
613

614
        m_locals_locator.createScope();
1,285✔
615
        page(p).emplace_back(CREATE_SCOPE);
1,285✔
616
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,285✔
617

618
        // save current position to jump there at the end of the loop
619
        const auto label_loop = IR::Entity::Label(m_current_label++);
1,285✔
620
        page(p).emplace_back(label_loop);
1,285✔
621
        // push condition
622
        compileExpression(x.list()[1], p, false, false);
1,285✔
623
        // absolute jump to end of block if condition is false
624
        const auto label_end = IR::Entity::Label(m_current_label++);
1,285✔
625
        page(p).emplace_back(IR::Entity::GotoIf(label_end, false));
1,285✔
626
        // push code to page
627
        compileExpression(x.list()[2], p, true, false);
1,285✔
628

629
        // reset the scope at the end of the loop so that indices are still valid
630
        // otherwise, (while true { (let a 5) (print a) (let b 6) (print b) })
631
        // would print 5, 6, then only 6 as we emit LOAD_SYMBOL_FROM_INDEX 0 and b is the last in the scope
632
        // loop, jump to the condition
633
        page(p).emplace_back(IR::Entity::Goto(label_loop, RESET_SCOPE_JUMP));
1,285✔
634

635
        // absolute address to jump to if condition is false
636
        page(p).emplace_back(label_end);
1,285✔
637

638
        page(p).emplace_back(POP_SCOPE);
1,285✔
639
        m_locals_locator.deleteScope();
1,285✔
640
    }
1,286✔
641

642
    void ASTLowerer::compilePluginImport(const Node& x, const Page p)
2✔
643
    {
2✔
644
        std::string path;
2✔
645
        const Node package_node = x.constList()[1];
2✔
646
        for (std::size_t i = 0, end = package_node.constList().size(); i < end; ++i)
4✔
647
        {
648
            path += package_node.constList()[i].string();
2✔
649
            if (i + 1 != end)
2✔
650
                path += "/";
×
651
        }
2✔
652
        path += ".arkm";
2✔
653

654
        // register plugin path in the constants table
655
        uint16_t id = addValue(Node(NodeType::String, path));
2✔
656
        // add plugin instruction + id of the constant referring to the plugin path
657
        page(p).emplace_back(PLUGIN, id);
2✔
658
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
659
    }
2✔
660

661
    void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call)
8,791✔
662
    {
8,791✔
663
        const auto node = call.constList()[0];
8,791✔
664

665
        // push the arguments in reverse order because the function loads its arguments in the order they are defined:
666
        // (fun (a b c) ...) -> load 'a', then 'b', then 'c'
667
        // We have to push arguments in this order and load them in reverse, because we are using references internally,
668
        // which can cause problems for recursive functions that swap their arguments around.
669
        // Eg (let foo (fun (a b c) (if (> a 0) (foo (- a 1) c (+ b c)) 1))) (foo 12 0 1)
670
        // On the second self-call, b and c would have the same value, since we set c to (+ b c), and we pushed c as the
671
        // value for argument b, but loaded it as a reference.
672
        for (Node& value : std::ranges::drop_view(call.list(), 1) | std::views::reverse)
24,384✔
673
        {
674
            // FIXME: in (foo a b (breakpoint (< c 0)) c), we will push c before the breakpoint
675
            if (nodeProducesOutput(value) || isBreakpoint(value))
15,593✔
676
            {
677
                // we have to disallow usage of references in tail calls, because if we shuffle arguments around while using refs, they will end up with the same value
678
                if (value.nodeType() == NodeType::Symbol && is_tail_call)
15,591✔
679
                    compileSymbol(value, p, false, /* can_use_ref= */ false);
85✔
680
                else
681
                    compileExpression(value, p, false, false);
15,506✔
682
            }
15,585✔
683
            else
684
                makeError(is_tail_call ? ErrorKind::InvalidNodeInTailCallNoReturnValue : ErrorKind::InvalidNodeNoReturnValue, value, node.repr());
2✔
685
        }
15,593✔
686
    }
8,799✔
687

688
    void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal)
23,458✔
689
    {
23,458✔
690
        const Node& node = x.constList()[0];
23,458✔
691
        bool matched = false;
23,458✔
692

693
        if (node.nodeType() == NodeType::Symbol)
23,458✔
694
        {
695
            if (node.string() == Language::And || node.string() == Language::Or)
22,726✔
696
            {
697
                matched = true;
511✔
698
                handleShortcircuit(x, p);
511✔
699
            }
511✔
700
            if (const auto maybe_operator = getOperator(node.string()); maybe_operator.has_value())
36,907✔
701
            {
702
                matched = true;
14,181✔
703
                if (maybe_operator.value() == BREAKPOINT)
14,181✔
704
                    is_result_unused = false;
17✔
705
                handleOperator(x, p, maybe_operator.value());
14,181✔
706
            }
14,181✔
707
        }
22,726✔
708

709
        if (!matched)
23,458✔
710
        {
711
            // if nothing else matched, then compile a function call
712
            if (handleFunctionCall(x, p, is_terminal))
8,783✔
713
                // if it returned true, we compiled a tail call, skip the POP at the end
714
                return;
113✔
715
        }
8,670✔
716

717
        if (is_result_unused)
23,345✔
718
            page(p).emplace_back(POP);
3,559✔
719
    }
23,458✔
720

721
    void ASTLowerer::handleShortcircuit(Node& x, const Page p)
511✔
722
    {
511✔
723
        const Node& node = x.constList()[0];
511✔
724
        const auto name = node.string();  // and / or
511✔
725
        const Instruction inst = name == Language::And ? SHORTCIRCUIT_AND : SHORTCIRCUIT_OR;
511✔
726

727
        // short circuit implementation
728
        if (x.constList().size() < 3)
511✔
729
            buildAndThrowError(
2✔
730
                fmt::format(
4✔
731
                    "Expected at least 2 arguments while compiling '{}', got {}",
2✔
732
                    name,
733
                    x.constList().size() - 1),
2✔
734
                x);
2✔
735

736
        if (!nodeProducesOutput(x.list()[1]))
509✔
UNCOV
737
            buildAndThrowError(
×
UNCOV
738
                fmt::format(
×
UNCOV
739
                    "Can not use `{}' inside a `{}' expression, as it doesn't return a value",
×
UNCOV
740
                    x.list()[1].repr(), name),
×
UNCOV
741
                x.list()[1]);
×
742
        compileExpression(x.list()[1], p, false, false);
509✔
743

744
        const auto label_shortcircuit = IR::Entity::Label(m_current_label++);
509✔
745
        auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, inst);
509✔
746
        page(p).emplace_back(shortcircuit_entity);
509✔
747

748
        for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
1,143✔
749
        {
750
            if (!nodeProducesOutput(x.list()[i]))
634✔
751
                buildAndThrowError(
1✔
752
                    fmt::format(
2✔
753
                        "Can not use `{}' inside a `{}' expression, as it doesn't return a value",
1✔
754
                        x.list()[i].repr(), name),
1✔
755
                    x.list()[i]);
1✔
756
            compileExpression(x.list()[i], p, false, false);
633✔
757
            if (i + 1 != end)
633✔
758
                page(p).emplace_back(shortcircuit_entity);
125✔
759
        }
633✔
760

761
        page(p).emplace_back(label_shortcircuit);
508✔
762
    }
514✔
763

764
    void ASTLowerer::handleOperator(Node& x, const Page p, const Instruction op)
14,181✔
765
    {
14,181✔
766
        constexpr std::size_t start_index = 1;
14,181✔
767
        const Node& node = x.constList()[0];
14,181✔
768
        const auto op_name = Language::operators[static_cast<std::size_t>(op - FIRST_OPERATOR)];
14,181✔
769

770

771
        // push arguments on current page
772
        std::size_t exp_count = 0;
14,181✔
773
        for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
40,016✔
774
        {
775
            const bool is_breakpoint = isBreakpoint(x.constList()[index]);
25,835✔
776
            if (nodeProducesOutput(x.constList()[index]) || is_breakpoint)
25,835✔
777
                compileExpression(x.list()[index], p, false, false);
25,834✔
778
            else
779
                makeError(ErrorKind::InvalidNodeInOperatorNoReturnValue, x.constList()[index], node.repr());
1✔
780

781
            if (!is_breakpoint)
25,834✔
782
                exp_count++;
25,832✔
783

784
            // in order to be able to handle things like (op A B C D...)
785
            // which should be transformed into A B op C op D op...
786
            if (exp_count >= 2 && !isTernaryInst(op) && !is_breakpoint)
25,834✔
787
                page(p).emplace_back(op);
11,445✔
788
        }
25,835✔
789

790
        if (isBreakpoint(x))
14,180✔
791
        {
792
            if (exp_count > 1)
17✔
793
                buildAndThrowError(fmt::format("`{}' expected at most one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
794
            page(p).emplace_back(op, exp_count);
16✔
795
        }
16✔
796
        else if (isUnaryInst(op))
14,163✔
797
        {
798
            if (exp_count != 1)
2,749✔
799
                buildAndThrowError(fmt::format("`{}' expected one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
800
            page(p).emplace_back(op);
2,748✔
801
        }
2,748✔
802
        else if (isTernaryInst(op))
11,414✔
803
        {
804
            if (exp_count != 3)
107✔
805
                buildAndThrowError(fmt::format("`{}' expected three arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
806
            page(p).emplace_back(op);
106✔
807
        }
106✔
808
        else if (exp_count <= 1)
11,307✔
809
            buildAndThrowError(fmt::format("`{}' expected two arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
2✔
810

811
        // need to check we didn't push the (op A B C D...) things for operators not supporting it
812
        if (exp_count > 2 && !isRepeatableOperation(op) && !isTernaryInst(op))
14,175✔
813
            buildAndThrowError(fmt::format("`{}' requires 2 arguments, but got {}.", op_name, exp_count), x);
8✔
814

815
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
14,167✔
816
    }
14,181✔
817

818
    bool ASTLowerer::handleFunctionCall(Node& x, const Page p, const bool is_terminal)
8,794✔
819
    {
8,794✔
820
        constexpr std::size_t start_index = 1;
8,794✔
821
        Node& node = x.list()[0];
8,794✔
822

823
        if (is_terminal && node.nodeType() == NodeType::Symbol && isFunctionCallingItself(node.string()))
8,794✔
824
        {
825
            pushFunctionCallArguments(x, p, /* is_tail_call= */ true);
113✔
826

827
            // jump to the top of the function
828
            page(p).emplace_back(TAIL_CALL_SELF);
113✔
829
            page(p).back().setSourceLocation(node.filename(), node.position().start.line);
113✔
830
            return true;  // skip the potential Instruction::POP at the end
113✔
831
        }
832

833
        if (!nodeProducesOutput(node))
8,681✔
834
            buildAndThrowError(fmt::format("Can not call `{}', as it doesn't return a value", node.repr()), node);
2✔
835

836
        const auto proc_page = createNewCodePage(/* temp= */ true);
8,679✔
837

838
        // compile the function resolution to a separate page
839
        if (node.nodeType() == NodeType::Symbol && isFunctionCallingItself(node.string()))
8,679✔
840
        {
841
            // The function is trying to call itself, but this isn't a tail call.
842
            // We can skip the LOAD_FAST function_name and directly push the current
843
            // function page, which will be quicker than a local variable resolution.
844
            // We set its argument to the symbol id of the function we are calling,
845
            // so that the VM knows the name of the last called function.
846
            page(proc_page).emplace_back(GET_CURRENT_PAGE_ADDR, addSymbol(node));
57✔
847
        }
57✔
848
        else
849
        {
850
            // closure chains have been handled (eg: closure.field.field.function)
851
            compileExpression(node, proc_page, false, false);  // storing proc
8,622✔
852
        }
853

854
        if (m_temp_pages.back().empty())
8,679✔
855
            buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x);
×
856

857
        const auto label_return = IR::Entity::Label(m_current_label++);
8,679✔
858
        page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
8,679✔
859
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
8,679✔
860

861
        pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
8,679✔
862
        // push proc from temp page
863
        for (const auto& inst : m_temp_pages.back())
18,108✔
864
            page(p).push_back(inst);
9,436✔
865
        m_temp_pages.pop_back();
8,672✔
866

867
        // number of arguments
868
        std::size_t args_count = 0;
8,672✔
869
        for (auto it = x.constList().begin() + start_index, it_end = x.constList().end(); it != it_end; ++it)
23,976✔
870
        {
871
            if (it->nodeType() != NodeType::Capture && !isBreakpoint(*it))
15,304✔
872
                args_count++;
15,300✔
873
        }
15,304✔
874
        // call the procedure
875
        page(p).emplace_back(CALL, args_count);
8,672✔
876
        page(p).back().setSourceLocation(node.filename(), node.position().start.line);
8,672✔
877

878
        // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
879
        page(p).emplace_back(label_return);
8,672✔
880
        return false;  // we didn't compile a tail call
8,672✔
881
    }
8,796✔
882

883
    uint16_t ASTLowerer::addSymbol(const Node& sym)
41,756✔
884
    {
41,756✔
885
        // otherwise, add the symbol, and return its id in the table
886
        auto it = std::ranges::find(m_symbols, sym.string());
41,756✔
887
        if (it == m_symbols.end())
41,756✔
888
        {
889
            m_symbols.push_back(sym.string());
7,395✔
890
            it = m_symbols.begin() + static_cast<std::vector<std::string>::difference_type>(m_symbols.size() - 1);
7,395✔
891
        }
7,395✔
892

893
        const auto distance = std::distance(m_symbols.begin(), it);
41,756✔
894
        if (std::cmp_less(distance, MaxValue16Bits))
41,756✔
895
            return static_cast<uint16_t>(distance);
83,512✔
896
        buildAndThrowError(fmt::format("Too many symbols (exceeds {}), aborting compilation.", MaxValue16Bits), sym);
×
897
    }
41,756✔
898

899
    uint16_t ASTLowerer::addValue(const Node& x)
18,089✔
900
    {
18,089✔
901
        const ValTableElem v(x);
18,089✔
902
        auto it = std::ranges::find(m_values, v);
18,089✔
903
        if (it == m_values.end())
18,089✔
904
        {
905
            m_values.push_back(v);
3,974✔
906
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,974✔
907
        }
3,974✔
908

909
        const auto distance = std::distance(m_values.begin(), it);
18,089✔
910
        if (std::cmp_less(distance, MaxValue16Bits))
18,089✔
911
            return static_cast<uint16_t>(distance);
18,089✔
912
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), x);
×
913
    }
18,089✔
914

915
    uint16_t ASTLowerer::addValue(const std::size_t page_id, const Node& current)
3,809✔
916
    {
3,809✔
917
        const ValTableElem v(page_id);
3,809✔
918
        auto it = std::ranges::find(m_values, v);
3,809✔
919
        if (it == m_values.end())
3,809✔
920
        {
921
            m_values.push_back(v);
3,809✔
922
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,809✔
923
        }
3,809✔
924

925
        const auto distance = std::distance(m_values.begin(), it);
3,809✔
926
        if (std::cmp_less(distance, MaxValue16Bits))
3,809✔
927
            return static_cast<uint16_t>(distance);
3,809✔
928
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), current);
×
929
    }
3,809✔
930
}
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