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

ArkScript-lang / Ark / 17621217597

10 Sep 2025 05:06PM UTC coverage: 90.868% (+0.02%) from 90.848%
17621217597

push

github

SuperFola
feat(vm, tests): improving the stack overflow error message, and adding proper tests for it

7960 of 8760 relevant lines covered (90.87%)

158460.88 hits per line

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

94.19
/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
#include <fmt/color.h>
8

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

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

18
    ASTLowerer::ASTLowerer(const unsigned debug) :
341✔
19
        m_logger("ASTLowerer", debug)
341✔
20
    {}
682✔
21

22
    void ASTLowerer::process(Node& ast)
256✔
23
    {
256✔
24
        m_logger.traceStart("process");
256✔
25
        m_code_pages.emplace_back();  // create empty page
256✔
26

27
        // gather symbols, values, and start to create code segments
28
        compileExpression(
256✔
29
            ast,
256✔
30
            /* current_page */ Page { .index = 0, .is_temp = false },
256✔
31
            /* is_result_unused */ false,
32
            /* is_terminal */ false);
33
        m_logger.traceEnd();
256✔
34
    }
256✔
35

36
    const std::vector<IR::Block>& ASTLowerer::intermediateRepresentation() const noexcept
218✔
37
    {
218✔
38
        return m_code_pages;
218✔
39
    }
40

41
    const std::vector<std::string>& ASTLowerer::symbols() const noexcept
430✔
42
    {
430✔
43
        return m_symbols;
430✔
44
    }
45

46
    const std::vector<ValTableElem>& ASTLowerer::values() const noexcept
430✔
47
    {
430✔
48
        return m_values;
430✔
49
    }
50

51
    std::optional<Instruction> ASTLowerer::getOperator(const std::string& name) noexcept
34,096✔
52
    {
34,096✔
53
        const auto it = std::ranges::find(Language::operators, name);
34,096✔
54
        if (it != Language::operators.end())
34,096✔
55
            return static_cast<Instruction>(std::distance(Language::operators.begin(), it) + FIRST_OPERATOR);
8,984✔
56
        return std::nullopt;
25,112✔
57
    }
34,096✔
58

59
    std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
21,727✔
60
    {
21,727✔
61
        const auto it = std::ranges::find_if(Builtins::builtins,
21,727✔
62
                                             [&name](const std::pair<std::string, Value>& element) -> bool {
1,325,565✔
63
                                                 return name == element.first;
1,303,838✔
64
                                             });
65
        if (it != Builtins::builtins.end())
21,727✔
66
            return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
2,396✔
67
        return std::nullopt;
19,331✔
68
    }
21,727✔
69

70
    std::optional<Instruction> ASTLowerer::getListInstruction(const std::string& name) noexcept
20,387✔
71
    {
20,387✔
72
        const auto it = std::ranges::find(Language::listInstructions, name);
20,387✔
73
        if (it != Language::listInstructions.end())
20,387✔
74
            return static_cast<Instruction>(std::distance(Language::listInstructions.begin(), it) + LIST);
5,622✔
75
        return std::nullopt;
14,765✔
76
    }
20,387✔
77

78
    bool ASTLowerer::nodeProducesOutput(const Node& node)
40,123✔
79
    {
40,123✔
80
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
40,123✔
81
            // a begin node produces a value if the last node in it produces a value
82
            return (node.constList()[0].keyword() == Keyword::Begin && node.constList().size() > 1 && nodeProducesOutput(node.constList().back())) ||
580✔
83
                // a function always produces a value ; even if it ends with a node not producing one, the VM returns nil
84
                node.constList()[0].keyword() == Keyword::Fun ||
340✔
85
                // a condition produces a value if all its branches produce a value
86
                (node.constList()[0].keyword() == Keyword::If &&
50✔
87
                 nodeProducesOutput(node.constList()[2]) &&
44✔
88
                 (node.constList().size() == 3 || nodeProducesOutput(node.constList()[3])));
44✔
89
        // in place list instruction, as well as assert, do not produce values
341✔
90
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
39,833✔
91
            return std::ranges::find(Language::UpdateRef, node.constList().front().string()) == Language::UpdateRef.end() &&
13,508✔
92
                node.constList().front().string() != "assert";
6,754✔
93
        return true;  // any other node, function call, symbol, number...
33,079✔
94
    }
40,123✔
95

96
    bool ASTLowerer::isUnaryInst(const Instruction inst) noexcept
8,982✔
97
    {
8,982✔
98
        switch (inst)
8,982✔
99
        {
1,849✔
100
            case NOT: [[fallthrough]];
101
            case LEN: [[fallthrough]];
102
            case EMPTY: [[fallthrough]];
103
            case TAIL: [[fallthrough]];
104
            case HEAD: [[fallthrough]];
105
            case ISNIL: [[fallthrough]];
106
            case TO_NUM: [[fallthrough]];
107
            case TO_STR: [[fallthrough]];
108
            case TYPE:
109
                return true;
8,982✔
110

111
            default:
112
                return false;
7,133✔
113
        }
114
    }
8,982✔
115

116
    bool ASTLowerer::isTernaryInst(const Instruction inst) noexcept
14,398✔
117
    {
14,398✔
118
        switch (inst)
14,398✔
119
        {
143✔
120
            case AT_AT:
121
                return true;
14,398✔
122

123
            default:
124
                return false;
14,255✔
125
        }
126
    }
14,398✔
127

128
    void ASTLowerer::warning(const std::string& message, const Node& node)
×
129
    {
×
130
        fmt::println("{} {}", fmt::styled("Warning", fmt::fg(fmt::color::dark_orange)), Diagnostics::makeContextWithNode(message, node));
×
131
    }
×
132

133
    void ASTLowerer::buildAndThrowError(const std::string& message, const Node& node)
38✔
134
    {
38✔
135
        throw CodeError(message, CodeErrorContext(node.filename(), node.position()));
38✔
136
    }
38✔
137

138
    void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
70,011✔
139
    {
70,011✔
140
        // register symbols
141
        if (x.nodeType() == NodeType::Symbol)
70,011✔
142
            compileSymbol(x, p, is_result_unused);
20,260✔
143
        else if (x.nodeType() == NodeType::Field)
49,751✔
144
        {
145
            // the parser guarantees us that there is at least 2 elements (eg: a.b)
146
            compileSymbol(x.list()[0], p, is_result_unused);
1,467✔
147
            for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
2,942✔
148
            {
149
                uint16_t i = addSymbol(*it);
1,475✔
150
                page(p).emplace_back(GET_FIELD, i);
1,475✔
151
            }
1,475✔
152
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,467✔
153
        }
1,467✔
154
        // register values
155
        else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
48,284✔
156
        {
157
            uint16_t i = addValue(x);
14,435✔
158

159
            if (!is_result_unused)
14,435✔
160
                page(p).emplace_back(LOAD_CONST, i);
14,435✔
161
        }
14,435✔
162
        // namespace nodes
163
        else if (x.nodeType() == NodeType::Namespace)
33,849✔
164
            compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
134✔
165
        else if (x.nodeType() == NodeType::Unused)
33,715✔
166
        {
167
            // do nothing, explicitly
168
        }
5✔
169
        // empty code block should be nil
170
        else if (x.constList().empty())
33,710✔
171
        {
172
            if (!is_result_unused)
×
173
            {
174
                static const std::optional<uint16_t> nil = getBuiltin("nil");
91✔
175
                page(p).emplace_back(BUILTIN, nil.value());
×
176
            }
×
177
        }
×
178
        // list instructions
179
        else if (const auto head = x.constList()[0]; head.nodeType() == NodeType::Symbol && getListInstruction(head.string()).has_value())
67,420✔
180
            compileListInstruction(x, p, is_result_unused);
2,811✔
181
        // registering structures
182
        else if (x.constList()[0].nodeType() == NodeType::Keyword)
30,899✔
183
        {
184
            switch (const Keyword keyword = x.constList()[0].keyword())
15,660✔
185
            {
1,753✔
186
                case Keyword::If:
187
                    compileIf(x, p, is_result_unused, is_terminal);
1,753✔
188
                    break;
9,183✔
189

190
                case Keyword::Set:
191
                    [[fallthrough]];
192
                case Keyword::Let:
193
                    [[fallthrough]];
194
                case Keyword::Mut:
195
                    compileLetMutSet(keyword, x, p);
7,432✔
196
                    break;
10,245✔
197

198
                case Keyword::Fun:
199
                    compileFunction(x, p, is_result_unused);
2,820✔
200
                    break;
5,563✔
201

202
                case Keyword::Begin:
203
                {
204
                    for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
14,816✔
205
                        compileExpression(
24,136✔
206
                            x.list()[i],
12,068✔
207
                            p,
12,068✔
208
                            // All the nodes in a begin (except for the last one) are producing a result that we want to drop.
209
                            (i != size - 1) || is_result_unused,
12,068✔
210
                            // If the begin is a terminal node, only its last node is terminal.
211
                            is_terminal && (i == size - 1));
12,068✔
212
                    break;
2,708✔
213
                }
903✔
214

215
                case Keyword::While:
216
                    compileWhile(x, p);
903✔
217
                    break;
904✔
218

219
                case Keyword::Import:
220
                    compilePluginImport(x, p);
2✔
221
                    break;
4✔
222

223
                case Keyword::Del:
224
                    page(p).emplace_back(DEL, addSymbol(x.constList()[1]));
2✔
225
                    page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
226
                    break;
2✔
227
            }
15,660✔
228
        }
15,605✔
229
        else if (x.nodeType() == NodeType::List)
15,239✔
230
        {
231
            // If we are here, we should have a function name via the m_opened_vars.
232
            // Push arguments first, then function name, then call it.
233
            handleCalls(x, p, is_result_unused, is_terminal);
15,239✔
234
        }
15,213✔
235
        else
236
            buildAndThrowError(
×
237
                fmt::format(
×
238
                    "NodeType `{}' not handled in ASTLowerer::compileExpression. Please fill an issue on GitHub: https://github.com/ArkScript-lang/Ark",
×
239
                    typeToString(x)),
×
240
                x);
×
241
    }
70,011✔
242

243
    void ASTLowerer::compileSymbol(const Node& x, const Page p, const bool is_result_unused)
21,727✔
244
    {
21,727✔
245
        const std::string& name = x.string();
21,727✔
246

247
        if (const auto it_builtin = getBuiltin(name))
43,454✔
248
            page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
2,396✔
249
        else if (getOperator(name).has_value())
19,331✔
250
            buildAndThrowError(fmt::format("Found a free standing operator: `{}`", name), x);
1✔
251
        else
252
        {
253
            const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
19,330✔
254
            if (maybe_local_idx.has_value())
19,330✔
255
                page(p).emplace_back(LOAD_SYMBOL_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
8,076✔
256
            else
257
                page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
11,254✔
258
        }
19,330✔
259

260
        if (is_result_unused)
21,726✔
261
        {
262
            warning("Statement has no effect", x);
×
263
            page(p).emplace_back(POP);
×
264
        }
×
265
    }
21,727✔
266

267
    void ASTLowerer::compileListInstruction(Node& x, const Page p, const bool is_result_unused)
2,811✔
268
    {
2,811✔
269
        const Node head = x.constList()[0];
2,811✔
270
        std::string name = x.constList()[0].string();
2,811✔
271
        Instruction inst = getListInstruction(name).value();
2,811✔
272

273
        // length of at least 1 since we got a symbol name
274
        const auto argc = x.constList().size() - 1u;
2,811✔
275
        // error, can not use append/concat/pop (and their in place versions) with a <2 length argument list
276
        if (argc < 2 && APPEND <= inst && inst <= POP)
2,811✔
277
            buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), head);
6✔
278
        if (inst <= POP && std::cmp_greater(argc, MaxValue16Bits))
2,805✔
279
            buildAndThrowError(fmt::format("Too many arguments ({}), exceeds {}", argc, MaxValue16Bits), x);
1✔
280
        if (argc != 3 && inst == SET_AT_INDEX)
2,804✔
281
            buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), head);
1✔
282
        if (argc != 4 && inst == SET_AT_2_INDEX)
2,803✔
283
            buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), head);
1✔
284

285
        // compile arguments in reverse order
286
        for (std::size_t i = x.constList().size() - 1u; i > 0; --i)
10,377✔
287
        {
288
            Node& node = x.list()[i];
7,575✔
289
            if (nodeProducesOutput(node))
7,575✔
290
                compileExpression(node, p, false, false);
7,574✔
291
            else
292
                buildAndThrowError(fmt::format("Invalid node inside call to {}", name), node);
1✔
293
        }
7,575✔
294

295
        // put inst and number of arguments
296
        std::size_t inst_argc = 0;
2,801✔
297
        switch (inst)
2,801✔
298
        {
2,072✔
299
            case LIST:
300
                inst_argc = argc;
2,072✔
301
                break;
2,762✔
302

303
            case APPEND:
304
            case APPEND_IN_PLACE:
305
            case CONCAT:
306
            case CONCAT_IN_PLACE:
307
                inst_argc = argc - 1;
690✔
308
                break;
708✔
309

310
            case POP_LIST:
311
            case POP_LIST_IN_PLACE:
312
                inst_argc = 0;
18✔
313
                break;
39✔
314

315
            default:
316
                break;
21✔
317
        }
2,801✔
318
        page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
2,801✔
319
        page(p).back().setSourceLocation(head.filename(), head.position().start.line);
2,801✔
320

321
        if (is_result_unused && name.back() != '!' && inst <= POP_LIST_IN_PLACE)  // in-place functions never push a value
2,801✔
322
        {
323
            warning("Ignoring return value of function", x);
×
324
            page(p).emplace_back(POP);
×
325
        }
×
326
    }
2,821✔
327

328
    void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
1,753✔
329
    {
1,753✔
330
        if (x.constList().size() == 1)
1,753✔
331
            buildAndThrowError("Invalid condition: missing 'cond' and 'then' nodes, expected (if cond then)", x);
2✔
332
        if (x.constList().size() == 2)
1,752✔
333
            buildAndThrowError(fmt::format("Invalid condition: missing 'then' node, expected (if {} then)", x.constList()[1].repr()), x);
1✔
334

335
        // compile condition
336
        compileExpression(x.list()[1], p, false, false);
1,751✔
337
        page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
1,751✔
338

339
        // jump only if needed to the "true" branch
340
        const auto label_then = IR::Entity::Label(m_current_label++);
1,751✔
341
        page(p).emplace_back(IR::Entity::GotoIf(label_then, true));
1,751✔
342

343
        // "false" branch code
344
        if (x.constList().size() == 4)  // we have an else clause
1,751✔
345
        {
346
            m_locals_locator.saveScopeLengthForBranch();
1,367✔
347
            compileExpression(x.list()[3], p, is_result_unused, is_terminal);
1,367✔
348
            page(p).back().setSourceLocation(x.constList()[3].filename(), x.constList()[3].position().start.line);
1,367✔
349
            m_locals_locator.dropVarsForBranch();
1,367✔
350
        }
1,367✔
351

352
        // when else is finished, jump to end
353
        const auto label_end = IR::Entity::Label(m_current_label++);
1,751✔
354
        page(p).emplace_back(IR::Entity::Goto(label_end));
1,751✔
355

356
        // absolute address to jump to if condition is true
357
        page(p).emplace_back(label_then);
1,751✔
358
        // if code
359
        m_locals_locator.saveScopeLengthForBranch();
1,751✔
360
        compileExpression(x.list()[2], p, is_result_unused, is_terminal);
1,751✔
361
        page(p).back().setSourceLocation(x.constList()[2].filename(), x.constList()[2].position().start.line);
1,751✔
362
        m_locals_locator.dropVarsForBranch();
1,751✔
363
        // set jump to end pos
364
        page(p).emplace_back(label_end);
1,751✔
365
    }
1,753✔
366

367
    void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused)
2,820✔
368
    {
2,820✔
369
        if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List)
2,822✔
370
            buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args);
1✔
371
        if (x.constList().size() != 3)
2,819✔
372
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
373

374
        // capture, if needed
375
        std::size_t capture_inst_count = 0;
2,818✔
376
        for (const auto& node : x.constList()[1].constList())
7,362✔
377
        {
378
            if (node.nodeType() == NodeType::Capture)
4,544✔
379
            {
380
                const uint16_t symbol_id = addSymbol(node);
227✔
381

382
                // We have an unqualified name that isn't the captured name
383
                // This means we need to rename the captured value
384
                if (const auto& maybe_nqn = node.getUnqualifiedName(); maybe_nqn.has_value() && maybe_nqn.value() != node.string())
454✔
385
                {
386
                    const uint16_t nqn_id = addSymbol(Node(NodeType::Symbol, maybe_nqn.value()));
14✔
387

388
                    page(p).emplace_back(RENAME_NEXT_CAPTURE, nqn_id);
14✔
389
                    page(p).emplace_back(CAPTURE, symbol_id);
14✔
390
                }
14✔
391
                else
392
                    page(p).emplace_back(CAPTURE, symbol_id);
213✔
393

394
                ++capture_inst_count;
227✔
395
            }
227✔
396
        }
4,544✔
397
        const bool is_closure = capture_inst_count > 0;
2,818✔
398

399
        m_locals_locator.createScope(
5,636✔
400
            is_closure
2,818✔
401
                ? LocalsLocator::ScopeType::Closure
402
                : LocalsLocator::ScopeType::Function);
403

404
        // create new page for function body
405
        m_code_pages.emplace_back();
2,818✔
406
        const auto function_body_page = Page { .index = m_code_pages.size() - 1, .is_temp = false };
2,818✔
407
        // save page_id into the constants table as PageAddr and load the const
408
        page(p).emplace_back(is_closure ? MAKE_CLOSURE : LOAD_CONST, addValue(function_body_page.index, x));
2,818✔
409

410
        // pushing arguments from the stack into variables in the new scope
411
        for (const auto& node : x.constList()[1].constList())
7,362✔
412
        {
413
            if (node.nodeType() == NodeType::Symbol)
4,544✔
414
            {
415
                page(function_body_page).emplace_back(STORE, addSymbol(node));
4,317✔
416
                m_locals_locator.addLocal(node.string());
4,317✔
417
            }
4,317✔
418
        }
4,544✔
419

420
        // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls.
421
        // This way we can continue to safely apply optimisations on
422
        // (let name (fun (e) (map lst (fun (e) (name e)))))
423
        // Otherwise, `name` would have been optimized to a GET_CURRENT_PAGE_ADDRESS, which would have returned the wrong page.
424
        if (x.isAnonymousFunction())
2,818✔
425
            m_opened_vars.push("#anonymous");
302✔
426
        // push body of the function
427
        compileExpression(x.list()[2], function_body_page, false, true);
2,818✔
428
        if (x.isAnonymousFunction())
2,818✔
429
            m_opened_vars.pop();
302✔
430

431
        // return last value on the stack
432
        page(function_body_page).emplace_back(RET);
2,818✔
433
        m_locals_locator.deleteScope();
2,818✔
434

435
        // if the computed function is unused, pop it
436
        if (is_result_unused)
2,818✔
437
        {
438
            warning("Unused declared function", x);
×
439
            page(p).emplace_back(POP);
×
440
        }
×
441
    }
2,820✔
442

443
    void ASTLowerer::compileLetMutSet(const Keyword n, Node& x, const Page p)
7,432✔
444
    {
7,432✔
445
        if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol)
7,439✔
446
            buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym);
×
447
        if (x.constList().size() != 3)
7,432✔
448
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
449

450
        const std::string name = x.constList()[1].string();
7,431✔
451
        uint16_t i = addSymbol(x.constList()[1]);
7,431✔
452

453
        if (!m_opened_vars.empty() && m_opened_vars.top() == name)
7,431✔
454
            buildAndThrowError("Can not define a variable using the same name as the function it is defined inside", x);
1✔
455

456
        const bool is_function = x.constList()[2].isFunction();
7,430✔
457
        if (is_function)
7,430✔
458
        {
459
            m_opened_vars.push(name);
2,518✔
460
            x.list()[2].setFunctionKind(/* anonymous= */ false);
2,518✔
461
        }
2,518✔
462

463
        // put value before symbol id
464
        // starting at index = 2 because x is a (let|mut|set variable ...) node
465
        for (std::size_t idx = 2, end = x.constList().size(); idx < end; ++idx)
14,860✔
466
            compileExpression(x.list()[idx], p, false, false);
7,430✔
467

468
        if (n == Keyword::Let || n == Keyword::Mut)
7,425✔
469
        {
470
            page(p).emplace_back(STORE, i);
5,706✔
471
            m_locals_locator.addLocal(name);
5,706✔
472
        }
5,706✔
473
        else
474
            page(p).emplace_back(SET_VAL, i);
1,719✔
475

476
        if (is_function)
7,425✔
477
            m_opened_vars.pop();
2,513✔
478
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
7,425✔
479
    }
7,433✔
480

481
    void ASTLowerer::compileWhile(Node& x, const Page p)
903✔
482
    {
903✔
483
        if (x.constList().size() != 3)
903✔
484
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
485

486
        m_locals_locator.createScope();
902✔
487
        page(p).emplace_back(CREATE_SCOPE);
902✔
488
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
902✔
489

490
        // save current position to jump there at the end of the loop
491
        const auto label_loop = IR::Entity::Label(m_current_label++);
902✔
492
        page(p).emplace_back(label_loop);
902✔
493
        // push condition
494
        compileExpression(x.list()[1], p, false, false);
902✔
495
        // absolute jump to end of block if condition is false
496
        const auto label_end = IR::Entity::Label(m_current_label++);
902✔
497
        page(p).emplace_back(IR::Entity::GotoIf(label_end, false));
902✔
498
        // push code to page
499
        compileExpression(x.list()[2], p, true, false);
902✔
500

501
        // reset the scope at the end of the loop so that indices are still valid
502
        // otherwise, (while true { (let a 5) (print a) (let b 6) (print b) })
503
        // would print 5, 6, then only 6 as we emit LOAD_SYMBOL_FROM_INDEX 0 and b is the last in the scope
504
        // loop, jump to the condition
505
        page(p).emplace_back(IR::Entity::Goto(label_loop, RESET_SCOPE_JUMP));
902✔
506

507
        // absolute address to jump to if condition is false
508
        page(p).emplace_back(label_end);
902✔
509

510
        page(p).emplace_back(POP_SCOPE);
902✔
511
        m_locals_locator.deleteScope();
902✔
512
    }
903✔
513

514
    void ASTLowerer::compilePluginImport(const Node& x, const Page p)
2✔
515
    {
2✔
516
        std::string path;
2✔
517
        const Node package_node = x.constList()[1];
2✔
518
        for (std::size_t i = 0, end = package_node.constList().size(); i < end; ++i)
4✔
519
        {
520
            path += package_node.constList()[i].string();
2✔
521
            if (i + 1 != end)
2✔
522
                path += "/";
×
523
        }
2✔
524
        path += ".arkm";
2✔
525

526
        // register plugin path in the constants table
527
        uint16_t id = addValue(Node(NodeType::String, path));
2✔
528
        // add plugin instruction + id of the constant referring to the plugin path
529
        page(p).emplace_back(PLUGIN, id);
2✔
530
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
531
    }
2✔
532

533
    void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call)
5,949✔
534
    {
5,949✔
535
        const auto node = call.constList()[0];
5,949✔
536

537
        // push the arguments in reverse order because the function loads its arguments in the order they are defined:
538
        // (fun (a b c) ...) -> load 'a', then 'b', then 'c'
539
        // We have to push arguments in this order and load them in reverse, because we are using references internally,
540
        // which can cause problems for recursive functions that swap their arguments around.
541
        // Eg (let foo (fun (a b c) (if (> a 0) (foo (- a 1) c (+ b c)) 1))) (foo 12 0 1)
542
        // 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
543
        // value for argument b, but loaded it as a reference.
544
        for (Node& value : std::ranges::drop_view(call.list(), 1) | std::views::reverse)
16,295✔
545
        {
546
            if (nodeProducesOutput(value))
10,346✔
547
                compileExpression(value, p, false, false);
10,344✔
548
            else
549
            {
550
                std::string message;
2✔
551
                if (is_tail_call)
2✔
552
                    message = fmt::format("Invalid node inside tail call to `{}'", node.repr());
1✔
553
                else
554
                    message = fmt::format("Invalid node inside call to `{}'", node.repr());
1✔
555
                buildAndThrowError(message, value);
2✔
556
            }
2✔
557
        }
10,346✔
558
    }
5,955✔
559

560
    void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal)
15,238✔
561
    {
15,238✔
562
        constexpr std::size_t start_index = 1;
15,238✔
563

564
        Node& node = x.list()[0];
15,238✔
565
        const std::optional<Instruction> maybe_operator = node.nodeType() == NodeType::Symbol ? getOperator(node.string()) : std::nullopt;
15,238✔
566

567
        const std::optional<Instruction> maybe_shortcircuit =
15,238✔
568
            node.nodeType() == NodeType::Symbol
30,003✔
569
            ? (node.string() == Language::And
29,333✔
570
                   ? std::make_optional(Instruction::SHORTCIRCUIT_AND)
197✔
571
                   : (node.string() == Language::Or
14,568✔
572
                          ? std::make_optional(Instruction::SHORTCIRCUIT_OR)
106✔
573
                          : std::nullopt))
14,462✔
574
            : std::nullopt;
473✔
575

576
        if (maybe_shortcircuit.has_value())
15,238✔
577
        {
578
            // short circuit implementation
579
            if (x.constList().size() < 3)
303✔
580
                buildAndThrowError(
2✔
581
                    fmt::format(
4✔
582
                        "Expected at least 2 arguments while compiling '{}', got {}",
2✔
583
                        node.string(),
2✔
584
                        x.constList().size() - 1),
2✔
585
                    x);
2✔
586

587
            compileExpression(x.list()[1], p, false, false);
301✔
588

589
            const auto label_shortcircuit = IR::Entity::Label(m_current_label++);
301✔
590
            auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, maybe_shortcircuit.value());
301✔
591
            page(p).emplace_back(shortcircuit_entity);
301✔
592

593
            for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
313✔
594
            {
595
                compileExpression(x.list()[i], p, false, false);
12✔
596
                if (i + 1 != end)
12✔
597
                    page(p).emplace_back(shortcircuit_entity);
12✔
598
            }
12✔
599

600
            page(p).emplace_back(label_shortcircuit);
301✔
601
        }
301✔
602
        else if (!maybe_operator.has_value())
14,935✔
603
        {
604
            if (is_terminal && node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string())
5,952✔
605
            {
606
                pushFunctionCallArguments(x, p, /* is_tail_call= */ true);
84✔
607

608
                // jump to the top of the function
609
                page(p).emplace_back(JUMP, 0_u16);
84✔
610
                page(p).back().setSourceLocation(node.filename(), node.position().start.line);
84✔
611
                return;  // skip the potential Instruction::POP at the end
84✔
612
            }
613
            else
614
            {
615
                if (!nodeProducesOutput(node))
5,868✔
616
                    buildAndThrowError(fmt::format("Can not call `{}', as it doesn't return a value", node.repr()), node);
2✔
617

618
                m_temp_pages.emplace_back();
5,866✔
619
                const auto proc_page = Page { .index = m_temp_pages.size() - 1u, .is_temp = true };
5,866✔
620

621
                // compile the function resolution to a separate page
622
                if (node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string())
5,866✔
623
                {
624
                    // The function is trying to call itself, but this isn't a tail call.
625
                    // We can skip the LOAD_SYMBOL function_name and directly push the current
626
                    // function page, which will be quicker than a local variable resolution.
627
                    // We set its argument to the symbol id of the function we are calling,
628
                    // so that the VM knows the name of the last called function.
629
                    page(proc_page).emplace_back(GET_CURRENT_PAGE_ADDR, addSymbol(node));
11✔
630
                }
11✔
631
                else
632
                {
633
                    // closure chains have been handled (eg: closure.field.field.function)
634
                    compileExpression(node, proc_page, false, false);  // storing proc
5,855✔
635
                }
636

637
                if (m_temp_pages.back().empty())
5,866✔
638
                    buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x);
×
639

640
                const auto label_return = IR::Entity::Label(m_current_label++);
5,866✔
641
                page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
5,866✔
642

643
                pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
5,866✔
644
                // push proc from temp page
645
                for (const auto& inst : m_temp_pages.back())
12,212✔
646
                    page(p).push_back(inst);
6,351✔
647
                m_temp_pages.pop_back();
5,861✔
648

649
                // number of arguments
650
                std::size_t args_count = 0;
5,861✔
651
                for (auto it = x.constList().begin() + start_index, it_end = x.constList().end(); it != it_end; ++it)
15,978✔
652
                {
653
                    if (it->nodeType() != NodeType::Capture)
10,117✔
654
                        args_count++;
10,117✔
655
                }
10,117✔
656
                // call the procedure
657
                page(p).emplace_back(CALL, args_count);
5,861✔
658
                page(p).back().setSourceLocation(node.filename(), node.position().start.line);
5,861✔
659

660
                // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
661
                page(p).emplace_back(label_return);
5,861✔
662
            }
5,866✔
663
        }
5,861✔
664
        else  // operator
665
        {
666
            // retrieve operator
667
            auto op = maybe_operator.value();
8,983✔
668

669
            if (op == ASSERT)
8,983✔
670
                is_result_unused = false;
346✔
671

672
            // push arguments on current page
673
            std::size_t exp_count = 0;
8,983✔
674
            for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
25,229✔
675
            {
676
                if (nodeProducesOutput(x.constList()[index]))
16,246✔
677
                    compileExpression(x.list()[index], p, false, false);
16,245✔
678
                else
679
                    buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'", node.repr()), x.constList()[index]);
1✔
680

681
                if ((index + 1 < size && x.constList()[index + 1].nodeType() != NodeType::Capture) || index + 1 == size)
16,245✔
682
                    exp_count++;
16,245✔
683

684
                // in order to be able to handle things like (op A B C D...)
685
                // which should be transformed into A B op C op D op...
686
                if (exp_count >= 2 && !isTernaryInst(op))
16,245✔
687
                    page(p).emplace_back(op);
7,170✔
688
            }
16,245✔
689

690
            if (isUnaryInst(op))
8,982✔
691
            {
692
                if (exp_count != 1)
1,849✔
693
                    buildAndThrowError(fmt::format("Operator needs one argument, but was called with {}", exp_count), x.constList()[0]);
1✔
694
                page(p).emplace_back(op);
1,848✔
695
            }
1,848✔
696
            else if (isTernaryInst(op))
7,133✔
697
            {
698
                if (exp_count != 3)
48✔
699
                    buildAndThrowError(fmt::format("Operator needs three arguments, but was called with {}", exp_count), x.constList()[0]);
1✔
700
                page(p).emplace_back(op);
47✔
701
            }
47✔
702
            else if (exp_count <= 1)
7,085✔
703
                buildAndThrowError(fmt::format("Operator needs two arguments, but was called with {}", exp_count), x.constList()[0]);
2✔
704

705
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
8,978✔
706

707
            // need to check we didn't push the (op A B C D...) things for operators not supporting it
708
            if (exp_count > 2)
8,978✔
709
            {
710
                switch (op)
120✔
711
                {
111✔
712
                    // authorized instructions
713
                    case ADD: [[fallthrough]];
714
                    case SUB: [[fallthrough]];
715
                    case MUL: [[fallthrough]];
716
                    case DIV: [[fallthrough]];
717
                    case MOD: [[fallthrough]];
718
                    case AT_AT:
719
                        break;
120✔
720

721
                    default:
722
                        buildAndThrowError(
9✔
723
                            fmt::format(
18✔
724
                                "`{}' requires 2 arguments, but got {}.",
9✔
725
                                Language::operators[static_cast<std::size_t>(op - FIRST_OPERATOR)],
9✔
726
                                exp_count),
727
                            x);
9✔
728
                }
111✔
729
            }
111✔
730
        }
8,983✔
731

732
        if (is_result_unused)
15,131✔
733
            page(p).emplace_back(POP);
2,045✔
734
    }
15,259✔
735

736
    uint16_t ASTLowerer::addSymbol(const Node& sym)
24,731✔
737
    {
24,731✔
738
        // otherwise, add the symbol, and return its id in the table
739
        auto it = std::ranges::find(m_symbols, sym.string());
24,731✔
740
        if (it == m_symbols.end())
24,731✔
741
        {
742
            m_symbols.push_back(sym.string());
5,461✔
743
            it = m_symbols.begin() + static_cast<std::vector<std::string>::difference_type>(m_symbols.size() - 1);
5,461✔
744
        }
5,461✔
745

746
        const auto distance = std::distance(m_symbols.begin(), it);
24,731✔
747
        if (std::cmp_less(distance, MaxValue16Bits))
24,731✔
748
            return static_cast<uint16_t>(distance);
49,462✔
749
        buildAndThrowError(fmt::format("Too many symbols (exceeds {}), aborting compilation.", MaxValue16Bits), sym);
×
750
    }
24,731✔
751

752
    uint16_t ASTLowerer::addValue(const Node& x)
14,437✔
753
    {
14,437✔
754
        const ValTableElem v(x);
14,437✔
755
        auto it = std::ranges::find(m_values, v);
14,437✔
756
        if (it == m_values.end())
14,437✔
757
        {
758
            m_values.push_back(v);
2,878✔
759
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
2,878✔
760
        }
2,878✔
761

762
        const auto distance = std::distance(m_values.begin(), it);
14,437✔
763
        if (std::cmp_less(distance, MaxValue16Bits))
14,437✔
764
            return static_cast<uint16_t>(distance);
14,437✔
765
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), x);
×
766
    }
14,437✔
767

768
    uint16_t ASTLowerer::addValue(const std::size_t page_id, const Node& current)
2,818✔
769
    {
2,818✔
770
        const ValTableElem v(page_id);
2,818✔
771
        auto it = std::ranges::find(m_values, v);
2,818✔
772
        if (it == m_values.end())
2,818✔
773
        {
774
            m_values.push_back(v);
2,818✔
775
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
2,818✔
776
        }
2,818✔
777

778
        const auto distance = std::distance(m_values.begin(), it);
2,818✔
779
        if (std::cmp_less(distance, MaxValue16Bits))
2,818✔
780
            return static_cast<uint16_t>(distance);
2,818✔
781
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), current);
×
782
    }
2,818✔
783
}
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