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

ArkScript-lang / Ark / 21915596651

11 Feb 2026 05:26PM UTC coverage: 93.464% (+0.05%) from 93.413%
21915596651

Pull #640

github

web-flow
Merge 867682372 into 9cb43e0cb
Pull Request #640: Feat/apply

623 of 648 new or added lines in 8 files covered. (96.14%)

1 existing line in 1 file now uncovered.

9195 of 9838 relevant lines covered (93.46%)

267918.01 hits per line

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

94.24
/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) :
427✔
19
        m_logger("ASTLowerer", debug)
427✔
20
    {}
854✔
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)
339✔
34
    {
339✔
35
        m_logger.traceStart("process");
339✔
36
        const Page global = createNewCodePage();
339✔
37

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

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

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

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

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

70
    std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
35,922✔
71
    {
35,922✔
72
        const auto it = std::ranges::find_if(Builtins::builtins,
35,922✔
73
                                             [&name](const std::pair<std::string, Value>& element) -> bool {
3,026,995✔
74
                                                 return name == element.first;
2,991,073✔
75
                                             });
76
        if (it != Builtins::builtins.end())
35,922✔
77
            return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
3,591✔
78
        return std::nullopt;
32,331✔
79
    }
35,922✔
80

81
    std::optional<Instruction> ASTLowerer::getListInstruction(const std::string& name) noexcept
29,056✔
82
    {
29,056✔
83
        const auto it = std::ranges::find(Language::listInstructions, name);
29,056✔
84
        if (it != Language::listInstructions.end())
29,056✔
85
            return static_cast<Instruction>(std::distance(Language::listInstructions.begin(), it) + LIST);
7,258✔
86
        return std::nullopt;
21,798✔
87
    }
29,056✔
88

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

96
    bool ASTLowerer::nodeProducesOutput(const Node& node)
57,474✔
97
    {
57,474✔
98
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
57,474✔
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())) ||
884✔
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 ||
544✔
103
                // a condition produces a value if all its branches produce a value
104
                (node.constList()[0].keyword() == Keyword::If &&
102✔
105
                 nodeProducesOutput(node.constList()[2]) &&
521✔
106
                 (node.constList().size() == 3 || nodeProducesOutput(node.constList()[3])));
94✔
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)
57,459✔
109
            return std::ranges::find(Language::UpdateRef, node.constList().front().string()) == Language::UpdateRef.end() &&
19,100✔
110
                node.constList().front().string() != "breakpoint";
9,550✔
111
        return true;  // any other node, function call, symbol, number...
47,482✔
112
    }
57,474✔
113

114
    bool ASTLowerer::isUnaryInst(const Instruction inst) noexcept
13,366✔
115
    {
13,366✔
116
        switch (inst)
13,366✔
117
        {
2,703✔
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,366✔
128

129
            default:
130
                return false;
10,663✔
131
        }
132
    }
13,366✔
133

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

141
            default:
142
                return false;
21,363✔
143
        }
144
    }
21,581✔
145

146
    bool ASTLowerer::isRepeatableOperation(const Instruction inst) noexcept
179✔
147
    {
179✔
148
        switch (inst)
179✔
149
        {
117✔
150
            case ADD: [[fallthrough]];
151
            case SUB: [[fallthrough]];
152
            case MUL: [[fallthrough]];
153
            case DIV:
154
                return true;
179✔
155

156
            default:
157
                return false;
62✔
158
        }
159
    }
179✔
160

161
    void ASTLowerer::warning(const std::string& message, const Node& node)
×
162
    {
×
163
        fmt::println("{} {}", fmt::styled("Warning", fmt::fg(fmt::color::dark_orange)), Diagnostics::makeContextWithNode(message, node));
×
164
    }
×
165

166
    void ASTLowerer::buildAndThrowError(const std::string& message, const Node& node)
41✔
167
    {
41✔
168
        throw CodeError(message, CodeErrorContext(node.filename(), node.position()));
41✔
169
    }
41✔
170

171
    void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
104,588✔
172
    {
104,588✔
173
        // register symbols
174
        if (x.nodeType() == NodeType::Symbol)
104,588✔
175
            compileSymbol(x, p, is_result_unused, /* can_use_ref= */ true);
34,028✔
176
        else if (x.nodeType() == NodeType::Field)
70,560✔
177
        {
178
            // the parser guarantees us that there is at least 2 elements (eg: a.b)
179
            compileSymbol(x.list()[0], p, is_result_unused, /* can_use_ref= */ true);
1,809✔
180
            for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
3,624✔
181
            {
182
                uint16_t i = addSymbol(*it);
1,815✔
183
                page(p).emplace_back(GET_FIELD, i);
1,815✔
184
            }
1,815✔
185
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,809✔
186
        }
1,809✔
187
        // register values
188
        else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
68,751✔
189
        {
190
            uint16_t i = addValue(x);
16,926✔
191

192
            if (!is_result_unused)
16,926✔
193
                page(p).emplace_back(LOAD_CONST, i);
16,926✔
194
        }
16,926✔
195
        // namespace nodes
196
        else if (x.nodeType() == NodeType::Namespace)
51,825✔
197
            compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
136✔
198
        else if (x.nodeType() == NodeType::List)
51,689✔
199
        {
200
            // empty code block should be nil
201
            if (x.constList().empty())
51,684✔
202
            {
NEW
203
                if (!is_result_unused)
×
204
                {
205
                    static const std::optional<uint16_t> nil = getBuiltin("nil");
99✔
NEW
206
                    page(p).emplace_back(BUILTIN, nil.value());
×
NEW
207
                }
×
208
            }
×
209
            // list instructions
210
            else if (const auto head = x.constList()[0]; head.nodeType() == NodeType::Symbol && getListInstruction(head.string()).has_value())
103,368✔
211
                compileListInstruction(x, p, is_result_unused);
3,629✔
212
            else if (head.nodeType() == NodeType::Symbol && head.string() == Language::Apply)
48,055✔
213
                compileApplyInstruction(x, p, is_result_unused);
184✔
214
            // registering structures
215
            else if (head.nodeType() == NodeType::Keyword)
47,871✔
216
            {
217
                switch (const Keyword keyword = head.keyword())
25,505✔
218
                {
2,918✔
219
                    case Keyword::If:
220
                        compileIf(x, p, is_result_unused, is_terminal);
2,918✔
221
                        break;
15,342✔
222

223
                    case Keyword::Set:
224
                        [[fallthrough]];
225
                    case Keyword::Let:
226
                        [[fallthrough]];
227
                    case Keyword::Mut:
228
                        compileLetMutSet(keyword, x, p);
12,426✔
229
                        break;
16,211✔
230

231
                    case Keyword::Fun:
232
                        compileFunction(x, p, is_result_unused);
3,792✔
233
                        break;
8,920✔
234

235
                    case Keyword::Begin:
236
                    {
237
                        for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
25,399✔
238
                            compileExpression(
40,532✔
239
                                x.list()[i],
20,266✔
240
                                p,
20,266✔
241
                                // All the nodes in a 'begin' (except for the last one) are producing a result that we want to drop.
242
                                /* is_result_unused= */ (i != size - 1) || is_result_unused,
20,266✔
243
                                // If the 'begin' is a terminal node, only its last node is terminal.
244
                                /* is_terminal= */ is_terminal && (i == size - 1));
20,266✔
245
                        break;
5,090✔
246
                    }
1,232✔
247

248
                    case Keyword::While:
249
                        compileWhile(x, p);
1,232✔
250
                        break;
1,233✔
251

252
                    case Keyword::Import:
253
                        compilePluginImport(x, p);
2✔
254
                        break;
4✔
255

256
                    case Keyword::Del:
257
                        page(p).emplace_back(DEL, addSymbol(x.constList()[1]));
2✔
258
                        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
259
                        break;
2✔
260
                }
25,505✔
261
            }
25,447✔
262
            else
263
            {
264
                // If we are here, we should have a function name via the m_opened_vars.
265
                // Push arguments first, then function name, then call it.
266
                handleCalls(x, p, is_result_unused, is_terminal);
22,366✔
267
            }
268
        }
51,585✔
269
        else if (x.nodeType() != NodeType::Unused)
5✔
UNCOV
270
            buildAndThrowError(
×
271
                fmt::format(
×
272
                    "NodeType `{}' not handled in ASTLowerer::compileExpression. Please fill an issue on GitHub: https://github.com/ArkScript-lang/Ark",
×
273
                    typeToString(x)),
×
274
                x);
×
275
    }
104,588✔
276

277
    void ASTLowerer::compileSymbol(const Node& x, const Page p, const bool is_result_unused, const bool can_use_ref)
35,922✔
278
    {
35,922✔
279
        const std::string& name = x.string();
35,922✔
280

281
        if (const auto it_builtin = getBuiltin(name))
71,844✔
282
            page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
3,591✔
283
        else if (getOperator(name).has_value())
32,331✔
284
            buildAndThrowError(fmt::format("Found a freestanding operator: `{}`. It can not be used as value like `+', where (let add +) (add 1 2) would be valid", name), x);
2✔
285
        else
286
        {
287
            if (can_use_ref)
32,329✔
288
            {
289
                const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
32,244✔
290
                if (maybe_local_idx.has_value())
32,244✔
291
                    page(p).emplace_back(LOAD_FAST_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
12,215✔
292
                else
293
                    page(p).emplace_back(LOAD_FAST, addSymbol(x));
20,029✔
294
            }
32,244✔
295
            else
296
                page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
85✔
297
        }
298

299
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
35,920✔
300

301
        if (is_result_unused)
35,920✔
302
        {
303
            warning("Statement has no effect", x);
×
304
            page(p).emplace_back(POP);
×
305
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
×
306
        }
×
307
    }
35,922✔
308

309
    void ASTLowerer::compileListInstruction(Node& x, const Page p, const bool is_result_unused)
3,629✔
310
    {
3,629✔
311
        const Node head = x.constList()[0];
3,629✔
312
        std::string name = x.constList()[0].string();
3,629✔
313
        Instruction inst = getListInstruction(name).value();
3,629✔
314

315
        // length of at least 1 since we got a symbol name
316
        const auto argc = x.constList().size() - 1u;
3,629✔
317
        // error, can not use append/concat/pop (and their in place versions) with a <2 length argument list
318
        if (argc < 2 && APPEND <= inst && inst <= POP)
3,629✔
319
            buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), head);
6✔
320
        if (inst <= POP && std::cmp_greater(argc, MaxValue16Bits))
3,623✔
321
            buildAndThrowError(fmt::format("Too many arguments ({}), exceeds {}", argc, MaxValue16Bits), x);
1✔
322
        if (argc != 3 && inst == SET_AT_INDEX)
3,622✔
323
            buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), head);
1✔
324
        if (argc != 4 && inst == SET_AT_2_INDEX)
3,621✔
325
            buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), head);
1✔
326

327
        // compile arguments in reverse order
328
        for (std::size_t i = x.constList().size() - 1u; i > 0; --i)
11,929✔
329
        {
330
            Node& node = x.list()[i];
8,309✔
331
            if (nodeProducesOutput(node))
8,309✔
332
                compileExpression(node, p, false, false);
8,308✔
333
            else
334
                buildAndThrowError(fmt::format("Invalid node inside call to {}", name), node);
1✔
335
        }
8,309✔
336

337
        // put inst and number of arguments
338
        std::size_t inst_argc = 0;
3,619✔
339
        switch (inst)
3,619✔
340
        {
2,702✔
341
            case LIST:
342
                inst_argc = argc;
2,702✔
343
                break;
3,514✔
344

345
            case APPEND:
346
            case APPEND_IN_PLACE:
347
            case CONCAT:
348
            case CONCAT_IN_PLACE:
349
                inst_argc = argc - 1;
812✔
350
                break;
827✔
351

352
            case POP_LIST:
353
            case POP_LIST_IN_PLACE:
354
                inst_argc = 0;
15✔
355
                break;
105✔
356

357
            default:
358
                break;
90✔
359
        }
3,619✔
360
        page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
3,619✔
361
        page(p).back().setSourceLocation(head.filename(), head.position().start.line);
3,619✔
362

363
        if (is_result_unused && name.back() != '!' && inst <= POP_LIST_IN_PLACE)  // in-place functions never push a value
3,619✔
364
        {
365
            warning("Ignoring return value of function", x);
×
366
            page(p).emplace_back(POP);
×
367
        }
×
368
    }
3,639✔
369

370
    void ASTLowerer::compileApplyInstruction(Node& x, const Page p, const bool is_result_unused)
184✔
371
    {
184✔
372
        const Node head = x.constList()[0];
184✔
373
        const auto argc = x.constList().size() - 1u;
184✔
374

375
        if (argc != 2)
184✔
NEW
376
            buildAndThrowError(fmt::format("Expected 2 arguments (function, arguments) for apply, got {}", argc), head);
×
377

378
        const auto label_return = IR::Entity::Label(m_current_label++);
184✔
379
        page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
184✔
380

381
        for (Node& node : x.list() | std::ranges::views::drop(1))
552✔
382
        {
383
            if (nodeProducesOutput(node))
368✔
384
                compileExpression(node, p, false, false);
368✔
385
            else
NEW
386
                buildAndThrowError("Invalid node inside call to apply", node);
×
387
        }
368✔
388
        page(p).emplace_back(APPLY);
184✔
389
        // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
390
        page(p).emplace_back(label_return);
184✔
391

392
        if (is_result_unused)
184✔
NEW
393
            page(p).emplace_back(POP);
×
394
    }
184✔
395

396
    void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
2,918✔
397
    {
2,918✔
398
        if (x.constList().size() == 1)
2,918✔
399
            buildAndThrowError("Invalid condition: missing 'cond' and 'then' nodes, expected (if cond then)", x);
2✔
400
        if (x.constList().size() == 2)
2,917✔
401
            buildAndThrowError(fmt::format("Invalid condition: missing 'then' node, expected (if {} then)", x.constList()[1].repr()), x);
1✔
402

403
        // compile condition
404
        compileExpression(x.list()[1], p, false, false);
2,916✔
405
        page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
2,916✔
406

407
        // jump only if needed to the "true" branch
408
        const auto label_then = IR::Entity::Label(m_current_label++);
2,916✔
409
        page(p).emplace_back(IR::Entity::GotoIf(label_then, true));
2,916✔
410

411
        // "false" branch code
412
        if (x.constList().size() == 4)  // we have an else clause
2,916✔
413
        {
414
            m_locals_locator.saveScopeLengthForBranch();
2,203✔
415
            compileExpression(x.list()[3], p, is_result_unused, is_terminal);
2,203✔
416
            page(p).back().setSourceLocation(x.constList()[3].filename(), x.constList()[3].position().start.line);
2,203✔
417
            m_locals_locator.dropVarsForBranch();
2,203✔
418
        }
2,203✔
419

420
        // when else is finished, jump to end
421
        const auto label_end = IR::Entity::Label(m_current_label++);
2,916✔
422
        page(p).emplace_back(IR::Entity::Goto(label_end));
2,916✔
423

424
        // absolute address to jump to if condition is true
425
        page(p).emplace_back(label_then);
2,916✔
426
        // if code
427
        m_locals_locator.saveScopeLengthForBranch();
2,916✔
428
        compileExpression(x.list()[2], p, is_result_unused, is_terminal);
2,916✔
429
        page(p).back().setSourceLocation(x.constList()[2].filename(), x.constList()[2].position().start.line);
2,916✔
430
        m_locals_locator.dropVarsForBranch();
2,916✔
431
        // set jump to end pos
432
        page(p).emplace_back(label_end);
2,916✔
433
    }
2,918✔
434

435
    void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused)
3,792✔
436
    {
3,792✔
437
        if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List)
3,794✔
438
            buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args);
1✔
439
        if (x.constList().size() != 3)
3,791✔
440
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
441

442
        // capture, if needed
443
        std::size_t capture_inst_count = 0;
3,790✔
444
        for (const auto& node : x.constList()[1].constList())
9,853✔
445
        {
446
            if (node.nodeType() == NodeType::Capture)
6,063✔
447
            {
448
                const uint16_t symbol_id = addSymbol(node);
227✔
449

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

456
                    page(p).emplace_back(RENAME_NEXT_CAPTURE, nqn_id);
14✔
457
                    page(p).emplace_back(CAPTURE, symbol_id);
14✔
458
                }
14✔
459
                else
460
                    page(p).emplace_back(CAPTURE, symbol_id);
213✔
461

462
                ++capture_inst_count;
227✔
463
            }
227✔
464
        }
6,063✔
465
        const bool is_closure = capture_inst_count > 0;
3,790✔
466

467
        m_locals_locator.createScope(
7,580✔
468
            is_closure
3,790✔
469
                ? LocalsLocator::ScopeType::Closure
470
                : LocalsLocator::ScopeType::Function);
471

472
        // create new page for function body
473
        const auto function_body_page = createNewCodePage();
3,790✔
474
        // save page_id into the constants table as PageAddr and load the const
475
        page(p).emplace_back(is_closure ? MAKE_CLOSURE : LOAD_CONST, addValue(function_body_page.index, x));
3,790✔
476

477
        // pushing arguments from the stack into variables in the new scope
478
        for (const auto& node : x.constList()[1].constList())
9,853✔
479
        {
480
            if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg)
6,063✔
481
            {
482
                page(function_body_page).emplace_back(STORE, addSymbol(node));
4,981✔
483
                m_locals_locator.addLocal(node.string());
4,981✔
484
            }
4,981✔
485
            else if (node.nodeType() == NodeType::RefArg)
1,082✔
486
            {
487
                page(function_body_page).emplace_back(STORE_REF, addSymbol(node));
855✔
488
                m_locals_locator.addLocal(node.string());
855✔
489
            }
855✔
490
        }
6,063✔
491

492
        // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls.
493
        // This way we can continue to safely apply optimisations on
494
        // (let name (fun (e) (map lst (fun (e) (name e)))))
495
        // Otherwise, `name` would have been optimized to a GET_CURRENT_PAGE_ADDRESS, which would have returned the wrong page.
496
        if (x.isAnonymousFunction())
3,790✔
497
            m_opened_vars.emplace("#anonymous");
402✔
498
        // push body of the function
499
        compileExpression(x.list()[2], function_body_page, false, true);
3,790✔
500
        if (x.isAnonymousFunction())
3,790✔
501
            m_opened_vars.pop();
402✔
502

503
        // return last value on the stack
504
        page(function_body_page).emplace_back(RET);
3,790✔
505
        m_locals_locator.deleteScope();
3,790✔
506

507
        // if the computed function is unused, pop it
508
        if (is_result_unused)
3,790✔
509
        {
510
            warning("Unused declared function", x);
×
511
            page(p).emplace_back(POP);
×
512
        }
×
513
    }
3,792✔
514

515
    void ASTLowerer::compileLetMutSet(const Keyword n, Node& x, const Page p)
12,426✔
516
    {
12,426✔
517
        if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol)
12,433✔
518
            buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym);
×
519
        if (x.constList().size() != 3)
12,426✔
520
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
521

522
        const std::string name = x.constList()[1].string();
12,425✔
523
        uint16_t i = addSymbol(x.constList()[1]);
12,425✔
524

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

528
        const bool is_function = x.constList()[2].isFunction();
12,424✔
529
        if (is_function)
12,424✔
530
        {
531
            m_opened_vars.push(name);
3,390✔
532
            x.list()[2].setFunctionKind(/* anonymous= */ false);
3,390✔
533
        }
3,390✔
534

535
        // put value before symbol id
536
        // starting at index = 2 because x is a (let|mut|set variable ...) node
537
        for (std::size_t idx = 2, end = x.constList().size(); idx < end; ++idx)
24,848✔
538
            compileExpression(x.list()[idx], p, false, false);
12,424✔
539

540
        if (n == Keyword::Let || n == Keyword::Mut)
12,419✔
541
        {
542
            page(p).emplace_back(STORE, i);
8,050✔
543
            m_locals_locator.addLocal(name);
8,050✔
544
        }
8,050✔
545
        else
546
            page(p).emplace_back(SET_VAL, i);
4,369✔
547

548
        if (is_function)
12,419✔
549
            m_opened_vars.pop();
3,385✔
550
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
12,419✔
551
    }
12,427✔
552

553
    void ASTLowerer::compileWhile(Node& x, const Page p)
1,232✔
554
    {
1,232✔
555
        if (x.constList().size() != 3)
1,232✔
556
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
557

558
        m_locals_locator.createScope();
1,231✔
559
        page(p).emplace_back(CREATE_SCOPE);
1,231✔
560
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,231✔
561

562
        // save current position to jump there at the end of the loop
563
        const auto label_loop = IR::Entity::Label(m_current_label++);
1,231✔
564
        page(p).emplace_back(label_loop);
1,231✔
565
        // push condition
566
        compileExpression(x.list()[1], p, false, false);
1,231✔
567
        // absolute jump to end of block if condition is false
568
        const auto label_end = IR::Entity::Label(m_current_label++);
1,231✔
569
        page(p).emplace_back(IR::Entity::GotoIf(label_end, false));
1,231✔
570
        // push code to page
571
        compileExpression(x.list()[2], p, true, false);
1,231✔
572

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

579
        // absolute address to jump to if condition is false
580
        page(p).emplace_back(label_end);
1,231✔
581

582
        page(p).emplace_back(POP_SCOPE);
1,231✔
583
        m_locals_locator.deleteScope();
1,231✔
584
    }
1,232✔
585

586
    void ASTLowerer::compilePluginImport(const Node& x, const Page p)
2✔
587
    {
2✔
588
        std::string path;
2✔
589
        const Node package_node = x.constList()[1];
2✔
590
        for (std::size_t i = 0, end = package_node.constList().size(); i < end; ++i)
4✔
591
        {
592
            path += package_node.constList()[i].string();
2✔
593
            if (i + 1 != end)
2✔
594
                path += "/";
×
595
        }
2✔
596
        path += ".arkm";
2✔
597

598
        // register plugin path in the constants table
599
        uint16_t id = addValue(Node(NodeType::String, path));
2✔
600
        // add plugin instruction + id of the constant referring to the plugin path
601
        page(p).emplace_back(PLUGIN, id);
2✔
602
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
603
    }
2✔
604

605
    void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call)
8,494✔
606
    {
8,494✔
607
        const auto node = call.constList()[0];
8,494✔
608

609
        // push the arguments in reverse order because the function loads its arguments in the order they are defined:
610
        // (fun (a b c) ...) -> load 'a', then 'b', then 'c'
611
        // We have to push arguments in this order and load them in reverse, because we are using references internally,
612
        // which can cause problems for recursive functions that swap their arguments around.
613
        // Eg (let foo (fun (a b c) (if (> a 0) (foo (- a 1) c (+ b c)) 1))) (foo 12 0 1)
614
        // 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
615
        // value for argument b, but loaded it as a reference.
616
        for (Node& value : std::ranges::drop_view(call.list(), 1) | std::views::reverse)
23,417✔
617
        {
618
            // FIXME: in (foo a b (breakpoint (< c 0)) c), we will push c before the breakpoint
619
            if (nodeProducesOutput(value) || isBreakpoint(value))
14,923✔
620
            {
621
                // 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
622
                if (value.nodeType() == NodeType::Symbol && is_tail_call)
14,921✔
623
                    compileSymbol(value, p, false, /* can_use_ref= */ false);
85✔
624
                else
625
                    compileExpression(value, p, false, false);
14,836✔
626
            }
14,914✔
627
            else
628
            {
629
                std::string message;
2✔
630
                if (is_tail_call)
2✔
631
                    message = fmt::format("Invalid node inside tail call to `{}'", node.repr());
1✔
632
                else
633
                    message = fmt::format("Invalid node inside call to `{}'", node.repr());
1✔
634
                buildAndThrowError(message, value);
2✔
635
            }
2✔
636
        }
14,923✔
637
    }
8,503✔
638

639
    void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal)
22,335✔
640
    {
22,335✔
641
        const Node& node = x.constList()[0];
22,335✔
642
        bool matched = false;
22,335✔
643

644
        if (node.nodeType() == NodeType::Symbol)
22,335✔
645
        {
646
            if (node.string() == Language::And || node.string() == Language::Or)
21,614✔
647
            {
648
                matched = true;
486✔
649
                handleShortcircuit(x, p);
486✔
650
            }
486✔
651
            if (const auto maybe_operator = getOperator(node.string()); maybe_operator.has_value())
34,996✔
652
            {
653
                matched = true;
13,382✔
654
                if (maybe_operator.value() == BREAKPOINT)
13,382✔
655
                    is_result_unused = false;
15✔
656
                handleOperator(x, p, maybe_operator.value());
13,382✔
657
            }
13,382✔
658
        }
21,614✔
659

660
        if (!matched)
22,335✔
661
        {
662
            // if nothing else matched, then compile a function call
663
            if (handleFunctionCall(x, p, is_terminal))
8,485✔
664
                // if it returned true, we compiled a tail call, skip the POP at the end
665
                return;
112✔
666
        }
8,373✔
667

668
        if (is_result_unused)
22,223✔
669
            page(p).emplace_back(POP);
3,373✔
670
    }
22,335✔
671

672
    void ASTLowerer::handleShortcircuit(Node& x, const Page p)
486✔
673
    {
486✔
674
        const Node& node = x.constList()[0];
486✔
675
        const auto name = node.string();  // and / or
486✔
676
        const Instruction inst = name == Language::And ? SHORTCIRCUIT_AND : SHORTCIRCUIT_OR;
486✔
677

678
        // short circuit implementation
679
        if (x.constList().size() < 3)
486✔
680
            buildAndThrowError(
2✔
681
                fmt::format(
4✔
682
                    "Expected at least 2 arguments while compiling '{}', got {}",
2✔
683
                    name,
684
                    x.constList().size() - 1),
2✔
685
                x);
2✔
686

687
        if (!nodeProducesOutput(x.list()[1]))
484✔
688
            buildAndThrowError(
1✔
689
                fmt::format(
2✔
690
                    "Can not use `{}' inside a `{}' expression, as it doesn't return a value",
1✔
691
                    x.list()[1].repr(), name),
1✔
692
                x.list()[1]);
1✔
693
        compileExpression(x.list()[1], p, false, false);
483✔
694

695
        const auto label_shortcircuit = IR::Entity::Label(m_current_label++);
483✔
696
        auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, inst);
483✔
697
        page(p).emplace_back(shortcircuit_entity);
483✔
698

699
        for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
1,065✔
700
        {
701
            if (!nodeProducesOutput(x.list()[i]))
582✔
702
                buildAndThrowError(
1✔
703
                    fmt::format(
2✔
704
                        "Can not use `{}' inside a `{}' expression, as it doesn't return a value",
1✔
705
                        x.list()[i].repr(), name),
1✔
706
                    x.list()[i]);
1✔
707
            compileExpression(x.list()[i], p, false, false);
581✔
708
            if (i + 1 != end)
581✔
709
                page(p).emplace_back(shortcircuit_entity);
99✔
710
        }
581✔
711

712
        page(p).emplace_back(label_shortcircuit);
482✔
713
    }
490✔
714

715
    void ASTLowerer::handleOperator(Node& x, const Page p, const Instruction op)
13,382✔
716
    {
13,382✔
717
        constexpr std::size_t start_index = 1;
13,382✔
718
        const Node& node = x.constList()[0];
13,382✔
719
        const auto op_name = Language::operators[static_cast<std::size_t>(op - FIRST_OPERATOR)];
13,382✔
720

721

722
        // push arguments on current page
723
        std::size_t exp_count = 0;
13,382✔
724
        for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
37,617✔
725
        {
726
            const bool is_breakpoint = isBreakpoint(x.constList()[index]);
24,235✔
727
            if (nodeProducesOutput(x.constList()[index]) || is_breakpoint)
24,235✔
728
                compileExpression(x.list()[index], p, false, false);
24,234✔
729
            else
730
                buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'", node.repr()), x.constList()[index]);
1✔
731

732
            if (!is_breakpoint)
24,234✔
733
                exp_count++;
24,232✔
734

735
            // in order to be able to handle things like (op A B C D...)
736
            // which should be transformed into A B op C op D op...
737
            if (exp_count >= 2 && !isTernaryInst(op) && !is_breakpoint)
24,234✔
738
                page(p).emplace_back(op);
10,746✔
739
        }
24,235✔
740

741
        if (isBreakpoint(x))
13,381✔
742
        {
743
            if (exp_count > 1)
15✔
744
                buildAndThrowError(fmt::format("`{}' expected at most one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
745
            page(p).emplace_back(op, exp_count);
14✔
746
        }
14✔
747
        else if (isUnaryInst(op))
13,366✔
748
        {
749
            if (exp_count != 1)
2,703✔
750
                buildAndThrowError(fmt::format("`{}' expected one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
751
            page(p).emplace_back(op);
2,702✔
752
        }
2,702✔
753
        else if (isTernaryInst(op))
10,663✔
754
        {
755
            if (exp_count != 3)
55✔
756
                buildAndThrowError(fmt::format("`{}' expected three arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
757
            page(p).emplace_back(op);
54✔
758
        }
54✔
759
        else if (exp_count <= 1)
10,608✔
760
            buildAndThrowError(fmt::format("`{}' expected two arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
2✔
761

762
        // need to check we didn't push the (op A B C D...) things for operators not supporting it
763
        if (exp_count > 2 && !isRepeatableOperation(op) && !isTernaryInst(op))
13,376✔
764
            buildAndThrowError(fmt::format("`{}' requires 2 arguments, but got {}.", op_name, exp_count), x);
8✔
765

766
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
13,368✔
767
    }
13,383✔
768

769
    bool ASTLowerer::handleFunctionCall(Node& x, const Page p, const bool is_terminal)
8,497✔
770
    {
8,497✔
771
        constexpr std::size_t start_index = 1;
8,497✔
772
        Node& node = x.list()[0];
8,497✔
773

774
        if (is_terminal && node.nodeType() == NodeType::Symbol && isFunctionCallingItself(node.string()))
8,497✔
775
        {
776
            pushFunctionCallArguments(x, p, /* is_tail_call= */ true);
112✔
777

778
            // jump to the top of the function
779
            page(p).emplace_back(JUMP, 0_u16);
112✔
780
            page(p).back().setSourceLocation(node.filename(), node.position().start.line);
112✔
781
            return true;  // skip the potential Instruction::POP at the end
112✔
782
        }
783

784
        if (!nodeProducesOutput(node))
8,385✔
785
            buildAndThrowError(fmt::format("Can not call `{}', as it doesn't return a value", node.repr()), node);
2✔
786

787
        const auto proc_page = createNewCodePage(/* temp= */ true);
8,383✔
788

789
        // compile the function resolution to a separate page
790
        if (node.nodeType() == NodeType::Symbol && isFunctionCallingItself(node.string()))
8,383✔
791
        {
792
            // The function is trying to call itself, but this isn't a tail call.
793
            // We can skip the LOAD_FAST function_name and directly push the current
794
            // function page, which will be quicker than a local variable resolution.
795
            // We set its argument to the symbol id of the function we are calling,
796
            // so that the VM knows the name of the last called function.
797
            page(proc_page).emplace_back(GET_CURRENT_PAGE_ADDR, addSymbol(node));
57✔
798
        }
57✔
799
        else
800
        {
801
            // closure chains have been handled (eg: closure.field.field.function)
802
            compileExpression(node, proc_page, false, false);  // storing proc
8,326✔
803
        }
804

805
        if (m_temp_pages.back().empty())
8,383✔
NEW
806
            buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x);
×
807

808
        const auto label_return = IR::Entity::Label(m_current_label++);
8,383✔
809
        page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
8,383✔
810
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
8,383✔
811

812
        pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
8,383✔
813
        // push proc from temp page
814
        for (const auto& inst : m_temp_pages.back())
17,505✔
815
            page(p).push_back(inst);
9,130✔
816
        m_temp_pages.pop_back();
8,375✔
817

818
        // number of arguments
819
        std::size_t args_count = 0;
8,375✔
820
        for (auto it = x.constList().begin() + start_index, it_end = x.constList().end(); it != it_end; ++it)
23,010✔
821
        {
822
            if (it->nodeType() != NodeType::Capture && !isBreakpoint(*it))
14,635✔
823
                args_count++;
14,631✔
824
        }
14,635✔
825
        // call the procedure
826
        page(p).emplace_back(CALL, args_count);
8,375✔
827
        page(p).back().setSourceLocation(node.filename(), node.position().start.line);
8,375✔
828

829
        // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
830
        page(p).emplace_back(label_return);
8,375✔
831
        return false;  // we didn't compile a tail call
8,375✔
832
    }
8,499✔
833

834
    uint16_t ASTLowerer::addSymbol(const Node& sym)
40,490✔
835
    {
40,490✔
836
        // otherwise, add the symbol, and return its id in the table
837
        auto it = std::ranges::find(m_symbols, sym.string());
40,490✔
838
        if (it == m_symbols.end())
40,490✔
839
        {
840
            m_symbols.push_back(sym.string());
7,288✔
841
            it = m_symbols.begin() + static_cast<std::vector<std::string>::difference_type>(m_symbols.size() - 1);
7,288✔
842
        }
7,288✔
843

844
        const auto distance = std::distance(m_symbols.begin(), it);
40,490✔
845
        if (std::cmp_less(distance, MaxValue16Bits))
40,490✔
846
            return static_cast<uint16_t>(distance);
80,980✔
847
        buildAndThrowError(fmt::format("Too many symbols (exceeds {}), aborting compilation.", MaxValue16Bits), sym);
×
848
    }
40,490✔
849

850
    uint16_t ASTLowerer::addValue(const Node& x)
16,928✔
851
    {
16,928✔
852
        const ValTableElem v(x);
16,928✔
853
        auto it = std::ranges::find(m_values, v);
16,928✔
854
        if (it == m_values.end())
16,928✔
855
        {
856
            m_values.push_back(v);
3,733✔
857
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,733✔
858
        }
3,733✔
859

860
        const auto distance = std::distance(m_values.begin(), it);
16,928✔
861
        if (std::cmp_less(distance, MaxValue16Bits))
16,928✔
862
            return static_cast<uint16_t>(distance);
16,928✔
863
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), x);
×
864
    }
16,928✔
865

866
    uint16_t ASTLowerer::addValue(const std::size_t page_id, const Node& current)
3,790✔
867
    {
3,790✔
868
        const ValTableElem v(page_id);
3,790✔
869
        auto it = std::ranges::find(m_values, v);
3,790✔
870
        if (it == m_values.end())
3,790✔
871
        {
872
            m_values.push_back(v);
3,790✔
873
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,790✔
874
        }
3,790✔
875

876
        const auto distance = std::distance(m_values.begin(), it);
3,790✔
877
        if (std::cmp_less(distance, MaxValue16Bits))
3,790✔
878
            return static_cast<uint16_t>(distance);
3,790✔
879
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), current);
×
880
    }
3,790✔
881
}
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