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

ArkScript-lang / Ark / 20853932363

09 Jan 2026 01:46PM UTC coverage: 92.742% (+0.007%) from 92.735%
20853932363

push

github

SuperFola
chore(tests): test tail calls to ensure arguments are correct when swapping them around between calls

8497 of 9162 relevant lines covered (92.74%)

281117.87 hits per line

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

94.13
/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) :
358✔
19
        m_logger("ASTLowerer", debug)
358✔
20
    {}
716✔
21

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

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

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

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

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

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

59
    std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
33,832✔
60
    {
33,832✔
61
        const auto it = std::ranges::find_if(Builtins::builtins,
33,832✔
62
                                             [&name](const std::pair<std::string, Value>& element) -> bool {
2,162,462✔
63
                                                 return name == element.first;
2,128,630✔
64
                                             });
65
        if (it != Builtins::builtins.end())
33,832✔
66
            return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
2,839✔
67
        return std::nullopt;
30,993✔
68
    }
33,832✔
69

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

78
    bool ASTLowerer::nodeProducesOutput(const Node& node)
52,993✔
79
    {
52,993✔
80
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
52,993✔
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())) ||
866✔
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 ||
529✔
85
                // a condition produces a value if all its branches produce a value
86
                (node.constList()[0].keyword() == Keyword::If &&
96✔
87
                 nodeProducesOutput(node.constList()[2]) &&
90✔
88
                 (node.constList().size() == 3 || nodeProducesOutput(node.constList()[3])));
90✔
89
        // in place list instruction, as well as assert, do not produce values
358✔
90
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
52,560✔
91
            return std::ranges::find(Language::UpdateRef, node.constList().front().string()) == Language::UpdateRef.end() &&
16,150✔
92
                node.constList().front().string() != "assert";
8,075✔
93
        return true;  // any other node, function call, symbol, number...
44,485✔
94
    }
52,993✔
95

96
    bool ASTLowerer::isUnaryInst(const Instruction inst) noexcept
13,367✔
97
    {
13,367✔
98
        switch (inst)
13,367✔
99
        {
2,605✔
100
            case NOT: [[fallthrough]];
101
            case LEN: [[fallthrough]];
102
            case IS_EMPTY: [[fallthrough]];
103
            case TAIL: [[fallthrough]];
104
            case HEAD: [[fallthrough]];
105
            case IS_NIL: [[fallthrough]];
106
            case TO_NUM: [[fallthrough]];
107
            case TO_STR: [[fallthrough]];
108
            case TYPE:
109
                return true;
13,367✔
110

111
            default:
112
                return false;
10,762✔
113
        }
114
    }
13,367✔
115

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

123
            default:
124
                return false;
21,542✔
125
        }
126
    }
21,691✔
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)
99,137✔
139
    {
99,137✔
140
        // register symbols
141
        if (x.nodeType() == NodeType::Symbol)
99,137✔
142
            compileSymbol(x, p, is_result_unused, /* can_use_ref= */ true);
32,039✔
143
        else if (x.nodeType() == NodeType::Field)
67,098✔
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, /* can_use_ref= */ true);
1,708✔
147
            for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
3,422✔
148
            {
149
                uint16_t i = addSymbol(*it);
1,714✔
150
                page(p).emplace_back(GET_FIELD, i);
1,714✔
151
            }
1,714✔
152
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,708✔
153
        }
1,708✔
154
        // register values
155
        else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
65,390✔
156
        {
157
            uint16_t i = addValue(x);
15,990✔
158

159
            if (!is_result_unused)
15,990✔
160
                page(p).emplace_back(LOAD_CONST, i);
15,990✔
161
        }
15,990✔
162
        // namespace nodes
163
        else if (x.nodeType() == NodeType::Namespace)
49,400✔
164
            compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
135✔
165
        else if (x.nodeType() == NodeType::Unused)
49,265✔
166
        {
167
            // do nothing, explicitly
168
        }
5✔
169
        // empty code block should be nil
170
        else if (x.constList().empty())
49,260✔
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())
98,520✔
180
            compileListInstruction(x, p, is_result_unused);
3,216✔
181
        // registering structures
182
        else if (x.constList()[0].nodeType() == NodeType::Keyword)
46,044✔
183
        {
184
            switch (const Keyword keyword = x.constList()[0].keyword())
24,601✔
185
            {
2,742✔
186
                case Keyword::If:
187
                    compileIf(x, p, is_result_unused, is_terminal);
2,742✔
188
                    break;
14,782✔
189

190
                case Keyword::Set:
191
                    [[fallthrough]];
192
                case Keyword::Let:
193
                    [[fallthrough]];
194
                case Keyword::Mut:
195
                    compileLetMutSet(keyword, x, p);
12,042✔
196
                    break;
15,808✔
197

198
                case Keyword::Fun:
199
                    compileFunction(x, p, is_result_unused);
3,773✔
200
                    break;
8,584✔
201

202
                case Keyword::Begin:
203
                {
204
                    for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
24,108✔
205
                        compileExpression(
38,584✔
206
                            x.list()[i],
19,292✔
207
                            p,
19,292✔
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,
19,292✔
210
                            // If the 'begin' is a terminal node, only its last node is terminal.
211
                            is_terminal && (i == size - 1));
19,292✔
212
                    break;
4,776✔
213
                }
1,224✔
214

215
                case Keyword::While:
216
                    compileWhile(x, p);
1,224✔
217
                    break;
1,225✔
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
            }
24,601✔
228
        }
24,546✔
229
        else if (x.nodeType() == NodeType::List)
21,443✔
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);
21,443✔
234
        }
21,417✔
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
    }
99,137✔
242

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

247
        if (const auto it_builtin = getBuiltin(name))
67,664✔
248
            page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
2,839✔
249
        else if (getOperator(name).has_value())
30,993✔
250
            buildAndThrowError(fmt::format("Found a free standing operator: `{}`", name), x);
1✔
251
        else
252
        {
253
            if (can_use_ref)
30,992✔
254
            {
255
                const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
30,907✔
256
                if (maybe_local_idx.has_value())
30,907✔
257
                    page(p).emplace_back(LOAD_FAST_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
12,036✔
258
                else
259
                    page(p).emplace_back(LOAD_FAST, addSymbol(x));
18,871✔
260
            }
30,907✔
261
            else
262
                page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
85✔
263
        }
264

265
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
33,831✔
266

267
        if (is_result_unused)
33,831✔
268
        {
269
            warning("Statement has no effect", x);
×
270
            page(p).emplace_back(POP);
×
271
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
×
272
        }
×
273
    }
33,832✔
274

275
    void ASTLowerer::compileListInstruction(Node& x, const Page p, const bool is_result_unused)
3,216✔
276
    {
3,216✔
277
        const Node head = x.constList()[0];
3,216✔
278
        std::string name = x.constList()[0].string();
3,216✔
279
        Instruction inst = getListInstruction(name).value();
3,216✔
280

281
        // length of at least 1 since we got a symbol name
282
        const auto argc = x.constList().size() - 1u;
3,216✔
283
        // error, can not use append/concat/pop (and their in place versions) with a <2 length argument list
284
        if (argc < 2 && APPEND <= inst && inst <= POP)
3,216✔
285
            buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), head);
6✔
286
        if (inst <= POP && std::cmp_greater(argc, MaxValue16Bits))
3,210✔
287
            buildAndThrowError(fmt::format("Too many arguments ({}), exceeds {}", argc, MaxValue16Bits), x);
1✔
288
        if (argc != 3 && inst == SET_AT_INDEX)
3,209✔
289
            buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), head);
1✔
290
        if (argc != 4 && inst == SET_AT_2_INDEX)
3,208✔
291
            buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), head);
1✔
292

293
        // compile arguments in reverse order
294
        for (std::size_t i = x.constList().size() - 1u; i > 0; --i)
11,108✔
295
        {
296
            Node& node = x.list()[i];
7,901✔
297
            if (nodeProducesOutput(node))
7,901✔
298
                compileExpression(node, p, false, false);
7,900✔
299
            else
300
                buildAndThrowError(fmt::format("Invalid node inside call to {}", name), node);
1✔
301
        }
7,901✔
302

303
        // put inst and number of arguments
304
        std::size_t inst_argc = 0;
3,206✔
305
        switch (inst)
3,206✔
306
        {
2,291✔
307
            case LIST:
308
                inst_argc = argc;
2,291✔
309
                break;
3,101✔
310

311
            case APPEND:
312
            case APPEND_IN_PLACE:
313
            case CONCAT:
314
            case CONCAT_IN_PLACE:
315
                inst_argc = argc - 1;
810✔
316
                break;
825✔
317

318
            case POP_LIST:
319
            case POP_LIST_IN_PLACE:
320
                inst_argc = 0;
15✔
321
                break;
105✔
322

323
            default:
324
                break;
90✔
325
        }
3,206✔
326
        page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
3,206✔
327
        page(p).back().setSourceLocation(head.filename(), head.position().start.line);
3,206✔
328

329
        if (is_result_unused && name.back() != '!' && inst <= POP_LIST_IN_PLACE)  // in-place functions never push a value
3,206✔
330
        {
331
            warning("Ignoring return value of function", x);
×
332
            page(p).emplace_back(POP);
×
333
        }
×
334
    }
3,226✔
335

336
    void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
2,742✔
337
    {
2,742✔
338
        if (x.constList().size() == 1)
2,742✔
339
            buildAndThrowError("Invalid condition: missing 'cond' and 'then' nodes, expected (if cond then)", x);
2✔
340
        if (x.constList().size() == 2)
2,741✔
341
            buildAndThrowError(fmt::format("Invalid condition: missing 'then' node, expected (if {} then)", x.constList()[1].repr()), x);
1✔
342

343
        // compile condition
344
        compileExpression(x.list()[1], p, false, false);
2,740✔
345
        page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
2,740✔
346

347
        // jump only if needed to the "true" branch
348
        const auto label_then = IR::Entity::Label(m_current_label++);
2,740✔
349
        page(p).emplace_back(IR::Entity::GotoIf(label_then, true));
2,740✔
350

351
        // "false" branch code
352
        if (x.constList().size() == 4)  // we have an else clause
2,740✔
353
        {
354
            m_locals_locator.saveScopeLengthForBranch();
2,028✔
355
            compileExpression(x.list()[3], p, is_result_unused, is_terminal);
2,028✔
356
            page(p).back().setSourceLocation(x.constList()[3].filename(), x.constList()[3].position().start.line);
2,028✔
357
            m_locals_locator.dropVarsForBranch();
2,028✔
358
        }
2,028✔
359

360
        // when else is finished, jump to end
361
        const auto label_end = IR::Entity::Label(m_current_label++);
2,740✔
362
        page(p).emplace_back(IR::Entity::Goto(label_end));
2,740✔
363

364
        // absolute address to jump to if condition is true
365
        page(p).emplace_back(label_then);
2,740✔
366
        // if code
367
        m_locals_locator.saveScopeLengthForBranch();
2,740✔
368
        compileExpression(x.list()[2], p, is_result_unused, is_terminal);
2,740✔
369
        page(p).back().setSourceLocation(x.constList()[2].filename(), x.constList()[2].position().start.line);
2,740✔
370
        m_locals_locator.dropVarsForBranch();
2,740✔
371
        // set jump to end pos
372
        page(p).emplace_back(label_end);
2,740✔
373
    }
2,742✔
374

375
    void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused)
3,773✔
376
    {
3,773✔
377
        if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List)
3,775✔
378
            buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args);
1✔
379
        if (x.constList().size() != 3)
3,772✔
380
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
381

382
        // capture, if needed
383
        std::size_t capture_inst_count = 0;
3,771✔
384
        for (const auto& node : x.constList()[1].constList())
9,806✔
385
        {
386
            if (node.nodeType() == NodeType::Capture)
6,035✔
387
            {
388
                const uint16_t symbol_id = addSymbol(node);
227✔
389

390
                // We have an unqualified name that isn't the captured name
391
                // This means we need to rename the captured value
392
                if (const auto& maybe_nqn = node.getUnqualifiedName(); maybe_nqn.has_value() && maybe_nqn.value() != node.string())
454✔
393
                {
394
                    const uint16_t nqn_id = addSymbol(Node(NodeType::Symbol, maybe_nqn.value()));
14✔
395

396
                    page(p).emplace_back(RENAME_NEXT_CAPTURE, nqn_id);
14✔
397
                    page(p).emplace_back(CAPTURE, symbol_id);
14✔
398
                }
14✔
399
                else
400
                    page(p).emplace_back(CAPTURE, symbol_id);
213✔
401

402
                ++capture_inst_count;
227✔
403
            }
227✔
404
        }
6,035✔
405
        const bool is_closure = capture_inst_count > 0;
3,771✔
406

407
        m_locals_locator.createScope(
7,542✔
408
            is_closure
3,771✔
409
                ? LocalsLocator::ScopeType::Closure
410
                : LocalsLocator::ScopeType::Function);
411

412
        // create new page for function body
413
        m_code_pages.emplace_back();
3,771✔
414
        const auto function_body_page = Page { .index = m_code_pages.size() - 1, .is_temp = false };
3,771✔
415
        // save page_id into the constants table as PageAddr and load the const
416
        page(p).emplace_back(is_closure ? MAKE_CLOSURE : LOAD_CONST, addValue(function_body_page.index, x));
3,771✔
417

418
        // pushing arguments from the stack into variables in the new scope
419
        for (const auto& node : x.constList()[1].constList())
9,806✔
420
        {
421
            if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg)
6,035✔
422
            {
423
                page(function_body_page).emplace_back(STORE, addSymbol(node));
4,955✔
424
                m_locals_locator.addLocal(node.string());
4,955✔
425
            }
4,955✔
426
            else if (node.nodeType() == NodeType::RefArg)
1,080✔
427
            {
428
                page(function_body_page).emplace_back(STORE_REF, addSymbol(node));
853✔
429
                m_locals_locator.addLocal(node.string());
853✔
430
            }
853✔
431
        }
6,035✔
432

433
        // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls.
434
        // This way we can continue to safely apply optimisations on
435
        // (let name (fun (e) (map lst (fun (e) (name e)))))
436
        // Otherwise, `name` would have been optimized to a GET_CURRENT_PAGE_ADDRESS, which would have returned the wrong page.
437
        if (x.isAnonymousFunction())
3,771✔
438
            m_opened_vars.emplace("#anonymous");
399✔
439
        // push body of the function
440
        compileExpression(x.list()[2], function_body_page, false, true);
3,771✔
441
        if (x.isAnonymousFunction())
3,771✔
442
            m_opened_vars.pop();
399✔
443

444
        // return last value on the stack
445
        page(function_body_page).emplace_back(RET);
3,771✔
446
        m_locals_locator.deleteScope();
3,771✔
447

448
        // if the computed function is unused, pop it
449
        if (is_result_unused)
3,771✔
450
        {
451
            warning("Unused declared function", x);
×
452
            page(p).emplace_back(POP);
×
453
        }
×
454
    }
3,773✔
455

456
    void ASTLowerer::compileLetMutSet(const Keyword n, Node& x, const Page p)
12,042✔
457
    {
12,042✔
458
        if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol)
12,049✔
459
            buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym);
×
460
        if (x.constList().size() != 3)
12,042✔
461
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
462

463
        const std::string name = x.constList()[1].string();
12,041✔
464
        uint16_t i = addSymbol(x.constList()[1]);
12,041✔
465

466
        if (!m_opened_vars.empty() && m_opened_vars.top() == name)
12,041✔
467
            buildAndThrowError("Can not define a variable using the same name as the function it is defined inside", x);
1✔
468

469
        const bool is_function = x.constList()[2].isFunction();
12,040✔
470
        if (is_function)
12,040✔
471
        {
472
            m_opened_vars.push(name);
3,374✔
473
            x.list()[2].setFunctionKind(/* anonymous= */ false);
3,374✔
474
        }
3,374✔
475

476
        // put value before symbol id
477
        // starting at index = 2 because x is a (let|mut|set variable ...) node
478
        for (std::size_t idx = 2, end = x.constList().size(); idx < end; ++idx)
24,080✔
479
            compileExpression(x.list()[idx], p, false, false);
12,040✔
480

481
        if (n == Keyword::Let || n == Keyword::Mut)
12,035✔
482
        {
483
            page(p).emplace_back(STORE, i);
7,961✔
484
            m_locals_locator.addLocal(name);
7,961✔
485
        }
7,961✔
486
        else
487
            page(p).emplace_back(SET_VAL, i);
4,074✔
488

489
        if (is_function)
12,035✔
490
            m_opened_vars.pop();
3,369✔
491
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
12,035✔
492
    }
12,043✔
493

494
    void ASTLowerer::compileWhile(Node& x, const Page p)
1,224✔
495
    {
1,224✔
496
        if (x.constList().size() != 3)
1,224✔
497
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
498

499
        m_locals_locator.createScope();
1,223✔
500
        page(p).emplace_back(CREATE_SCOPE);
1,223✔
501
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,223✔
502

503
        // save current position to jump there at the end of the loop
504
        const auto label_loop = IR::Entity::Label(m_current_label++);
1,223✔
505
        page(p).emplace_back(label_loop);
1,223✔
506
        // push condition
507
        compileExpression(x.list()[1], p, false, false);
1,223✔
508
        // absolute jump to end of block if condition is false
509
        const auto label_end = IR::Entity::Label(m_current_label++);
1,223✔
510
        page(p).emplace_back(IR::Entity::GotoIf(label_end, false));
1,223✔
511
        // push code to page
512
        compileExpression(x.list()[2], p, true, false);
1,223✔
513

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

520
        // absolute address to jump to if condition is false
521
        page(p).emplace_back(label_end);
1,223✔
522

523
        page(p).emplace_back(POP_SCOPE);
1,223✔
524
        m_locals_locator.deleteScope();
1,223✔
525
    }
1,224✔
526

527
    void ASTLowerer::compilePluginImport(const Node& x, const Page p)
2✔
528
    {
2✔
529
        std::string path;
2✔
530
        const Node package_node = x.constList()[1];
2✔
531
        for (std::size_t i = 0, end = package_node.constList().size(); i < end; ++i)
4✔
532
        {
533
            path += package_node.constList()[i].string();
2✔
534
            if (i + 1 != end)
2✔
535
                path += "/";
×
536
        }
2✔
537
        path += ".arkm";
2✔
538

539
        // register plugin path in the constants table
540
        uint16_t id = addValue(Node(NodeType::String, path));
2✔
541
        // add plugin instruction + id of the constant referring to the plugin path
542
        page(p).emplace_back(PLUGIN, id);
2✔
543
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
544
    }
2✔
545

546
    void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call)
7,614✔
547
    {
7,614✔
548
        const auto node = call.constList()[0];
7,614✔
549

550
        // push the arguments in reverse order because the function loads its arguments in the order they are defined:
551
        // (fun (a b c) ...) -> load 'a', then 'b', then 'c'
552
        // We have to push arguments in this order and load them in reverse, because we are using references internally,
553
        // which can cause problems for recursive functions that swap their arguments around.
554
        // Eg (let foo (fun (a b c) (if (> a 0) (foo (- a 1) c (+ b c)) 1))) (foo 12 0 1)
555
        // 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
556
        // value for argument b, but loaded it as a reference.
557
        for (Node& value : std::ranges::drop_view(call.list(), 1) | std::views::reverse)
20,726✔
558
        {
559
            if (nodeProducesOutput(value))
13,112✔
560
            {
561
                // 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
562
                if (value.nodeType() == NodeType::Symbol && is_tail_call)
13,110✔
563
                    compileSymbol(value, p, false, /* can_use_ref= */ false);
85✔
564
                else
565
                    compileExpression(value, p, false, false);
13,025✔
566
            }
13,106✔
567
            else
568
            {
569
                std::string message;
2✔
570
                if (is_tail_call)
2✔
571
                    message = fmt::format("Invalid node inside tail call to `{}'", node.repr());
1✔
572
                else
573
                    message = fmt::format("Invalid node inside call to `{}'", node.repr());
1✔
574
                buildAndThrowError(message, value);
2✔
575
            }
2✔
576
        }
13,112✔
577
    }
7,620✔
578

579
    void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal)
21,442✔
580
    {
21,442✔
581
        constexpr std::size_t start_index = 1;
21,442✔
582

583
        Node& node = x.list()[0];
21,442✔
584
        const std::optional<Instruction> maybe_operator = node.nodeType() == NodeType::Symbol ? getOperator(node.string()) : std::nullopt;
21,442✔
585

586
        const std::optional<Instruction> maybe_shortcircuit =
21,442✔
587
            node.nodeType() == NodeType::Symbol
42,209✔
588
            ? (node.string() == Language::And
41,192✔
589
                   ? std::make_optional(Instruction::SHORTCIRCUIT_AND)
342✔
590
                   : (node.string() == Language::Or
20,425✔
591
                          ? std::make_optional(Instruction::SHORTCIRCUIT_OR)
115✔
592
                          : std::nullopt))
20,310✔
593
            : std::nullopt;
675✔
594

595
        if (maybe_shortcircuit.has_value())
21,442✔
596
        {
597
            // short circuit implementation
598
            if (x.constList().size() < 3)
457✔
599
                buildAndThrowError(
2✔
600
                    fmt::format(
4✔
601
                        "Expected at least 2 arguments while compiling '{}', got {}",
2✔
602
                        node.string(),
2✔
603
                        x.constList().size() - 1),
2✔
604
                    x);
2✔
605

606
            compileExpression(x.list()[1], p, false, false);
455✔
607

608
            const auto label_shortcircuit = IR::Entity::Label(m_current_label++);
455✔
609
            auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, maybe_shortcircuit.value());
455✔
610
            page(p).emplace_back(shortcircuit_entity);
455✔
611

612
            for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
554✔
613
            {
614
                compileExpression(x.list()[i], p, false, false);
99✔
615
                if (i + 1 != end)
99✔
616
                    page(p).emplace_back(shortcircuit_entity);
99✔
617
            }
99✔
618

619
            page(p).emplace_back(label_shortcircuit);
455✔
620
        }
455✔
621
        else if (!maybe_operator.has_value())
20,985✔
622
        {
623
            if (is_terminal && node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string())
7,617✔
624
            {
625
                pushFunctionCallArguments(x, p, /* is_tail_call= */ true);
112✔
626

627
                // jump to the top of the function
628
                page(p).emplace_back(JUMP, 0_u16);
112✔
629
                page(p).back().setSourceLocation(node.filename(), node.position().start.line);
112✔
630
                return;  // skip the potential Instruction::POP at the end
112✔
631
            }
632
            else
633
            {
634
                if (!nodeProducesOutput(node))
7,505✔
635
                    buildAndThrowError(fmt::format("Can not call `{}', as it doesn't return a value", node.repr()), node);
2✔
636

637
                m_temp_pages.emplace_back();
7,503✔
638
                const auto proc_page = Page { .index = m_temp_pages.size() - 1u, .is_temp = true };
7,503✔
639

640
                // compile the function resolution to a separate page
641
                if (node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string())
7,503✔
642
                {
643
                    // The function is trying to call itself, but this isn't a tail call.
644
                    // We can skip the LOAD_FAST function_name and directly push the current
645
                    // function page, which will be quicker than a local variable resolution.
646
                    // We set its argument to the symbol id of the function we are calling,
647
                    // so that the VM knows the name of the last called function.
648
                    page(proc_page).emplace_back(GET_CURRENT_PAGE_ADDR, addSymbol(node));
57✔
649
                }
57✔
650
                else
651
                {
652
                    // closure chains have been handled (eg: closure.field.field.function)
653
                    compileExpression(node, proc_page, false, false);  // storing proc
7,446✔
654
                }
655

656
                if (m_temp_pages.back().empty())
7,503✔
657
                    buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x);
×
658

659
                const auto label_return = IR::Entity::Label(m_current_label++);
7,503✔
660
                page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
7,503✔
661
                page(p).back().setSourceLocation(x.filename(), x.position().start.line);
7,503✔
662

663
                pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
7,503✔
664
                // push proc from temp page
665
                for (const auto& inst : m_temp_pages.back())
15,676✔
666
                    page(p).push_back(inst);
8,178✔
667
                m_temp_pages.pop_back();
7,498✔
668

669
                // number of arguments
670
                std::size_t args_count = 0;
7,498✔
671
                for (auto it = x.constList().begin() + start_index, it_end = x.constList().end(); it != it_end; ++it)
20,325✔
672
                {
673
                    if (it->nodeType() != NodeType::Capture)
12,827✔
674
                        args_count++;
12,827✔
675
                }
12,827✔
676
                // call the procedure
677
                page(p).emplace_back(CALL, args_count);
7,498✔
678
                page(p).back().setSourceLocation(node.filename(), node.position().start.line);
7,498✔
679

680
                // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
681
                page(p).emplace_back(label_return);
7,498✔
682
            }
7,503✔
683
        }
7,498✔
684
        else  // operator
685
        {
686
            // retrieve operator
687
            auto op = maybe_operator.value();
13,368✔
688

689
            if (op == ASSERT)
13,368✔
690
                is_result_unused = false;
395✔
691

692
            // push arguments on current page
693
            std::size_t exp_count = 0;
13,368✔
694
            for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
37,663✔
695
            {
696
                if (nodeProducesOutput(x.constList()[index]))
24,295✔
697
                    compileExpression(x.list()[index], p, false, false);
24,294✔
698
                else
699
                    buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'", node.repr()), x.constList()[index]);
1✔
700

701
                if ((index + 1 < size && x.constList()[index + 1].nodeType() != NodeType::Capture) || index + 1 == size)
24,294✔
702
                    exp_count++;
24,294✔
703

704
                // in order to be able to handle things like (op A B C D...)
705
                // which should be transformed into A B op C op D op...
706
                if (exp_count >= 2 && !isTernaryInst(op))
24,294✔
707
                    page(p).emplace_back(op);
10,830✔
708
            }
24,294✔
709

710
            if (isUnaryInst(op))
13,367✔
711
            {
712
                if (exp_count != 1)
2,605✔
713
                    buildAndThrowError(fmt::format("Operator needs one argument, but was called with {}", exp_count), x.constList()[0]);
1✔
714
                page(p).emplace_back(op);
2,604✔
715
            }
2,604✔
716
            else if (isTernaryInst(op))
10,762✔
717
            {
718
                if (exp_count != 3)
50✔
719
                    buildAndThrowError(fmt::format("Operator needs three arguments, but was called with {}", exp_count), x.constList()[0]);
1✔
720
                page(p).emplace_back(op);
49✔
721
            }
49✔
722
            else if (exp_count <= 1)
10,712✔
723
                buildAndThrowError(fmt::format("Operator needs two arguments, but was called with {}", exp_count), x.constList()[0]);
2✔
724

725
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
13,363✔
726

727
            // need to check we didn't push the (op A B C D...) things for operators not supporting it
728
            if (exp_count > 2)
13,363✔
729
            {
730
                switch (op)
155✔
731
                {
146✔
732
                    // authorized instructions
733
                    case ADD: [[fallthrough]];
734
                    case SUB: [[fallthrough]];
735
                    case MUL: [[fallthrough]];
736
                    case DIV: [[fallthrough]];
737
                    case MOD: [[fallthrough]];
738
                    case AT_AT:
739
                        break;
155✔
740

741
                    default:
742
                        buildAndThrowError(
9✔
743
                            fmt::format(
18✔
744
                                "`{}' requires 2 arguments, but got {}.",
9✔
745
                                Language::operators[static_cast<std::size_t>(op - FIRST_OPERATOR)],
9✔
746
                                exp_count),
747
                            x);
9✔
748
                }
146✔
749
            }
146✔
750
        }
13,368✔
751

752
        if (is_result_unused)
21,307✔
753
            page(p).emplace_back(POP);
2,651✔
754
    }
21,463✔
755

756
    uint16_t ASTLowerer::addSymbol(const Node& sym)
38,819✔
757
    {
38,819✔
758
        // otherwise, add the symbol, and return its id in the table
759
        auto it = std::ranges::find(m_symbols, sym.string());
38,819✔
760
        if (it == m_symbols.end())
38,819✔
761
        {
762
            m_symbols.push_back(sym.string());
7,228✔
763
            it = m_symbols.begin() + static_cast<std::vector<std::string>::difference_type>(m_symbols.size() - 1);
7,228✔
764
        }
7,228✔
765

766
        const auto distance = std::distance(m_symbols.begin(), it);
38,819✔
767
        if (std::cmp_less(distance, MaxValue16Bits))
38,819✔
768
            return static_cast<uint16_t>(distance);
77,638✔
769
        buildAndThrowError(fmt::format("Too many symbols (exceeds {}), aborting compilation.", MaxValue16Bits), sym);
×
770
    }
38,819✔
771

772
    uint16_t ASTLowerer::addValue(const Node& x)
15,992✔
773
    {
15,992✔
774
        const ValTableElem v(x);
15,992✔
775
        auto it = std::ranges::find(m_values, v);
15,992✔
776
        if (it == m_values.end())
15,992✔
777
        {
778
            m_values.push_back(v);
3,454✔
779
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,454✔
780
        }
3,454✔
781

782
        const auto distance = std::distance(m_values.begin(), it);
15,992✔
783
        if (std::cmp_less(distance, MaxValue16Bits))
15,992✔
784
            return static_cast<uint16_t>(distance);
15,992✔
785
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), x);
×
786
    }
15,992✔
787

788
    uint16_t ASTLowerer::addValue(const std::size_t page_id, const Node& current)
3,771✔
789
    {
3,771✔
790
        const ValTableElem v(page_id);
3,771✔
791
        auto it = std::ranges::find(m_values, v);
3,771✔
792
        if (it == m_values.end())
3,771✔
793
        {
794
            m_values.push_back(v);
3,771✔
795
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,771✔
796
        }
3,771✔
797

798
        const auto distance = std::distance(m_values.begin(), it);
3,771✔
799
        if (std::cmp_less(distance, MaxValue16Bits))
3,771✔
800
            return static_cast<uint16_t>(distance);
3,771✔
801
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), current);
×
802
    }
3,771✔
803
}
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