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

ArkScript-lang / Ark / 21146409233

19 Jan 2026 05:26PM UTC coverage: 93.417% (+0.7%) from 92.743%
21146409233

push

github

SuperFola
feat(tests, debugger): testing the debugger triggering on errors

5 of 5 new or added lines in 2 files covered. (100.0%)

125 existing lines in 4 files now uncovered.

8813 of 9434 relevant lines covered (93.42%)

273848.42 hits per line

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

94.3
/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) :
382✔
19
        m_logger("ASTLowerer", debug)
382✔
20
    {}
764✔
21

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

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

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

38
        // gather symbols, values, and start to create code segments
39
        compileExpression(
295✔
40
            ast,
295✔
41
            /* current_page */ global,
295✔
42
            /* is_result_unused */ false,
43
            /* is_terminal */ false);
44
        m_logger.traceEnd();
295✔
45
    }
295✔
46

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

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

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

62
    std::optional<Instruction> ASTLowerer::getOperator(const std::string& name) noexcept
51,985✔
63
    {
51,985✔
64
        const auto it = std::ranges::find(Language::operators, name);
51,985✔
65
        if (it != Language::operators.end())
51,985✔
66
            return static_cast<Instruction>(std::distance(Language::operators.begin(), it) + FIRST_OPERATOR);
13,049✔
67
        return std::nullopt;
38,936✔
68
    }
51,985✔
69

70
    std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
34,354✔
71
    {
34,354✔
72
        const auto it = std::ranges::find_if(Builtins::builtins,
34,354✔
73
                                             [&name](const std::pair<std::string, Value>& element) -> bool {
2,214,136✔
74
                                                 return name == element.first;
2,179,782✔
75
                                             });
76
        if (it != Builtins::builtins.end())
34,354✔
77
            return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
3,229✔
78
        return std::nullopt;
31,125✔
79
    }
34,354✔
80

81
    std::optional<Instruction> ASTLowerer::getListInstruction(const std::string& name) noexcept
27,294✔
82
    {
27,294✔
83
        const auto it = std::ranges::find(Language::listInstructions, name);
27,294✔
84
        if (it != Language::listInstructions.end())
27,294✔
85
            return static_cast<Instruction>(std::distance(Language::listInstructions.begin(), it) + LIST);
6,434✔
86
        return std::nullopt;
20,860✔
87
    }
27,294✔
88

89
    bool ASTLowerer::isBreakpoint(const Node& node)
50,297✔
90
    {
50,297✔
91
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
50,297✔
92
            return node.constList().front().string() == "breakpoint";
20,101✔
93
        return false;
30,196✔
94
    }
50,297✔
95

96
    bool ASTLowerer::nodeProducesOutput(const Node& node)
53,480✔
97
    {
53,480✔
98
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
53,480✔
99
            // a begin node produces a value if the last node in it produces a value
100
            return (node.constList()[0].keyword() == Keyword::Begin && node.constList().size() > 1 && nodeProducesOutput(node.constList().back())) ||
868✔
101
                // a function always produces a value ; even if it ends with a node not producing one, the VM returns nil
102
                node.constList()[0].keyword() == Keyword::Fun ||
530✔
103
                // a condition produces a value if all its branches produce a value
104
                (node.constList()[0].keyword() == Keyword::If &&
96✔
105
                 nodeProducesOutput(node.constList()[2]) &&
472✔
106
                 (node.constList().size() == 3 || nodeProducesOutput(node.constList()[3])));
90✔
107
        // in place list instruction, as well as breakpoint, do not produce values
108
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
53,428✔
109
            return std::ranges::find(Language::UpdateRef, node.constList().front().string()) == Language::UpdateRef.end() &&
16,142✔
110
                node.constList().front().string() != "breakpoint";
8,071✔
111
        return true;  // any other node, function call, symbol, number...
44,975✔
112
    }
53,480✔
113

114
    bool ASTLowerer::isUnaryInst(const Instruction inst) noexcept
13,032✔
115
    {
13,032✔
116
        switch (inst)
13,032✔
117
        {
2,632✔
118
            case NOT: [[fallthrough]];
119
            case LEN: [[fallthrough]];
120
            case IS_EMPTY: [[fallthrough]];
121
            case TAIL: [[fallthrough]];
122
            case HEAD: [[fallthrough]];
123
            case IS_NIL: [[fallthrough]];
124
            case TO_NUM: [[fallthrough]];
125
            case TO_STR: [[fallthrough]];
126
            case TYPE:
127
                return true;
13,032✔
128

129
            default:
130
                return false;
10,400✔
131
        }
132
    }
13,032✔
133

134
    bool ASTLowerer::isTernaryInst(const Instruction inst) noexcept
20,972✔
135
    {
20,972✔
136
        switch (inst)
20,972✔
137
        {
149✔
138
            case AT_AT:
139
                return true;
20,972✔
140

141
            default:
142
                return false;
20,823✔
143
        }
144
    }
20,972✔
145

UNCOV
146
    void ASTLowerer::warning(const std::string& message, const Node& node)
×
UNCOV
147
    {
×
UNCOV
148
        fmt::println("{} {}", fmt::styled("Warning", fmt::fg(fmt::color::dark_orange)), Diagnostics::makeContextWithNode(message, node));
×
UNCOV
149
    }
×
150

151
    void ASTLowerer::buildAndThrowError(const std::string& message, const Node& node)
38✔
152
    {
38✔
153
        throw CodeError(message, CodeErrorContext(node.filename(), node.position()));
38✔
154
    }
38✔
155

156
    void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
99,806✔
157
    {
99,806✔
158
        // register symbols
159
        if (x.nodeType() == NodeType::Symbol)
99,806✔
160
            compileSymbol(x, p, is_result_unused, /* can_use_ref= */ true);
32,557✔
161
        else if (x.nodeType() == NodeType::Field)
67,249✔
162
        {
163
            // the parser guarantees us that there is at least 2 elements (eg: a.b)
164
            compileSymbol(x.list()[0], p, is_result_unused, /* can_use_ref= */ true);
1,712✔
165
            for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
3,430✔
166
            {
167
                uint16_t i = addSymbol(*it);
1,718✔
168
                page(p).emplace_back(GET_FIELD, i);
1,718✔
169
            }
1,718✔
170
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,712✔
171
        }
1,712✔
172
        // register values
173
        else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
65,537✔
174
        {
175
            uint16_t i = addValue(x);
15,977✔
176

177
            if (!is_result_unused)
15,977✔
178
                page(p).emplace_back(LOAD_CONST, i);
15,977✔
179
        }
15,977✔
180
        // namespace nodes
181
        else if (x.nodeType() == NodeType::Namespace)
49,560✔
182
            compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
135✔
183
        else if (x.nodeType() == NodeType::Unused)
49,425✔
184
        {
185
            // do nothing, explicitly
186
        }
5✔
187
        // empty code block should be nil
188
        else if (x.constList().empty())
49,420✔
189
        {
UNCOV
190
            if (!is_result_unused)
×
191
            {
192
                static const std::optional<uint16_t> nil = getBuiltin("nil");
91✔
UNCOV
193
                page(p).emplace_back(BUILTIN, nil.value());
×
UNCOV
194
            }
×
UNCOV
195
        }
×
196
        // list instructions
197
        else if (const auto head = x.constList()[0]; head.nodeType() == NodeType::Symbol && getListInstruction(head.string()).has_value())
98,840✔
198
            compileListInstruction(x, p, is_result_unused);
3,217✔
199
        // registering structures
200
        else if (x.constList()[0].nodeType() == NodeType::Keyword)
46,203✔
201
        {
202
            switch (const Keyword keyword = x.constList()[0].keyword())
24,663✔
203
            {
2,744✔
204
                case Keyword::If:
205
                    compileIf(x, p, is_result_unused, is_terminal);
2,744✔
206
                    break;
14,789✔
207

208
                case Keyword::Set:
209
                    [[fallthrough]];
210
                case Keyword::Let:
211
                    [[fallthrough]];
212
                case Keyword::Mut:
213
                    compileLetMutSet(keyword, x, p);
12,047✔
214
                    break;
15,820✔
215

216
                case Keyword::Fun:
217
                    compileFunction(x, p, is_result_unused);
3,780✔
218
                    break;
8,637✔
219

220
                case Keyword::Begin:
221
                {
222
                    for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
24,239✔
223
                        compileExpression(
38,754✔
224
                            x.list()[i],
19,377✔
225
                            p,
19,377✔
226
                            // All the nodes in a 'begin' (except for the last one) are producing a result that we want to drop.
227
                            (i != size - 1) || is_result_unused,
19,377✔
228
                            // If the 'begin' is a terminal node, only its last node is terminal.
229
                            is_terminal && (i == size - 1));
19,377✔
230
                    break;
4,822✔
231
                }
1,226✔
232

233
                case Keyword::While:
234
                    compileWhile(x, p);
1,226✔
235
                    break;
1,227✔
236

237
                case Keyword::Import:
238
                    compilePluginImport(x, p);
2✔
239
                    break;
4✔
240

241
                case Keyword::Del:
242
                    page(p).emplace_back(DEL, addSymbol(x.constList()[1]));
2✔
243
                    page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
244
                    break;
2✔
245
            }
24,663✔
246
        }
24,608✔
247
        else if (x.nodeType() == NodeType::List)
21,540✔
248
        {
249
            // If we are here, we should have a function name via the m_opened_vars.
250
            // Push arguments first, then function name, then call it.
251
            handleCalls(x, p, is_result_unused, is_terminal);
21,540✔
252
        }
21,514✔
253
        else
UNCOV
254
            buildAndThrowError(
×
UNCOV
255
                fmt::format(
×
UNCOV
256
                    "NodeType `{}' not handled in ASTLowerer::compileExpression. Please fill an issue on GitHub: https://github.com/ArkScript-lang/Ark",
×
UNCOV
257
                    typeToString(x)),
×
UNCOV
258
                x);
×
259
    }
99,806✔
260

261
    void ASTLowerer::compileSymbol(const Node& x, const Page p, const bool is_result_unused, const bool can_use_ref)
34,354✔
262
    {
34,354✔
263
        const std::string& name = x.string();
34,354✔
264

265
        if (const auto it_builtin = getBuiltin(name))
68,708✔
266
            page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
3,229✔
267
        else if (getOperator(name).has_value())
31,125✔
268
            buildAndThrowError(fmt::format("Found a free standing operator: `{}`", name), x);
1✔
269
        else
270
        {
271
            if (can_use_ref)
31,124✔
272
            {
273
                const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
31,039✔
274
                if (maybe_local_idx.has_value())
31,039✔
275
                    page(p).emplace_back(LOAD_FAST_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
12,126✔
276
                else
277
                    page(p).emplace_back(LOAD_FAST, addSymbol(x));
18,913✔
278
            }
31,039✔
279
            else
280
                page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
85✔
281
        }
282

283
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
34,353✔
284

285
        if (is_result_unused)
34,353✔
286
        {
UNCOV
287
            warning("Statement has no effect", x);
×
UNCOV
288
            page(p).emplace_back(POP);
×
UNCOV
289
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
×
UNCOV
290
        }
×
291
    }
34,354✔
292

293
    void ASTLowerer::compileListInstruction(Node& x, const Page p, const bool is_result_unused)
3,217✔
294
    {
3,217✔
295
        const Node head = x.constList()[0];
3,217✔
296
        std::string name = x.constList()[0].string();
3,217✔
297
        Instruction inst = getListInstruction(name).value();
3,217✔
298

299
        // length of at least 1 since we got a symbol name
300
        const auto argc = x.constList().size() - 1u;
3,217✔
301
        // error, can not use append/concat/pop (and their in place versions) with a <2 length argument list
302
        if (argc < 2 && APPEND <= inst && inst <= POP)
3,217✔
303
            buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), head);
6✔
304
        if (inst <= POP && std::cmp_greater(argc, MaxValue16Bits))
3,211✔
305
            buildAndThrowError(fmt::format("Too many arguments ({}), exceeds {}", argc, MaxValue16Bits), x);
1✔
306
        if (argc != 3 && inst == SET_AT_INDEX)
3,210✔
307
            buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), head);
1✔
308
        if (argc != 4 && inst == SET_AT_2_INDEX)
3,209✔
309
            buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), head);
1✔
310

311
        // compile arguments in reverse order
312
        for (std::size_t i = x.constList().size() - 1u; i > 0; --i)
11,083✔
313
        {
314
            Node& node = x.list()[i];
7,875✔
315
            if (nodeProducesOutput(node))
7,875✔
316
                compileExpression(node, p, false, false);
7,874✔
317
            else
318
                buildAndThrowError(fmt::format("Invalid node inside call to {}", name), node);
1✔
319
        }
7,875✔
320

321
        // put inst and number of arguments
322
        std::size_t inst_argc = 0;
3,207✔
323
        switch (inst)
3,207✔
324
        {
2,292✔
325
            case LIST:
326
                inst_argc = argc;
2,292✔
327
                break;
3,102✔
328

329
            case APPEND:
330
            case APPEND_IN_PLACE:
331
            case CONCAT:
332
            case CONCAT_IN_PLACE:
333
                inst_argc = argc - 1;
810✔
334
                break;
825✔
335

336
            case POP_LIST:
337
            case POP_LIST_IN_PLACE:
338
                inst_argc = 0;
15✔
339
                break;
105✔
340

341
            default:
342
                break;
90✔
343
        }
3,207✔
344
        page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
3,207✔
345
        page(p).back().setSourceLocation(head.filename(), head.position().start.line);
3,207✔
346

347
        if (is_result_unused && name.back() != '!' && inst <= POP_LIST_IN_PLACE)  // in-place functions never push a value
3,207✔
348
        {
UNCOV
349
            warning("Ignoring return value of function", x);
×
UNCOV
350
            page(p).emplace_back(POP);
×
UNCOV
351
        }
×
352
    }
3,227✔
353

354
    void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
2,744✔
355
    {
2,744✔
356
        if (x.constList().size() == 1)
2,744✔
357
            buildAndThrowError("Invalid condition: missing 'cond' and 'then' nodes, expected (if cond then)", x);
2✔
358
        if (x.constList().size() == 2)
2,743✔
359
            buildAndThrowError(fmt::format("Invalid condition: missing 'then' node, expected (if {} then)", x.constList()[1].repr()), x);
1✔
360

361
        // compile condition
362
        compileExpression(x.list()[1], p, false, false);
2,742✔
363
        page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
2,742✔
364

365
        // jump only if needed to the "true" branch
366
        const auto label_then = IR::Entity::Label(m_current_label++);
2,742✔
367
        page(p).emplace_back(IR::Entity::GotoIf(label_then, true));
2,742✔
368

369
        // "false" branch code
370
        if (x.constList().size() == 4)  // we have an else clause
2,742✔
371
        {
372
            m_locals_locator.saveScopeLengthForBranch();
2,029✔
373
            compileExpression(x.list()[3], p, is_result_unused, is_terminal);
2,029✔
374
            page(p).back().setSourceLocation(x.constList()[3].filename(), x.constList()[3].position().start.line);
2,029✔
375
            m_locals_locator.dropVarsForBranch();
2,029✔
376
        }
2,029✔
377

378
        // when else is finished, jump to end
379
        const auto label_end = IR::Entity::Label(m_current_label++);
2,742✔
380
        page(p).emplace_back(IR::Entity::Goto(label_end));
2,742✔
381

382
        // absolute address to jump to if condition is true
383
        page(p).emplace_back(label_then);
2,742✔
384
        // if code
385
        m_locals_locator.saveScopeLengthForBranch();
2,742✔
386
        compileExpression(x.list()[2], p, is_result_unused, is_terminal);
2,742✔
387
        page(p).back().setSourceLocation(x.constList()[2].filename(), x.constList()[2].position().start.line);
2,742✔
388
        m_locals_locator.dropVarsForBranch();
2,742✔
389
        // set jump to end pos
390
        page(p).emplace_back(label_end);
2,742✔
391
    }
2,744✔
392

393
    void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused)
3,780✔
394
    {
3,780✔
395
        if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List)
3,782✔
396
            buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args);
1✔
397
        if (x.constList().size() != 3)
3,779✔
398
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
399

400
        // capture, if needed
401
        std::size_t capture_inst_count = 0;
3,778✔
402
        for (const auto& node : x.constList()[1].constList())
9,825✔
403
        {
404
            if (node.nodeType() == NodeType::Capture)
6,047✔
405
            {
406
                const uint16_t symbol_id = addSymbol(node);
227✔
407

408
                // We have an unqualified name that isn't the captured name
409
                // This means we need to rename the captured value
410
                if (const auto& maybe_nqn = node.getUnqualifiedName(); maybe_nqn.has_value() && maybe_nqn.value() != node.string())
454✔
411
                {
412
                    const uint16_t nqn_id = addSymbol(Node(NodeType::Symbol, maybe_nqn.value()));
14✔
413

414
                    page(p).emplace_back(RENAME_NEXT_CAPTURE, nqn_id);
14✔
415
                    page(p).emplace_back(CAPTURE, symbol_id);
14✔
416
                }
14✔
417
                else
418
                    page(p).emplace_back(CAPTURE, symbol_id);
213✔
419

420
                ++capture_inst_count;
227✔
421
            }
227✔
422
        }
6,047✔
423
        const bool is_closure = capture_inst_count > 0;
3,778✔
424

425
        m_locals_locator.createScope(
7,556✔
426
            is_closure
3,778✔
427
                ? LocalsLocator::ScopeType::Closure
428
                : LocalsLocator::ScopeType::Function);
429

430
        // create new page for function body
431
        const auto function_body_page = createNewCodePage();
3,778✔
432
        // save page_id into the constants table as PageAddr and load the const
433
        page(p).emplace_back(is_closure ? MAKE_CLOSURE : LOAD_CONST, addValue(function_body_page.index, x));
3,778✔
434

435
        // pushing arguments from the stack into variables in the new scope
436
        for (const auto& node : x.constList()[1].constList())
9,825✔
437
        {
438
            if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg)
6,047✔
439
            {
440
                page(function_body_page).emplace_back(STORE, addSymbol(node));
4,967✔
441
                m_locals_locator.addLocal(node.string());
4,967✔
442
            }
4,967✔
443
            else if (node.nodeType() == NodeType::RefArg)
1,080✔
444
            {
445
                page(function_body_page).emplace_back(STORE_REF, addSymbol(node));
853✔
446
                m_locals_locator.addLocal(node.string());
853✔
447
            }
853✔
448
        }
6,047✔
449

450
        // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls.
451
        // This way we can continue to safely apply optimisations on
452
        // (let name (fun (e) (map lst (fun (e) (name e)))))
453
        // Otherwise, `name` would have been optimized to a GET_CURRENT_PAGE_ADDRESS, which would have returned the wrong page.
454
        if (x.isAnonymousFunction())
3,778✔
455
            m_opened_vars.emplace("#anonymous");
400✔
456
        // push body of the function
457
        compileExpression(x.list()[2], function_body_page, false, true);
3,778✔
458
        if (x.isAnonymousFunction())
3,778✔
459
            m_opened_vars.pop();
400✔
460

461
        // return last value on the stack
462
        page(function_body_page).emplace_back(RET);
3,778✔
463
        m_locals_locator.deleteScope();
3,778✔
464

465
        // if the computed function is unused, pop it
466
        if (is_result_unused)
3,778✔
467
        {
UNCOV
468
            warning("Unused declared function", x);
×
UNCOV
469
            page(p).emplace_back(POP);
×
UNCOV
470
        }
×
471
    }
3,780✔
472

473
    void ASTLowerer::compileLetMutSet(const Keyword n, Node& x, const Page p)
12,047✔
474
    {
12,047✔
475
        if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol)
12,054✔
UNCOV
476
            buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym);
×
477
        if (x.constList().size() != 3)
12,047✔
478
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
479

480
        const std::string name = x.constList()[1].string();
12,046✔
481
        uint16_t i = addSymbol(x.constList()[1]);
12,046✔
482

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

486
        const bool is_function = x.constList()[2].isFunction();
12,045✔
487
        if (is_function)
12,045✔
488
        {
489
            m_opened_vars.push(name);
3,380✔
490
            x.list()[2].setFunctionKind(/* anonymous= */ false);
3,380✔
491
        }
3,380✔
492

493
        // put value before symbol id
494
        // starting at index = 2 because x is a (let|mut|set variable ...) node
495
        for (std::size_t idx = 2, end = x.constList().size(); idx < end; ++idx)
24,090✔
496
            compileExpression(x.list()[idx], p, false, false);
12,045✔
497

498
        if (n == Keyword::Let || n == Keyword::Mut)
12,040✔
499
        {
500
            page(p).emplace_back(STORE, i);
7,998✔
501
            m_locals_locator.addLocal(name);
7,998✔
502
        }
7,998✔
503
        else
504
            page(p).emplace_back(SET_VAL, i);
4,042✔
505

506
        if (is_function)
12,040✔
507
            m_opened_vars.pop();
3,375✔
508
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
12,040✔
509
    }
12,048✔
510

511
    void ASTLowerer::compileWhile(Node& x, const Page p)
1,226✔
512
    {
1,226✔
513
        if (x.constList().size() != 3)
1,226✔
514
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
515

516
        m_locals_locator.createScope();
1,225✔
517
        page(p).emplace_back(CREATE_SCOPE);
1,225✔
518
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,225✔
519

520
        // save current position to jump there at the end of the loop
521
        const auto label_loop = IR::Entity::Label(m_current_label++);
1,225✔
522
        page(p).emplace_back(label_loop);
1,225✔
523
        // push condition
524
        compileExpression(x.list()[1], p, false, false);
1,225✔
525
        // absolute jump to end of block if condition is false
526
        const auto label_end = IR::Entity::Label(m_current_label++);
1,225✔
527
        page(p).emplace_back(IR::Entity::GotoIf(label_end, false));
1,225✔
528
        // push code to page
529
        compileExpression(x.list()[2], p, true, false);
1,225✔
530

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

537
        // absolute address to jump to if condition is false
538
        page(p).emplace_back(label_end);
1,225✔
539

540
        page(p).emplace_back(POP_SCOPE);
1,225✔
541
        m_locals_locator.deleteScope();
1,225✔
542
    }
1,226✔
543

544
    void ASTLowerer::compilePluginImport(const Node& x, const Page p)
2✔
545
    {
2✔
546
        std::string path;
2✔
547
        const Node package_node = x.constList()[1];
2✔
548
        for (std::size_t i = 0, end = package_node.constList().size(); i < end; ++i)
4✔
549
        {
550
            path += package_node.constList()[i].string();
2✔
551
            if (i + 1 != end)
2✔
UNCOV
552
                path += "/";
×
553
        }
2✔
554
        path += ".arkm";
2✔
555

556
        // register plugin path in the constants table
557
        uint16_t id = addValue(Node(NodeType::String, path));
2✔
558
        // add plugin instruction + id of the constant referring to the plugin path
559
        page(p).emplace_back(PLUGIN, id);
2✔
560
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
561
    }
2✔
562

563
    void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call)
8,005✔
564
    {
8,005✔
565
        const auto node = call.constList()[0];
8,005✔
566

567
        // push the arguments in reverse order because the function loads its arguments in the order they are defined:
568
        // (fun (a b c) ...) -> load 'a', then 'b', then 'c'
569
        // We have to push arguments in this order and load them in reverse, because we are using references internally,
570
        // which can cause problems for recursive functions that swap their arguments around.
571
        // Eg (let foo (fun (a b c) (if (> a 0) (foo (- a 1) c (+ b c)) 1))) (foo 12 0 1)
572
        // 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
573
        // value for argument b, but loaded it as a reference.
574
        for (Node& value : std::ranges::drop_view(call.list(), 1) | std::views::reverse)
21,917✔
575
        {
576
            // FIXME: in (foo a b (breakpoint (< c 0)) c), we will push c before the breakpoint
577
            if (nodeProducesOutput(value) || isBreakpoint(value))
13,912✔
578
            {
579
                // 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
580
                if (value.nodeType() == NodeType::Symbol && is_tail_call)
13,910✔
581
                    compileSymbol(value, p, false, /* can_use_ref= */ false);
85✔
582
                else
583
                    compileExpression(value, p, false, false);
13,825✔
584
            }
13,906✔
585
            else
586
            {
587
                std::string message;
2✔
588
                if (is_tail_call)
2✔
589
                    message = fmt::format("Invalid node inside tail call to `{}'", node.repr());
1✔
590
                else
591
                    message = fmt::format("Invalid node inside call to `{}'", node.repr());
1✔
592
                buildAndThrowError(message, value);
2✔
593
            }
2✔
594
        }
13,912✔
595
    }
8,011✔
596

597
    void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal)
47,623✔
598
    {
47,623✔
599
        constexpr std::size_t start_index = 1;
47,623✔
600

601
        Node& node = x.list()[0];
47,623✔
602
        const std::optional<Instruction> maybe_operator = node.nodeType() == NodeType::Symbol ? getOperator(node.string()) : std::nullopt;
47,623✔
603

604
        const std::optional<Instruction> maybe_shortcircuit =
47,623✔
605
            node.nodeType() == NodeType::Symbol
68,483✔
606
            ? (node.string() == Language::And
41,352✔
607
                   ? std::make_optional(Instruction::SHORTCIRCUIT_AND)
368✔
608
                   : (node.string() == Language::Or
20,492✔
609
                          ? std::make_optional(Instruction::SHORTCIRCUIT_OR)
115✔
610
                          : std::nullopt))
20,377✔
611
            : std::nullopt;
26,763✔
612

613
        if (maybe_shortcircuit.has_value())
47,623✔
614
        {
615
            // short circuit implementation
616
            if (x.constList().size() < 3)
483✔
617
                buildAndThrowError(
2✔
618
                    fmt::format(
4✔
619
                        "Expected at least 2 arguments while compiling '{}', got {}",
2✔
620
                        node.string(),
2✔
621
                        x.constList().size() - 1),
2✔
622
                    x);
2✔
623

624
            compileExpression(x.list()[1], p, false, false);
481✔
625

626
            const auto label_shortcircuit = IR::Entity::Label(m_current_label++);
481✔
627
            auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, maybe_shortcircuit.value());
481✔
628
            page(p).emplace_back(shortcircuit_entity);
481✔
629

630
            for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
580✔
631
            {
632
                compileExpression(x.list()[i], p, false, false);
99✔
633
                if (i + 1 != end)
99✔
634
                    page(p).emplace_back(shortcircuit_entity);
99✔
635
            }
99✔
636

637
            page(p).emplace_back(label_shortcircuit);
481✔
638
        }
481✔
639
        else if (!maybe_operator.has_value())
47,140✔
640
        {
641
            if (is_terminal && node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string())
8,008✔
642
            {
643
                pushFunctionCallArguments(x, p, /* is_tail_call= */ true);
112✔
644

645
                // jump to the top of the function
646
                page(p).emplace_back(JUMP, 0_u16);
112✔
647
                page(p).back().setSourceLocation(node.filename(), node.position().start.line);
112✔
648
                return;  // skip the potential Instruction::POP at the end
112✔
649
            }
650
            else
651
            {
652
                if (!nodeProducesOutput(node))
7,896✔
653
                    buildAndThrowError(fmt::format("Can not call `{}', as it doesn't return a value", node.repr()), node);
2✔
654

655
                const auto proc_page = createNewCodePage(/* temp= */ true);
7,894✔
656

657
                // compile the function resolution to a separate page
658
                if (node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string())
7,894✔
659
                {
660
                    // The function is trying to call itself, but this isn't a tail call.
661
                    // We can skip the LOAD_FAST function_name and directly push the current
662
                    // function page, which will be quicker than a local variable resolution.
663
                    // We set its argument to the symbol id of the function we are calling,
664
                    // so that the VM knows the name of the last called function.
665
                    page(proc_page).emplace_back(GET_CURRENT_PAGE_ADDR, addSymbol(node));
57✔
666
                }
57✔
667
                else
668
                {
669
                    // closure chains have been handled (eg: closure.field.field.function)
670
                    compileExpression(node, proc_page, false, false);  // storing proc
7,837✔
671
                }
672

673
                if (m_temp_pages.back().empty())
7,894✔
UNCOV
674
                    buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x);
×
675

676
                const auto label_return = IR::Entity::Label(m_current_label++);
7,894✔
677
                page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
7,894✔
678
                page(p).back().setSourceLocation(x.filename(), x.position().start.line);
7,894✔
679

680
                pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
7,894✔
681
                // push proc from temp page
682
                for (const auto& inst : m_temp_pages.back())
16,461✔
683
                    page(p).push_back(inst);
8,572✔
684
                m_temp_pages.pop_back();
7,889✔
685

686
                // number of arguments
687
                std::size_t args_count = 0;
7,889✔
688
                for (auto it = x.constList().begin() + start_index, it_end = x.constList().end(); it != it_end; ++it)
21,516✔
689
                {
690
                    if (it->nodeType() != NodeType::Capture && !isBreakpoint(*it))
13,627✔
691
                        args_count++;
13,623✔
692
                }
13,627✔
693
                // call the procedure
694
                page(p).emplace_back(CALL, args_count);
7,889✔
695
                page(p).back().setSourceLocation(node.filename(), node.position().start.line);
7,889✔
696

697
                // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
698
                page(p).emplace_back(label_return);
7,889✔
699
            }
7,894✔
700
        }
7,889✔
701
        else  // operator
702
        {
703
            // retrieve operator
704
            const auto op = maybe_operator.value();
39,132✔
705
            const auto op_name = Language::operators[static_cast<std::size_t>(op - FIRST_OPERATOR)];
39,132✔
706

707
            if (op == BREAKPOINT)
39,132✔
708
                is_result_unused = false;
15✔
709

710
            // push arguments on current page
711
            std::size_t exp_count = 0;
39,132✔
712
            for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
49,705✔
713
            {
714
                const bool is_breakpoint = isBreakpoint(x.constList()[index]);
36,657✔
715
                if (nodeProducesOutput(x.constList()[index]) || is_breakpoint)
36,657✔
716
                    compileExpression(x.list()[index], p, false, false);
36,656✔
717
                else
718
                    buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'", node.repr()), x.constList()[index]);
1✔
719

720
                if (!is_breakpoint && ((index + 1 < size && x.constList()[index + 1].nodeType() != NodeType::Capture) || index + 1 == size))
36,656✔
721
                    exp_count++;
23,614✔
722

723
                // in order to be able to handle things like (op A B C D...)
724
                // which should be transformed into A B op C op D op...
725
                if (exp_count >= 2 && !isTernaryInst(op) && !is_breakpoint)
36,656✔
726
                    page(p).emplace_back(op);
10,472✔
727
            }
10,573✔
728

729
            if (isBreakpoint(x))
13,047✔
730
            {
731
                if (exp_count > 1)
15✔
732
                    buildAndThrowError(fmt::format("`{}' expected at most one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
733
                page(p).emplace_back(op, exp_count);
14✔
734
            }
14✔
735
            else if (isUnaryInst(op))
13,032✔
736
            {
737
                if (exp_count != 1)
2,632✔
738
                    buildAndThrowError(fmt::format("`{}' expected one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
739
                page(p).emplace_back(op);
2,631✔
740
            }
2,631✔
741
            else if (isTernaryInst(op))
10,400✔
742
            {
743
                if (exp_count != 3)
50✔
744
                    buildAndThrowError(fmt::format("`{}' expected three arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
745
                page(p).emplace_back(op);
49✔
746
            }
49✔
747
            else if (exp_count <= 1)
10,350✔
748
                buildAndThrowError(fmt::format("`{}' expected two arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
2✔
749

750
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
13,042✔
751

752
            // need to check we didn't push the (op A B C D...) things for operators not supporting it
753
            if (exp_count > 2)
13,042✔
754
            {
755
                switch (op)
158✔
756
                {
150✔
757
                    // authorized instructions
758
                    case ADD: [[fallthrough]];
759
                    case SUB: [[fallthrough]];
760
                    case MUL: [[fallthrough]];
761
                    case DIV: [[fallthrough]];
762
                    case MOD: [[fallthrough]];
763
                    case AT_AT:
764
                        break;
158✔
765

766
                    default:
767
                        buildAndThrowError(fmt::format("`{}' requires 2 arguments, but got {}.", op_name, exp_count), x);
8✔
768
                }
150✔
769
            }
150✔
770
        }
13,048✔
771

772
        if (is_result_unused)
21,404✔
773
            page(p).emplace_back(POP);
2,974✔
774
    }
21,560✔
775

776
    uint16_t ASTLowerer::addSymbol(const Node& sym)
38,882✔
777
    {
38,882✔
778
        // otherwise, add the symbol, and return its id in the table
779
        auto it = std::ranges::find(m_symbols, sym.string());
38,882✔
780
        if (it == m_symbols.end())
38,882✔
781
        {
782
            m_symbols.push_back(sym.string());
7,267✔
783
            it = m_symbols.begin() + static_cast<std::vector<std::string>::difference_type>(m_symbols.size() - 1);
7,267✔
784
        }
7,267✔
785

786
        const auto distance = std::distance(m_symbols.begin(), it);
38,882✔
787
        if (std::cmp_less(distance, MaxValue16Bits))
38,882✔
788
            return static_cast<uint16_t>(distance);
77,764✔
UNCOV
789
        buildAndThrowError(fmt::format("Too many symbols (exceeds {}), aborting compilation.", MaxValue16Bits), sym);
×
790
    }
38,882✔
791

792
    uint16_t ASTLowerer::addValue(const Node& x)
15,979✔
793
    {
15,979✔
794
        const ValTableElem v(x);
15,979✔
795
        auto it = std::ranges::find(m_values, v);
15,979✔
796
        if (it == m_values.end())
15,979✔
797
        {
798
            m_values.push_back(v);
3,471✔
799
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,471✔
800
        }
3,471✔
801

802
        const auto distance = std::distance(m_values.begin(), it);
15,979✔
803
        if (std::cmp_less(distance, MaxValue16Bits))
15,979✔
804
            return static_cast<uint16_t>(distance);
15,979✔
UNCOV
805
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), x);
×
806
    }
15,979✔
807

808
    uint16_t ASTLowerer::addValue(const std::size_t page_id, const Node& current)
3,778✔
809
    {
3,778✔
810
        const ValTableElem v(page_id);
3,778✔
811
        auto it = std::ranges::find(m_values, v);
3,778✔
812
        if (it == m_values.end())
3,778✔
813
        {
814
            m_values.push_back(v);
3,778✔
815
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,778✔
816
        }
3,778✔
817

818
        const auto distance = std::distance(m_values.begin(), it);
3,778✔
819
        if (std::cmp_less(distance, MaxValue16Bits))
3,778✔
820
            return static_cast<uint16_t>(distance);
3,778✔
UNCOV
821
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), current);
×
822
    }
3,778✔
823
}
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