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

ArkScript-lang / Ark / 18413665959

10 Oct 2025 05:22PM UTC coverage: 90.631% (-0.008%) from 90.639%
18413665959

push

github

SuperFola
fix(ast lowerer): set more source locations to be more accurate in errors

2 of 3 new or added lines in 1 file covered. (66.67%)

1 existing line in 1 file now uncovered.

7971 of 8795 relevant lines covered (90.63%)

157932.26 hits per line

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

94.02
/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
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
21,726✔
261

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

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

276
        // length of at least 1 since we got a symbol name
277
        const auto argc = x.constList().size() - 1u;
2,811✔
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,811✔
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,805✔
282
            buildAndThrowError(fmt::format("Too many arguments ({}), exceeds {}", argc, MaxValue16Bits), x);
1✔
283
        if (argc != 3 && inst == SET_AT_INDEX)
2,804✔
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,803✔
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,377✔
290
        {
291
            Node& node = x.list()[i];
7,575✔
292
            if (nodeProducesOutput(node))
7,575✔
293
                compileExpression(node, p, false, false);
7,574✔
294
            else
295
                buildAndThrowError(fmt::format("Invalid node inside call to {}", name), node);
1✔
296
        }
7,575✔
297

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

306
            case APPEND:
307
            case APPEND_IN_PLACE:
308
            case CONCAT:
309
            case CONCAT_IN_PLACE:
310
                inst_argc = argc - 1;
690✔
311
                break;
708✔
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,801✔
321
        page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
2,801✔
322
        page(p).back().setSourceLocation(head.filename(), head.position().start.line);
2,801✔
323

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

331
    void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
1,753✔
332
    {
1,753✔
333
        if (x.constList().size() == 1)
1,753✔
334
            buildAndThrowError("Invalid condition: missing 'cond' and 'then' nodes, expected (if cond then)", x);
2✔
335
        if (x.constList().size() == 2)
1,752✔
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,751✔
340
        page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
1,751✔
341

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

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

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

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

370
    void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused)
2,820✔
371
    {
2,820✔
372
        if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List)
2,822✔
373
            buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args);
1✔
374
        if (x.constList().size() != 3)
2,819✔
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;
2,818✔
379
        for (const auto& node : x.constList()[1].constList())
7,362✔
380
        {
381
            if (node.nodeType() == NodeType::Capture)
4,544✔
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
        }
4,544✔
400
        const bool is_closure = capture_inst_count > 0;
2,818✔
401

402
        m_locals_locator.createScope(
5,636✔
403
            is_closure
2,818✔
404
                ? LocalsLocator::ScopeType::Closure
405
                : LocalsLocator::ScopeType::Function);
406

407
        // create new page for function body
408
        m_code_pages.emplace_back();
2,818✔
409
        const auto function_body_page = Page { .index = m_code_pages.size() - 1, .is_temp = false };
2,818✔
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));
2,818✔
412

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

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

434
        // return last value on the stack
435
        page(function_body_page).emplace_back(RET);
2,818✔
436
        m_locals_locator.deleteScope();
2,818✔
437

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

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

453
        const std::string name = x.constList()[1].string();
7,431✔
454
        uint16_t i = addSymbol(x.constList()[1]);
7,431✔
455

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

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

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

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

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

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

489
        m_locals_locator.createScope();
902✔
490
        page(p).emplace_back(CREATE_SCOPE);
902✔
491
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
902✔
492

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

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

510
        // absolute address to jump to if condition is false
511
        page(p).emplace_back(label_end);
902✔
512

513
        page(p).emplace_back(POP_SCOPE);
902✔
514
        m_locals_locator.deleteScope();
902✔
515
    }
903✔
516

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

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

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

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

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

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

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

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

590
            compileExpression(x.list()[1], p, false, false);
301✔
591

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

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

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

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

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

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

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

643
                const auto label_return = IR::Entity::Label(m_current_label++);
5,866✔
644
                page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
5,866✔
645
                page(p).back().setSourceLocation(x.filename(), x.position().start.line);
5,866✔
646

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

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

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

673
            if (op == ASSERT)
8,983✔
674
                is_result_unused = false;
346✔
675

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

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

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

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

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

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

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

736
        if (is_result_unused)
15,131✔
737
            page(p).emplace_back(POP);
2,045✔
738
    }
15,259✔
739

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

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

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

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

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

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