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

ArkScript-lang / Ark / 20065764942

09 Dec 2025 01:49PM UTC coverage: 90.564% (+0.008%) from 90.556%
20065764942

push

github

SuperFola
chore(tests): adding IR generation tests for arg attributes

8043 of 8881 relevant lines covered (90.56%)

180911.91 hits per line

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

94.07
/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) :
345✔
19
        m_logger("ASTLowerer", debug)
345✔
20
    {}
690✔
21

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

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

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

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

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

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

59
    std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
24,030✔
60
    {
24,030✔
61
        const auto it = std::ranges::find_if(Builtins::builtins,
24,030✔
62
                                             [&name](const std::pair<std::string, Value>& element) -> bool {
1,512,910✔
63
                                                 return name == element.first;
1,488,880✔
64
                                             });
65
        if (it != Builtins::builtins.end())
24,030✔
66
            return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
2,536✔
67
        return std::nullopt;
21,494✔
68
    }
24,030✔
69

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

78
    bool ASTLowerer::nodeProducesOutput(const Node& node)
43,802✔
79
    {
43,802✔
80
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
43,802✔
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())) ||
706✔
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 ||
403✔
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
345✔
90
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
43,449✔
91
            return std::ranges::find(Language::UpdateRef, node.constList().front().string()) == Language::UpdateRef.end() &&
14,776✔
92
                node.constList().front().string() != "assert";
7,388✔
93
        return true;  // any other node, function call, symbol, number...
36,061✔
94
    }
43,802✔
95

96
    bool ASTLowerer::isUnaryInst(const Instruction inst) noexcept
9,960✔
97
    {
9,960✔
98
        switch (inst)
9,960✔
99
        {
2,070✔
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;
9,960✔
110

111
            default:
112
                return false;
7,890✔
113
        }
114
    }
9,960✔
115

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

123
            default:
124
                return false;
15,769✔
125
        }
126
    }
15,912✔
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)
76,574✔
139
    {
76,574✔
140
        // register symbols
141
        if (x.nodeType() == NodeType::Symbol)
76,574✔
142
            compileSymbol(x, p, is_result_unused);
22,543✔
143
        else if (x.nodeType() == NodeType::Field)
54,031✔
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,487✔
147
            for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
2,982✔
148
            {
149
                uint16_t i = addSymbol(*it);
1,495✔
150
                page(p).emplace_back(GET_FIELD, i);
1,495✔
151
            }
1,495✔
152
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,487✔
153
        }
1,487✔
154
        // register values
155
        else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
52,544✔
156
        {
157
            uint16_t i = addValue(x);
15,363✔
158

159
            if (!is_result_unused)
15,363✔
160
                page(p).emplace_back(LOAD_CONST, i);
15,363✔
161
        }
15,363✔
162
        // namespace nodes
163
        else if (x.nodeType() == NodeType::Namespace)
37,181✔
164
            compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
134✔
165
        else if (x.nodeType() == NodeType::Unused)
37,047✔
166
        {
167
            // do nothing, explicitly
168
        }
5✔
169
        // empty code block should be nil
170
        else if (x.constList().empty())
37,042✔
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())
74,084✔
180
            compileListInstruction(x, p, is_result_unused);
2,974✔
181
        // registering structures
182
        else if (x.constList()[0].nodeType() == NodeType::Keyword)
34,068✔
183
        {
184
            switch (const Keyword keyword = x.constList()[0].keyword())
17,156✔
185
            {
1,933✔
186
                case Keyword::If:
187
                    compileIf(x, p, is_result_unused, is_terminal);
1,933✔
188
                    break;
10,088✔
189

190
                case Keyword::Set:
191
                    [[fallthrough]];
192
                case Keyword::Let:
193
                    [[fallthrough]];
194
                case Keyword::Mut:
195
                    compileLetMutSet(keyword, x, p);
8,157✔
196
                    break;
11,301✔
197

198
                case Keyword::Fun:
199
                    compileFunction(x, p, is_result_unused);
3,151✔
200
                    break;
6,092✔
201

202
                case Keyword::Begin:
203
                {
204
                    for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
16,026✔
205
                        compileExpression(
26,160✔
206
                            x.list()[i],
13,080✔
207
                            p,
13,080✔
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,
13,080✔
210
                            // If the begin is a terminal node, only its last node is terminal.
211
                            is_terminal && (i == size - 1));
13,080✔
212
                    break;
2,906✔
213
                }
965✔
214

215
                case Keyword::While:
216
                    compileWhile(x, p);
965✔
217
                    break;
966✔
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
            }
17,156✔
228
        }
17,101✔
229
        else if (x.nodeType() == NodeType::List)
16,912✔
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);
16,912✔
234
        }
16,886✔
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
    }
76,574✔
242

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

247
        if (const auto it_builtin = getBuiltin(name))
48,060✔
248
            page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
2,536✔
249
        else if (getOperator(name).has_value())
21,494✔
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);
21,493✔
254
            if (maybe_local_idx.has_value())
21,493✔
255
                page(p).emplace_back(LOAD_SYMBOL_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
9,211✔
256
            else
257
                page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
12,282✔
258
        }
21,493✔
259

260
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
24,029✔
261

262
        if (is_result_unused)
24,029✔
263
        {
264
            warning("Statement has no effect", x);
×
265
            page(p).emplace_back(POP);
×
266
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
×
267
        }
×
268
    }
24,030✔
269

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

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

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

298
        // put inst and number of arguments
299
        std::size_t inst_argc = 0;
2,964✔
300
        switch (inst)
2,964✔
301
        {
2,212✔
302
            case LIST:
303
                inst_argc = argc;
2,212✔
304
                break;
2,925✔
305

306
            case APPEND:
307
            case APPEND_IN_PLACE:
308
            case CONCAT:
309
            case CONCAT_IN_PLACE:
310
                inst_argc = argc - 1;
713✔
311
                break;
731✔
312

313
            case POP_LIST:
314
            case POP_LIST_IN_PLACE:
315
                inst_argc = 0;
18✔
316
                break;
39✔
317

318
            default:
319
                break;
21✔
320
        }
2,964✔
321
        page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
2,964✔
322
        page(p).back().setSourceLocation(head.filename(), head.position().start.line);
2,964✔
323

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

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

338
        // compile condition
339
        compileExpression(x.list()[1], p, false, false);
1,931✔
340
        page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
1,931✔
341

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

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

355
        // when else is finished, jump to end
356
        const auto label_end = IR::Entity::Label(m_current_label++);
1,931✔
357
        page(p).emplace_back(IR::Entity::Goto(label_end));
1,931✔
358

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

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

377
        // capture, if needed
378
        std::size_t capture_inst_count = 0;
3,149✔
379
        for (const auto& node : x.constList()[1].constList())
8,193✔
380
        {
381
            if (node.nodeType() == NodeType::Capture)
5,044✔
382
            {
383
                const uint16_t symbol_id = addSymbol(node);
227✔
384

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

391
                    page(p).emplace_back(RENAME_NEXT_CAPTURE, nqn_id);
14✔
392
                    page(p).emplace_back(CAPTURE, symbol_id);
14✔
393
                }
14✔
394
                else
395
                    page(p).emplace_back(CAPTURE, symbol_id);
213✔
396

397
                ++capture_inst_count;
227✔
398
            }
227✔
399
        }
5,044✔
400
        const bool is_closure = capture_inst_count > 0;
3,149✔
401

402
        m_locals_locator.createScope(
6,298✔
403
            is_closure
3,149✔
404
                ? LocalsLocator::ScopeType::Closure
405
                : LocalsLocator::ScopeType::Function);
406

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

413
        // pushing arguments from the stack into variables in the new scope
414
        for (const auto& node : x.constList()[1].constList())
8,193✔
415
        {
416
            if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg)
5,044✔
417
            {
418
                page(function_body_page).emplace_back(STORE, addSymbol(node));
4,149✔
419
                m_locals_locator.addLocal(node.string());
4,149✔
420
            }
4,149✔
421
            else if (node.nodeType() == NodeType::RefArg)
895✔
422
            {
423
                page(function_body_page).emplace_back(STORE_REF, addSymbol(node));
668✔
424
                m_locals_locator.addLocal(node.string());
668✔
425
            }
668✔
426
        }
5,044✔
427

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

439
        // return last value on the stack
440
        page(function_body_page).emplace_back(RET);
3,149✔
441
        m_locals_locator.deleteScope();
3,149✔
442

443
        // if the computed function is unused, pop it
444
        if (is_result_unused)
3,149✔
445
        {
446
            warning("Unused declared function", x);
×
447
            page(p).emplace_back(POP);
×
448
        }
×
449
    }
3,151✔
450

451
    void ASTLowerer::compileLetMutSet(const Keyword n, Node& x, const Page p)
8,157✔
452
    {
8,157✔
453
        if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol)
8,164✔
454
            buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym);
×
455
        if (x.constList().size() != 3)
8,157✔
456
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
457

458
        const std::string name = x.constList()[1].string();
8,156✔
459
        uint16_t i = addSymbol(x.constList()[1]);
8,156✔
460

461
        if (!m_opened_vars.empty() && m_opened_vars.top() == name)
8,156✔
462
            buildAndThrowError("Can not define a variable using the same name as the function it is defined inside", x);
1✔
463

464
        const bool is_function = x.constList()[2].isFunction();
8,155✔
465
        if (is_function)
8,155✔
466
        {
467
            m_opened_vars.push(name);
2,786✔
468
            x.list()[2].setFunctionKind(/* anonymous= */ false);
2,786✔
469
        }
2,786✔
470

471
        // put value before symbol id
472
        // starting at index = 2 because x is a (let|mut|set variable ...) node
473
        for (std::size_t idx = 2, end = x.constList().size(); idx < end; ++idx)
16,310✔
474
            compileExpression(x.list()[idx], p, false, false);
8,155✔
475

476
        if (n == Keyword::Let || n == Keyword::Mut)
8,150✔
477
        {
478
            page(p).emplace_back(STORE, i);
6,315✔
479
            m_locals_locator.addLocal(name);
6,315✔
480
        }
6,315✔
481
        else
482
            page(p).emplace_back(SET_VAL, i);
1,835✔
483

484
        if (is_function)
8,150✔
485
            m_opened_vars.pop();
2,781✔
486
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
8,150✔
487
    }
8,158✔
488

489
    void ASTLowerer::compileWhile(Node& x, const Page p)
965✔
490
    {
965✔
491
        if (x.constList().size() != 3)
965✔
492
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
493

494
        m_locals_locator.createScope();
964✔
495
        page(p).emplace_back(CREATE_SCOPE);
964✔
496
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
964✔
497

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

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

515
        // absolute address to jump to if condition is false
516
        page(p).emplace_back(label_end);
964✔
517

518
        page(p).emplace_back(POP_SCOPE);
964✔
519
        m_locals_locator.deleteScope();
964✔
520
    }
965✔
521

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

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

541
    void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call)
6,579✔
542
    {
6,579✔
543
        const auto node = call.constList()[0];
6,579✔
544

545
        // push the arguments in reverse order because the function loads its arguments in the order they are defined:
546
        // (fun (a b c) ...) -> load 'a', then 'b', then 'c'
547
        // We have to push arguments in this order and load them in reverse, because we are using references internally,
548
        // which can cause problems for recursive functions that swap their arguments around.
549
        // Eg (let foo (fun (a b c) (if (> a 0) (foo (- a 1) c (+ b c)) 1))) (foo 12 0 1)
550
        // 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
551
        // value for argument b, but loaded it as a reference.
552
        for (Node& value : std::ranges::drop_view(call.list(), 1) | std::views::reverse)
18,086✔
553
        {
554
            if (nodeProducesOutput(value))
11,507✔
555
                compileExpression(value, p, false, false);
11,505✔
556
            else
557
            {
558
                std::string message;
2✔
559
                if (is_tail_call)
2✔
560
                    message = fmt::format("Invalid node inside tail call to `{}'", node.repr());
1✔
561
                else
562
                    message = fmt::format("Invalid node inside call to `{}'", node.repr());
1✔
563
                buildAndThrowError(message, value);
2✔
564
            }
2✔
565
        }
11,507✔
566
    }
6,585✔
567

568
    void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal)
16,911✔
569
    {
16,911✔
570
        constexpr std::size_t start_index = 1;
16,911✔
571

572
        Node& node = x.list()[0];
16,911✔
573
        const std::optional<Instruction> maybe_operator = node.nodeType() == NodeType::Symbol ? getOperator(node.string()) : std::nullopt;
16,911✔
574

575
        const std::optional<Instruction> maybe_shortcircuit =
16,911✔
576
            node.nodeType() == NodeType::Symbol
33,334✔
577
            ? (node.string() == Language::And
32,597✔
578
                   ? std::make_optional(Instruction::SHORTCIRCUIT_AND)
249✔
579
                   : (node.string() == Language::Or
16,174✔
580
                          ? std::make_optional(Instruction::SHORTCIRCUIT_OR)
119✔
581
                          : std::nullopt))
16,055✔
582
            : std::nullopt;
488✔
583

584
        if (maybe_shortcircuit.has_value())
16,911✔
585
        {
586
            // short circuit implementation
587
            if (x.constList().size() < 3)
368✔
588
                buildAndThrowError(
2✔
589
                    fmt::format(
4✔
590
                        "Expected at least 2 arguments while compiling '{}', got {}",
2✔
591
                        node.string(),
2✔
592
                        x.constList().size() - 1),
2✔
593
                    x);
2✔
594

595
            compileExpression(x.list()[1], p, false, false);
366✔
596

597
            const auto label_shortcircuit = IR::Entity::Label(m_current_label++);
366✔
598
            auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, maybe_shortcircuit.value());
366✔
599
            page(p).emplace_back(shortcircuit_entity);
366✔
600

601
            for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
469✔
602
            {
603
                compileExpression(x.list()[i], p, false, false);
103✔
604
                if (i + 1 != end)
103✔
605
                    page(p).emplace_back(shortcircuit_entity);
103✔
606
            }
103✔
607

608
            page(p).emplace_back(label_shortcircuit);
366✔
609
        }
366✔
610
        else if (!maybe_operator.has_value())
16,543✔
611
        {
612
            if (is_terminal && node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string())
6,582✔
613
            {
614
                pushFunctionCallArguments(x, p, /* is_tail_call= */ true);
84✔
615

616
                // jump to the top of the function
617
                page(p).emplace_back(JUMP, 0_u16);
84✔
618
                page(p).back().setSourceLocation(node.filename(), node.position().start.line);
84✔
619
                return;  // skip the potential Instruction::POP at the end
84✔
620
            }
621
            else
622
            {
623
                if (!nodeProducesOutput(node))
6,498✔
624
                    buildAndThrowError(fmt::format("Can not call `{}', as it doesn't return a value", node.repr()), node);
2✔
625

626
                m_temp_pages.emplace_back();
6,496✔
627
                const auto proc_page = Page { .index = m_temp_pages.size() - 1u, .is_temp = true };
6,496✔
628

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

645
                if (m_temp_pages.back().empty())
6,496✔
646
                    buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x);
×
647

648
                const auto label_return = IR::Entity::Label(m_current_label++);
6,496✔
649
                page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
6,496✔
650
                page(p).back().setSourceLocation(x.filename(), x.position().start.line);
6,496✔
651

652
                pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
6,496✔
653
                // push proc from temp page
654
                for (const auto& inst : m_temp_pages.back())
13,487✔
655
                    page(p).push_back(inst);
6,996✔
656
                m_temp_pages.pop_back();
6,491✔
657

658
                // number of arguments
659
                std::size_t args_count = 0;
6,491✔
660
                for (auto it = x.constList().begin() + start_index, it_end = x.constList().end(); it != it_end; ++it)
17,769✔
661
                {
662
                    if (it->nodeType() != NodeType::Capture)
11,278✔
663
                        args_count++;
11,278✔
664
                }
11,278✔
665
                // call the procedure
666
                page(p).emplace_back(CALL, args_count);
6,491✔
667
                page(p).back().setSourceLocation(node.filename(), node.position().start.line);
6,491✔
668

669
                // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
670
                page(p).emplace_back(label_return);
6,491✔
671
            }
6,496✔
672
        }
6,491✔
673
        else  // operator
674
        {
675
            // retrieve operator
676
            auto op = maybe_operator.value();
9,961✔
677

678
            if (op == ASSERT)
9,961✔
679
                is_result_unused = false;
346✔
680

681
            // push arguments on current page
682
            std::size_t exp_count = 0;
9,961✔
683
            for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
27,942✔
684
            {
685
                if (nodeProducesOutput(x.constList()[index]))
17,981✔
686
                    compileExpression(x.list()[index], p, false, false);
17,980✔
687
                else
688
                    buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'", node.repr()), x.constList()[index]);
1✔
689

690
                if ((index + 1 < size && x.constList()[index + 1].nodeType() != NodeType::Capture) || index + 1 == size)
17,980✔
691
                    exp_count++;
17,980✔
692

693
                // in order to be able to handle things like (op A B C D...)
694
                // which should be transformed into A B op C op D op...
695
                if (exp_count >= 2 && !isTernaryInst(op))
17,980✔
696
                    page(p).emplace_back(op);
7,927✔
697
            }
17,980✔
698

699
            if (isUnaryInst(op))
9,960✔
700
            {
701
                if (exp_count != 1)
2,070✔
702
                    buildAndThrowError(fmt::format("Operator needs one argument, but was called with {}", exp_count), x.constList()[0]);
1✔
703
                page(p).emplace_back(op);
2,069✔
704
            }
2,069✔
705
            else if (isTernaryInst(op))
7,890✔
706
            {
707
                if (exp_count != 3)
48✔
708
                    buildAndThrowError(fmt::format("Operator needs three arguments, but was called with {}", exp_count), x.constList()[0]);
1✔
709
                page(p).emplace_back(op);
47✔
710
            }
47✔
711
            else if (exp_count <= 1)
7,842✔
712
                buildAndThrowError(fmt::format("Operator needs two arguments, but was called with {}", exp_count), x.constList()[0]);
2✔
713

714
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
9,956✔
715

716
            // need to check we didn't push the (op A B C D...) things for operators not supporting it
717
            if (exp_count > 2)
9,956✔
718
            {
719
                switch (op)
120✔
720
                {
111✔
721
                    // authorized instructions
722
                    case ADD: [[fallthrough]];
723
                    case SUB: [[fallthrough]];
724
                    case MUL: [[fallthrough]];
725
                    case DIV: [[fallthrough]];
726
                    case MOD: [[fallthrough]];
727
                    case AT_AT:
728
                        break;
120✔
729

730
                    default:
731
                        buildAndThrowError(
9✔
732
                            fmt::format(
18✔
733
                                "`{}' requires 2 arguments, but got {}.",
9✔
734
                                Language::operators[static_cast<std::size_t>(op - FIRST_OPERATOR)],
9✔
735
                                exp_count),
736
                            x);
9✔
737
                }
111✔
738
            }
111✔
739
        }
9,961✔
740

741
        if (is_result_unused)
16,804✔
742
            page(p).emplace_back(POP);
2,207✔
743
    }
16,932✔
744

745
    uint16_t ASTLowerer::addSymbol(const Node& sym)
27,050✔
746
    {
27,050✔
747
        // otherwise, add the symbol, and return its id in the table
748
        auto it = std::ranges::find(m_symbols, sym.string());
27,050✔
749
        if (it == m_symbols.end())
27,050✔
750
        {
751
            m_symbols.push_back(sym.string());
6,039✔
752
            it = m_symbols.begin() + static_cast<std::vector<std::string>::difference_type>(m_symbols.size() - 1);
6,039✔
753
        }
6,039✔
754

755
        const auto distance = std::distance(m_symbols.begin(), it);
27,050✔
756
        if (std::cmp_less(distance, MaxValue16Bits))
27,050✔
757
            return static_cast<uint16_t>(distance);
54,100✔
758
        buildAndThrowError(fmt::format("Too many symbols (exceeds {}), aborting compilation.", MaxValue16Bits), sym);
×
759
    }
27,050✔
760

761
    uint16_t ASTLowerer::addValue(const Node& x)
15,365✔
762
    {
15,365✔
763
        const ValTableElem v(x);
15,365✔
764
        auto it = std::ranges::find(m_values, v);
15,365✔
765
        if (it == m_values.end())
15,365✔
766
        {
767
            m_values.push_back(v);
3,035✔
768
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,035✔
769
        }
3,035✔
770

771
        const auto distance = std::distance(m_values.begin(), it);
15,365✔
772
        if (std::cmp_less(distance, MaxValue16Bits))
15,365✔
773
            return static_cast<uint16_t>(distance);
15,365✔
774
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), x);
×
775
    }
15,365✔
776

777
    uint16_t ASTLowerer::addValue(const std::size_t page_id, const Node& current)
3,149✔
778
    {
3,149✔
779
        const ValTableElem v(page_id);
3,149✔
780
        auto it = std::ranges::find(m_values, v);
3,149✔
781
        if (it == m_values.end())
3,149✔
782
        {
783
            m_values.push_back(v);
3,149✔
784
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,149✔
785
        }
3,149✔
786

787
        const auto distance = std::distance(m_values.begin(), it);
3,149✔
788
        if (std::cmp_less(distance, MaxValue16Bits))
3,149✔
789
            return static_cast<uint16_t>(distance);
3,149✔
790
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), current);
×
791
    }
3,149✔
792
}
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