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

ArkScript-lang / Ark / 22823553555

08 Mar 2026 02:59PM UTC coverage: 93.335% (-0.2%) from 93.519%
22823553555

Pull #654

github

web-flow
Merge a9dde3b83 into caed4a158
Pull Request #654: Feat/show deprecation warnings

5 of 30 new or added lines in 5 files covered. (16.67%)

36 existing lines in 3 files now uncovered.

9438 of 10112 relevant lines covered (93.33%)

271685.22 hits per line

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

94.48
/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

8
#include <Ark/Error/Exceptions.hpp>
9
#include <Ark/Error/Diagnostics.hpp>
10
#include <Ark/Utils/Literals.hpp>
11
#include <Ark/Builtins/Builtins.hpp>
12

13
namespace Ark::internal
14
{
15
    using namespace literals;
16

17
    ASTLowerer::ASTLowerer(const unsigned debug) :
455✔
18
        m_logger("ASTLowerer", debug)
455✔
19
    {}
910✔
20

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

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

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

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

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

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

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

61
    std::optional<Instruction> ASTLowerer::getOperator(const std::string& name) noexcept
56,095✔
62
    {
56,095✔
63
        const auto it = std::ranges::find(Language::operators, name);
56,095✔
64
        if (it != Language::operators.end())
56,095✔
65
            return static_cast<Instruction>(std::distance(Language::operators.begin(), it) + FIRST_OPERATOR);
14,167✔
66
        return std::nullopt;
41,928✔
67
    }
56,095✔
68

69
    std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
37,193✔
70
    {
37,193✔
71
        const auto it = std::ranges::find_if(Builtins::builtins,
37,193✔
72
                                             [&name](const std::pair<std::string, Value>& element) -> bool {
3,164,419✔
73
                                                 return name == element.first;
3,127,226✔
74
                                             });
75
        if (it != Builtins::builtins.end())
37,193✔
76
            return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
3,773✔
77
        return std::nullopt;
33,420✔
78
    }
37,193✔
79

80
    std::optional<Instruction> ASTLowerer::getListInstruction(const std::string& name) noexcept
30,500✔
81
    {
30,500✔
82
        const auto it = std::ranges::find(Language::listInstructions, name);
30,500✔
83
        if (it != Language::listInstructions.end())
30,500✔
84
            return static_cast<Instruction>(std::distance(Language::listInstructions.begin(), it) + LIST);
7,632✔
85
        return std::nullopt;
22,868✔
86
    }
30,500✔
87

88
    bool ASTLowerer::isBreakpoint(const Node& node)
55,202✔
89
    {
55,202✔
90
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
55,202✔
91
            return node.constList().front().string() == "breakpoint";
22,029✔
92
        return false;
33,173✔
93
    }
55,202✔
94

95
    bool ASTLowerer::nodeProducesOutput(const Node& node)
60,498✔
96
    {
60,498✔
97
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
60,498✔
98
            // a begin node produces a value if the last node in it produces a value
99
            return (node.constList()[0].keyword() == Keyword::Begin && node.constList().size() > 1 && nodeProducesOutput(node.constList().back())) ||
962✔
100
                // a function always produces a value ; even if it ends with a node not producing one, the VM returns nil
101
                node.constList()[0].keyword() == Keyword::Fun ||
622✔
102
                // a condition produces a value if all its branches produce a value
103
                (node.constList()[0].keyword() == Keyword::If &&
141✔
104
                 nodeProducesOutput(node.constList()[2]) &&
133✔
105
                 (node.constList().size() == 3 || nodeProducesOutput(node.constList()[3])));
588✔
106
        // in place list instruction, as well as breakpoint, do not produce values
107
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
60,017✔
108
            return std::ranges::find(Language::UpdateRef, node.constList().front().string()) == Language::UpdateRef.end() &&
20,944✔
109
                node.constList().front().string() != "breakpoint";
10,244✔
110
        return true;  // any other node, function call, symbol, number...
49,772✔
111
    }
60,498✔
112

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

128
            default:
129
                return false;
11,398✔
130
        }
131
    }
14,147✔
132

133
    bool ASTLowerer::isTernaryInst(const Instruction inst) noexcept
23,155✔
134
    {
23,155✔
135
        switch (inst)
23,155✔
136
        {
426✔
137
            case AT_AT:
138
                return true;
23,155✔
139

140
            default:
141
                return false;
22,729✔
142
        }
143
    }
23,155✔
144

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

155
            default:
156
                return false;
114✔
157
        }
158
    }
231✔
159

160
    void ASTLowerer::warning(const std::string& message, const Node& node)
×
161
    {
×
NEW
162
        m_logger.warn("{}", Diagnostics::makeContextWithNode(message, node));
×
163
    }
×
164

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

170
    void ASTLowerer::makeError(const ErrorKind kind, const Node& node, const std::string& additional_ctx)
8✔
171
    {
8✔
172
        const std::string invalid_node_msg = "The given node doesn't return a value, and thus can't be used as an expression.";
8✔
173

174
        switch (kind)
8✔
175
        {
3✔
176
            case ErrorKind::InvalidNodeMacro:
177
                buildAndThrowError(fmt::format("Invalid node ; if it was computed by a macro, check that a node is returned"), node);
6✔
178
                break;
179

180
            case ErrorKind::InvalidNodeNoReturnValue:
181
                buildAndThrowError(fmt::format("Invalid node inside call to `{}'. {}", additional_ctx, invalid_node_msg), node);
4✔
182
                break;
183

184
            case ErrorKind::InvalidNodeInTailCallNoReturnValue:
185
                buildAndThrowError(fmt::format("Invalid node inside tail call to `{}'. {}", additional_ctx, invalid_node_msg), node);
2✔
186
                break;
187

188
            case ErrorKind::InvalidNodeInOperatorNoReturnValue:
189
                buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'. {}", additional_ctx, invalid_node_msg), node);
1✔
190
                break;
191
        }
×
192
    }
16✔
193

194
    void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
108,819✔
195
    {
108,819✔
196
        // register symbols
197
        if (x.nodeType() == NodeType::Symbol)
108,819✔
198
            compileSymbol(x, p, is_result_unused, /* can_use_ref= */ true);
35,291✔
199
        else if (x.nodeType() == NodeType::Field)
73,528✔
200
        {
201
            // the parser guarantees us that there is at least 2 elements (eg: a.b)
202
            compileSymbol(x.list()[0], p, is_result_unused, /* can_use_ref= */ true);
1,817✔
203
            for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
3,640✔
204
            {
205
                uint16_t i = addSymbol(*it);
1,823✔
206
                page(p).emplace_back(GET_FIELD, i);
1,823✔
207
            }
1,823✔
208
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,817✔
209
        }
1,817✔
210
        // register values
211
        else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
71,711✔
212
        {
213
            uint16_t i = addValue(x);
18,021✔
214

215
            if (!is_result_unused)
18,021✔
216
                page(p).emplace_back(LOAD_CONST, i);
18,021✔
217
        }
18,021✔
218
        // namespace nodes
219
        else if (x.nodeType() == NodeType::Namespace)
53,690✔
220
            compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
136✔
221
        else if (x.nodeType() == NodeType::List)
53,554✔
222
        {
223
            // empty code block should be nil
224
            if (x.constList().empty())
53,549✔
225
            {
226
                if (!is_result_unused)
×
227
                {
228
                    static const std::optional<uint16_t> nil = getBuiltin("nil");
103✔
229
                    page(p).emplace_back(BUILTIN, nil.value());
×
230
                }
×
231
            }
×
232
            // list instructions
233
            else if (const auto head = x.constList()[0]; head.nodeType() == NodeType::Symbol && getListInstruction(head.string()).has_value())
107,098✔
234
                compileListInstruction(x, p, is_result_unused);
3,816✔
235
            else if (head.nodeType() == NodeType::Symbol && head.string() == Language::Apply)
49,733✔
236
                compileApplyInstruction(x, p, is_result_unused);
189✔
237
            // registering structures
238
            else if (head.nodeType() == NodeType::Keyword)
49,544✔
239
            {
240
                switch (const Keyword keyword = head.keyword())
26,107✔
241
                {
3,013✔
242
                    case Keyword::If:
243
                        compileIf(x, p, is_result_unused, is_terminal);
3,013✔
244
                        break;
15,726✔
245

246
                    case Keyword::Set:
247
                        [[fallthrough]];
248
                    case Keyword::Let:
249
                        [[fallthrough]];
250
                    case Keyword::Mut:
251
                        compileLetMutSet(keyword, x, p);
12,715✔
252
                        break;
16,517✔
253

254
                    case Keyword::Fun:
255
                        compileFunction(x, p, is_result_unused);
3,809✔
256
                        break;
9,086✔
257

258
                    case Keyword::Begin:
259
                    {
260
                        for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
26,116✔
261
                            compileExpression(
41,668✔
262
                                x.list()[i],
20,834✔
263
                                p,
20,834✔
264
                                // All the nodes in a 'begin' (except for the last one) are producing a result that we want to drop.
265
                                /* is_result_unused= */ (i != size - 1) || is_result_unused,
20,834✔
266
                                // If the 'begin' is a terminal node, only its last node is terminal.
267
                                /* is_terminal= */ is_terminal && (i == size - 1));
20,834✔
268
                        break;
5,237✔
269
                    }
1,284✔
270

271
                    case Keyword::While:
272
                        compileWhile(x, p);
1,284✔
273
                        break;
1,285✔
274

275
                    case Keyword::Import:
276
                        compilePluginImport(x, p);
2✔
277
                        break;
4✔
278

279
                    case Keyword::Del:
280
                        page(p).emplace_back(DEL, addSymbol(x.constList()[1]));
2✔
281
                        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
282
                        break;
2✔
283
                }
26,107✔
284
            }
26,047✔
285
            else
286
            {
287
                // If we are here, we should have a function name via the m_opened_vars.
288
                // Push arguments first, then function name, then call it.
289
                handleCalls(x, p, is_result_unused, is_terminal);
23,437✔
290
            }
291
        }
53,446✔
292
        else if (x.nodeType() != NodeType::Unused)
5✔
293
            buildAndThrowError(
×
294
                fmt::format(
×
295
                    "NodeType `{}' not handled in ASTLowerer::compileExpression. Please fill an issue on GitHub: https://github.com/ArkScript-lang/Ark",
×
296
                    typeToString(x)),
×
297
                x);
×
298
    }
108,819✔
299

300
    void ASTLowerer::compileSymbol(const Node& x, const Page p, const bool is_result_unused, const bool can_use_ref)
37,193✔
301
    {
37,193✔
302
        const std::string& name = x.string();
37,193✔
303

304
        if (const auto it_builtin = getBuiltin(name))
74,386✔
305
            page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
3,773✔
306
        else if (getOperator(name).has_value())
33,420✔
307
            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✔
308
        else
309
        {
310
            if (can_use_ref)
33,418✔
311
            {
312
                const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
33,333✔
313
                if (maybe_local_idx.has_value())
33,333✔
314
                    page(p).emplace_back(LOAD_FAST_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
12,517✔
315
                else
316
                    page(p).emplace_back(LOAD_FAST, addSymbol(x));
20,816✔
317
            }
33,333✔
318
            else
319
                page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
85✔
320
        }
321

322
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
37,191✔
323

324
        if (is_result_unused)
37,191✔
325
        {
326
            warning("Statement has no effect", x);
×
327
            page(p).emplace_back(POP);
×
328
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
×
329
        }
×
330
    }
37,193✔
331

332
    void ASTLowerer::compileListInstruction(Node& x, const Page p, const bool is_result_unused)
3,816✔
333
    {
3,816✔
334
        const Node head = x.constList()[0];
3,816✔
335
        std::string name = x.constList()[0].string();
3,816✔
336
        Instruction inst = getListInstruction(name).value();
3,816✔
337

338
        // length of at least 1 since we got a symbol name
339
        const auto argc = x.constList().size() - 1u;
3,816✔
340
        // error, can not use append/concat/pop (and their in place versions) with a <2 length argument list
341
        if (argc < 2 && APPEND <= inst && inst <= POP)
3,816✔
342
            buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), head);
6✔
343
        if (inst <= POP && std::cmp_greater(argc, MaxValue16Bits))
3,810✔
344
            buildAndThrowError(fmt::format("Too many arguments ({}), exceeds {}", argc, MaxValue16Bits), x);
1✔
345
        if (argc != 3 && inst == SET_AT_INDEX)
3,809✔
346
            buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), head);
1✔
347
        if (argc != 4 && inst == SET_AT_2_INDEX)
3,808✔
348
            buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), head);
1✔
349

350
        // compile arguments in reverse order
351
        for (std::size_t i = x.constList().size() - 1u; i > 0; --i)
12,551✔
352
        {
353
            Node& node = x.list()[i];
8,744✔
354
            if (nodeProducesOutput(node))
8,744✔
355
                compileExpression(node, p, false, false);
8,743✔
356
            else
357
                makeError(ErrorKind::InvalidNodeNoReturnValue, node, name);
1✔
358
        }
8,744✔
359

360
        // put inst and number of arguments
361
        std::size_t inst_argc = 0;
3,806✔
362
        switch (inst)
3,806✔
363
        {
2,850✔
364
            case LIST:
365
                inst_argc = argc;
2,850✔
366
                break;
3,662✔
367

368
            case APPEND:
369
            case APPEND_IN_PLACE:
370
            case CONCAT:
371
            case CONCAT_IN_PLACE:
372
                inst_argc = argc - 1;
812✔
373
                break;
827✔
374

375
            case POP_LIST:
376
            case POP_LIST_IN_PLACE:
377
                inst_argc = 0;
15✔
378
                break;
144✔
379

380
            default:
381
                break;
129✔
382
        }
3,806✔
383
        page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
3,806✔
384
        page(p).back().setSourceLocation(head.filename(), head.position().start.line);
3,806✔
385

386
        if (is_result_unused && name.back() != '!' && inst <= POP_LIST_IN_PLACE)  // in-place functions never push a value
3,806✔
387
        {
388
            warning("Ignoring return value of function", x);
×
389
            page(p).emplace_back(POP);
×
390
        }
×
391
    }
3,825✔
392

393
    void ASTLowerer::compileApplyInstruction(Node& x, const Page p, const bool is_result_unused)
189✔
394
    {
189✔
395
        const Node head = x.constList()[0];
189✔
396
        const auto argc = x.constList().size() - 1u;
189✔
397

398
        if (argc != 2)
189✔
399
            buildAndThrowError(fmt::format("Expected 2 arguments (function, arguments) for apply, got {}", argc), head);
1✔
400

401
        const auto label_return = IR::Entity::Label(m_current_label++);
188✔
402
        page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
188✔
403

404
        for (Node& node : x.list() | std::ranges::views::drop(1))
564✔
405
        {
406
            if (nodeProducesOutput(node))
376✔
407
                compileExpression(node, p, false, false);
375✔
408
            else
409
                makeError(ErrorKind::InvalidNodeNoReturnValue, node, "apply");
1✔
410
        }
376✔
411
        page(p).emplace_back(APPLY);
187✔
412
        // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
413
        page(p).emplace_back(label_return);
187✔
414

415
        if (is_result_unused)
187✔
416
            page(p).emplace_back(POP);
×
417
    }
191✔
418

419
    void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
3,013✔
420
    {
3,013✔
421
        if (x.constList().size() == 1)
3,013✔
422
            buildAndThrowError("Invalid condition: missing 'cond' and 'then' nodes, expected (if cond then)", x);
2✔
423
        if (x.constList().size() == 2)
3,012✔
424
            buildAndThrowError(fmt::format("Invalid condition: missing 'then' node, expected (if {} then)", x.constList()[1].repr()), x);
1✔
425

426
        // compile condition
427
        compileExpression(x.list()[1], p, false, false);
3,011✔
428
        page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
3,011✔
429

430
        // jump only if needed to the "true" branch
431
        const auto label_then = IR::Entity::Label(m_current_label++);
3,011✔
432
        page(p).emplace_back(IR::Entity::GotoIf(label_then, true));
3,011✔
433

434
        // "false" branch code
435
        if (x.constList().size() == 4)  // we have an else clause
3,011✔
436
        {
437
            m_locals_locator.saveScopeLengthForBranch();
2,298✔
438
            compileExpression(x.list()[3], p, is_result_unused, is_terminal);
2,298✔
439
            page(p).back().setSourceLocation(x.constList()[3].filename(), x.constList()[3].position().start.line);
2,298✔
440
            m_locals_locator.dropVarsForBranch();
2,298✔
441
        }
2,298✔
442

443
        // when else is finished, jump to end
444
        const auto label_end = IR::Entity::Label(m_current_label++);
3,011✔
445
        page(p).emplace_back(IR::Entity::Goto(label_end));
3,011✔
446

447
        // absolute address to jump to if condition is true
448
        page(p).emplace_back(label_then);
3,011✔
449
        // if code
450
        m_locals_locator.saveScopeLengthForBranch();
3,011✔
451
        compileExpression(x.list()[2], p, is_result_unused, is_terminal);
3,011✔
452
        page(p).back().setSourceLocation(x.constList()[2].filename(), x.constList()[2].position().start.line);
3,011✔
453
        m_locals_locator.dropVarsForBranch();
3,011✔
454
        // set jump to end pos
455
        page(p).emplace_back(label_end);
3,011✔
456
    }
3,013✔
457

458
    void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused)
3,809✔
459
    {
3,809✔
460
        if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List)
3,811✔
461
            buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args);
1✔
462
        if (x.constList().size() != 3)
3,808✔
463
            makeError(ErrorKind::InvalidNodeMacro, x, "");
1✔
464

465
        // capture, if needed
466
        std::size_t capture_inst_count = 0;
3,807✔
467
        for (const auto& node : x.constList()[1].constList())
9,902✔
468
        {
469
            if (node.nodeType() == NodeType::Capture)
6,095✔
470
            {
471
                const uint16_t symbol_id = addSymbol(node);
227✔
472

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

479
                    page(p).emplace_back(RENAME_NEXT_CAPTURE, nqn_id);
14✔
480
                    page(p).emplace_back(CAPTURE, symbol_id);
14✔
481
                }
14✔
482
                else
483
                    page(p).emplace_back(CAPTURE, symbol_id);
213✔
484

485
                ++capture_inst_count;
227✔
486
            }
227✔
487
        }
6,095✔
488
        const bool is_closure = capture_inst_count > 0;
3,807✔
489

490
        m_locals_locator.createScope(
7,614✔
491
            is_closure
3,807✔
492
                ? LocalsLocator::ScopeType::Closure
493
                : LocalsLocator::ScopeType::Function);
494

495
        // create new page for function body
496
        const auto function_body_page = createNewCodePage();
3,807✔
497
        // save page_id into the constants table as PageAddr and load the const
498
        page(p).emplace_back(is_closure ? MAKE_CLOSURE : LOAD_CONST, addValue(function_body_page.index, x));
3,807✔
499

500
        // pushing arguments from the stack into variables in the new scope
501
        for (const auto& node : x.constList()[1].constList())
9,902✔
502
        {
503
            if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg)
6,095✔
504
            {
505
                page(function_body_page).emplace_back(STORE, addSymbol(node));
5,013✔
506
                m_locals_locator.addLocal(node.string());
5,013✔
507
            }
5,013✔
508
            else if (node.nodeType() == NodeType::RefArg)
1,082✔
509
            {
510
                page(function_body_page).emplace_back(STORE_REF, addSymbol(node));
855✔
511
                m_locals_locator.addLocal(node.string());
855✔
512
            }
855✔
513
        }
6,095✔
514

515
        // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls.
516
        // This way we can continue to safely apply optimisations on
517
        // (let name (fun (e) (map lst (fun (e) (name e)))))
518
        // Otherwise, `name` would have been optimized to a GET_CURRENT_PAGE_ADDRESS, which would have returned the wrong page.
519
        if (x.isAnonymousFunction())
3,807✔
520
            m_opened_vars.emplace("#anonymous");
402✔
521
        // push body of the function
522
        compileExpression(x.list()[2], function_body_page, false, true);
3,807✔
523
        if (x.isAnonymousFunction())
3,807✔
524
            m_opened_vars.pop();
402✔
525

526
        // return last value on the stack
527
        page(function_body_page).emplace_back(RET);
3,807✔
528
        m_locals_locator.deleteScope();
3,807✔
529

530
        // if the computed function is unused, pop it
531
        if (is_result_unused)
3,807✔
532
        {
533
            warning("Unused declared function", x);
×
534
            page(p).emplace_back(POP);
×
535
        }
×
536
    }
3,809✔
537

538
    void ASTLowerer::compileLetMutSet(const Keyword n, Node& x, const Page p)
12,715✔
539
    {
12,715✔
540
        if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol)
12,722✔
541
            buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym);
×
542
        if (x.constList().size() != 3)
12,715✔
543
            makeError(ErrorKind::InvalidNodeMacro, x, "");
1✔
544

545
        const std::string name = x.constList()[1].string();
12,714✔
546
        uint16_t i = addSymbol(x.constList()[1]);
12,714✔
547

548
        if (!m_opened_vars.empty() && m_opened_vars.top() == name)
12,714✔
549
            buildAndThrowError("Can not define a variable using the same name as the function it is defined inside. You need to rename the function or the variable", x);
1✔
550

551
        const bool is_function = x.constList()[2].isFunction();
12,713✔
552
        if (is_function)
12,713✔
553
        {
554
            m_opened_vars.push(name);
3,407✔
555
            x.list()[2].setFunctionKind(/* anonymous= */ false);
3,407✔
556
        }
3,407✔
557

558
        // put value before symbol id
559
        // starting at index = 2 because x is a (let|mut|set variable ...) node
560
        for (std::size_t idx = 2, end = x.constList().size(); idx < end; ++idx)
25,426✔
561
            compileExpression(x.list()[idx], p, false, false);
12,713✔
562

563
        if (n == Keyword::Let || n == Keyword::Mut)
12,708✔
564
        {
565
            page(p).emplace_back(STORE, i);
8,151✔
566
            m_locals_locator.addLocal(name);
8,151✔
567
        }
8,151✔
568
        else
569
            page(p).emplace_back(SET_VAL, i);
4,557✔
570

571
        if (is_function)
12,708✔
572
            m_opened_vars.pop();
3,402✔
573
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
12,708✔
574
    }
12,716✔
575

576
    void ASTLowerer::compileWhile(Node& x, const Page p)
1,284✔
577
    {
1,284✔
578
        if (x.constList().size() != 3)
1,284✔
579
            makeError(ErrorKind::InvalidNodeMacro, x, "");
1✔
580

581
        m_locals_locator.createScope();
1,283✔
582
        page(p).emplace_back(CREATE_SCOPE);
1,283✔
583
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,283✔
584

585
        // save current position to jump there at the end of the loop
586
        const auto label_loop = IR::Entity::Label(m_current_label++);
1,283✔
587
        page(p).emplace_back(label_loop);
1,283✔
588
        // push condition
589
        compileExpression(x.list()[1], p, false, false);
1,283✔
590
        // absolute jump to end of block if condition is false
591
        const auto label_end = IR::Entity::Label(m_current_label++);
1,283✔
592
        page(p).emplace_back(IR::Entity::GotoIf(label_end, false));
1,283✔
593
        // push code to page
594
        compileExpression(x.list()[2], p, true, false);
1,283✔
595

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

602
        // absolute address to jump to if condition is false
603
        page(p).emplace_back(label_end);
1,283✔
604

605
        page(p).emplace_back(POP_SCOPE);
1,283✔
606
        m_locals_locator.deleteScope();
1,283✔
607
    }
1,284✔
608

609
    void ASTLowerer::compilePluginImport(const Node& x, const Page p)
2✔
610
    {
2✔
611
        std::string path;
2✔
612
        const Node package_node = x.constList()[1];
2✔
613
        for (std::size_t i = 0, end = package_node.constList().size(); i < end; ++i)
4✔
614
        {
615
            path += package_node.constList()[i].string();
2✔
616
            if (i + 1 != end)
2✔
617
                path += "/";
×
618
        }
2✔
619
        path += ".arkm";
2✔
620

621
        // register plugin path in the constants table
622
        uint16_t id = addValue(Node(NodeType::String, path));
2✔
623
        // add plugin instruction + id of the constant referring to the plugin path
624
        page(p).emplace_back(PLUGIN, id);
2✔
625
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
626
    }
2✔
627

628
    void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call)
8,756✔
629
    {
8,756✔
630
        const auto node = call.constList()[0];
8,756✔
631

632
        // push the arguments in reverse order because the function loads its arguments in the order they are defined:
633
        // (fun (a b c) ...) -> load 'a', then 'b', then 'c'
634
        // We have to push arguments in this order and load them in reverse, because we are using references internally,
635
        // which can cause problems for recursive functions that swap their arguments around.
636
        // Eg (let foo (fun (a b c) (if (> a 0) (foo (- a 1) c (+ b c)) 1))) (foo 12 0 1)
637
        // 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
638
        // value for argument b, but loaded it as a reference.
639
        for (Node& value : std::ranges::drop_view(call.list(), 1) | std::views::reverse)
24,275✔
640
        {
641
            // FIXME: in (foo a b (breakpoint (< c 0)) c), we will push c before the breakpoint
642
            if (nodeProducesOutput(value) || isBreakpoint(value))
15,519✔
643
            {
644
                // 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
645
                if (value.nodeType() == NodeType::Symbol && is_tail_call)
15,517✔
646
                    compileSymbol(value, p, false, /* can_use_ref= */ false);
85✔
647
                else
648
                    compileExpression(value, p, false, false);
15,432✔
649
            }
15,510✔
650
            else
651
                makeError(is_tail_call ? ErrorKind::InvalidNodeInTailCallNoReturnValue : ErrorKind::InvalidNodeNoReturnValue, value, node.repr());
2✔
652
        }
15,519✔
653
    }
8,765✔
654

655
    void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal)
23,406✔
656
    {
23,406✔
657
        const Node& node = x.constList()[0];
23,406✔
658
        bool matched = false;
23,406✔
659

660
        if (node.nodeType() == NodeType::Symbol)
23,406✔
661
        {
662
            if (node.string() == Language::And || node.string() == Language::Or)
22,679✔
663
            {
664
                matched = true;
512✔
665
                handleShortcircuit(x, p);
512✔
666
            }
512✔
667
            if (const auto maybe_operator = getOperator(node.string()); maybe_operator.has_value())
36,844✔
668
            {
669
                matched = true;
14,165✔
670
                if (maybe_operator.value() == BREAKPOINT)
14,165✔
671
                    is_result_unused = false;
17✔
672
                handleOperator(x, p, maybe_operator.value());
14,165✔
673
            }
14,165✔
674
        }
22,679✔
675

676
        if (!matched)
23,406✔
677
        {
678
            // if nothing else matched, then compile a function call
679
            if (handleFunctionCall(x, p, is_terminal))
8,747✔
680
                // if it returned true, we compiled a tail call, skip the POP at the end
681
                return;
113✔
682
        }
8,634✔
683

684
        if (is_result_unused)
23,293✔
685
            page(p).emplace_back(POP);
3,517✔
686
    }
23,406✔
687

688
    void ASTLowerer::handleShortcircuit(Node& x, const Page p)
512✔
689
    {
512✔
690
        const Node& node = x.constList()[0];
512✔
691
        const auto name = node.string();  // and / or
512✔
692
        const Instruction inst = name == Language::And ? SHORTCIRCUIT_AND : SHORTCIRCUIT_OR;
512✔
693

694
        // short circuit implementation
695
        if (x.constList().size() < 3)
512✔
696
            buildAndThrowError(
2✔
697
                fmt::format(
4✔
698
                    "Expected at least 2 arguments while compiling '{}', got {}",
2✔
699
                    name,
700
                    x.constList().size() - 1),
2✔
701
                x);
2✔
702

703
        if (!nodeProducesOutput(x.list()[1]))
510✔
704
            buildAndThrowError(
1✔
705
                fmt::format(
2✔
706
                    "Can not use `{}' inside a `{}' expression, as it doesn't return a value",
1✔
707
                    x.list()[1].repr(), name),
1✔
708
                x.list()[1]);
1✔
709
        compileExpression(x.list()[1], p, false, false);
509✔
710

711
        const auto label_shortcircuit = IR::Entity::Label(m_current_label++);
509✔
712
        auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, inst);
509✔
713
        page(p).emplace_back(shortcircuit_entity);
509✔
714

715
        for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
1,143✔
716
        {
717
            if (!nodeProducesOutput(x.list()[i]))
634✔
718
                buildAndThrowError(
1✔
719
                    fmt::format(
2✔
720
                        "Can not use `{}' inside a `{}' expression, as it doesn't return a value",
1✔
721
                        x.list()[i].repr(), name),
1✔
722
                    x.list()[i]);
1✔
723
            compileExpression(x.list()[i], p, false, false);
633✔
724
            if (i + 1 != end)
633✔
725
                page(p).emplace_back(shortcircuit_entity);
125✔
726
        }
633✔
727

728
        page(p).emplace_back(label_shortcircuit);
508✔
729
    }
516✔
730

731
    void ASTLowerer::handleOperator(Node& x, const Page p, const Instruction op)
14,165✔
732
    {
14,165✔
733
        constexpr std::size_t start_index = 1;
14,165✔
734
        const Node& node = x.constList()[0];
14,165✔
735
        const auto op_name = Language::operators[static_cast<std::size_t>(op - FIRST_OPERATOR)];
14,165✔
736

737

738
        // push arguments on current page
739
        std::size_t exp_count = 0;
14,165✔
740
        for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
39,968✔
741
        {
742
            const bool is_breakpoint = isBreakpoint(x.constList()[index]);
25,803✔
743
            if (nodeProducesOutput(x.constList()[index]) || is_breakpoint)
25,803✔
744
                compileExpression(x.list()[index], p, false, false);
25,802✔
745
            else
746
                makeError(ErrorKind::InvalidNodeInOperatorNoReturnValue, x.constList()[index], node.repr());
1✔
747

748
            if (!is_breakpoint)
25,802✔
749
                exp_count++;
25,800✔
750

751
            // in order to be able to handle things like (op A B C D...)
752
            // which should be transformed into A B op C op D op...
753
            if (exp_count >= 2 && !isTernaryInst(op) && !is_breakpoint)
25,802✔
754
                page(p).emplace_back(op);
11,429✔
755
        }
25,803✔
756

757
        if (isBreakpoint(x))
14,164✔
758
        {
759
            if (exp_count > 1)
17✔
760
                buildAndThrowError(fmt::format("`{}' expected at most one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
761
            page(p).emplace_back(op, exp_count);
16✔
762
        }
16✔
763
        else if (isUnaryInst(op))
14,147✔
764
        {
765
            if (exp_count != 1)
2,749✔
766
                buildAndThrowError(fmt::format("`{}' expected one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
767
            page(p).emplace_back(op);
2,748✔
768
        }
2,748✔
769
        else if (isTernaryInst(op))
11,398✔
770
        {
771
            if (exp_count != 3)
107✔
772
                buildAndThrowError(fmt::format("`{}' expected three arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
773
            page(p).emplace_back(op);
106✔
774
        }
106✔
775
        else if (exp_count <= 1)
11,291✔
776
            buildAndThrowError(fmt::format("`{}' expected two arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
2✔
777

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

782
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
14,151✔
783
    }
14,165✔
784

785
    bool ASTLowerer::handleFunctionCall(Node& x, const Page p, const bool is_terminal)
8,759✔
786
    {
8,759✔
787
        constexpr std::size_t start_index = 1;
8,759✔
788
        Node& node = x.list()[0];
8,759✔
789

790
        if (is_terminal && node.nodeType() == NodeType::Symbol && isFunctionCallingItself(node.string()))
8,759✔
791
        {
792
            pushFunctionCallArguments(x, p, /* is_tail_call= */ true);
113✔
793

794
            // jump to the top of the function
795
            page(p).emplace_back(TAIL_CALL_SELF);
113✔
796
            page(p).back().setSourceLocation(node.filename(), node.position().start.line);
113✔
797
            return true;  // skip the potential Instruction::POP at the end
113✔
798
        }
799

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

803
        const auto proc_page = createNewCodePage(/* temp= */ true);
8,644✔
804

805
        // compile the function resolution to a separate page
806
        if (node.nodeType() == NodeType::Symbol && isFunctionCallingItself(node.string()))
8,644✔
807
        {
808
            // The function is trying to call itself, but this isn't a tail call.
809
            // We can skip the LOAD_FAST function_name and directly push the current
810
            // function page, which will be quicker than a local variable resolution.
811
            // We set its argument to the symbol id of the function we are calling,
812
            // so that the VM knows the name of the last called function.
813
            page(proc_page).emplace_back(GET_CURRENT_PAGE_ADDR, addSymbol(node));
57✔
814
        }
57✔
815
        else
816
        {
817
            // closure chains have been handled (eg: closure.field.field.function)
818
            compileExpression(node, proc_page, false, false);  // storing proc
8,587✔
819
        }
820

821
        if (m_temp_pages.back().empty())
8,644✔
822
            buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x);
×
823

824
        const auto label_return = IR::Entity::Label(m_current_label++);
8,644✔
825
        page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
8,644✔
826
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
8,644✔
827

828
        pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
8,644✔
829
        // push proc from temp page
830
        for (const auto& inst : m_temp_pages.back())
18,033✔
831
            page(p).push_back(inst);
9,397✔
832
        m_temp_pages.pop_back();
8,636✔
833

834
        // number of arguments
835
        std::size_t args_count = 0;
8,636✔
836
        for (auto it = x.constList().begin() + start_index, it_end = x.constList().end(); it != it_end; ++it)
23,865✔
837
        {
838
            if (it->nodeType() != NodeType::Capture && !isBreakpoint(*it))
15,229✔
839
                args_count++;
15,225✔
840
        }
15,229✔
841
        // call the procedure
842
        page(p).emplace_back(CALL, args_count);
8,636✔
843
        page(p).back().setSourceLocation(node.filename(), node.position().start.line);
8,636✔
844

845
        // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
846
        page(p).emplace_back(label_return);
8,636✔
847
        return false;  // we didn't compile a tail call
8,636✔
848
    }
8,761✔
849

850
    uint16_t ASTLowerer::addSymbol(const Node& sym)
41,606✔
851
    {
41,606✔
852
        // otherwise, add the symbol, and return its id in the table
853
        auto it = std::ranges::find(m_symbols, sym.string());
41,606✔
854
        if (it == m_symbols.end())
41,606✔
855
        {
856
            m_symbols.push_back(sym.string());
7,387✔
857
            it = m_symbols.begin() + static_cast<std::vector<std::string>::difference_type>(m_symbols.size() - 1);
7,387✔
858
        }
7,387✔
859

860
        const auto distance = std::distance(m_symbols.begin(), it);
41,606✔
861
        if (std::cmp_less(distance, MaxValue16Bits))
41,606✔
862
            return static_cast<uint16_t>(distance);
83,212✔
863
        buildAndThrowError(fmt::format("Too many symbols (exceeds {}), aborting compilation.", MaxValue16Bits), sym);
×
864
    }
41,606✔
865

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

876
        const auto distance = std::distance(m_values.begin(), it);
18,023✔
877
        if (std::cmp_less(distance, MaxValue16Bits))
18,023✔
878
            return static_cast<uint16_t>(distance);
18,023✔
879
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), x);
×
880
    }
18,023✔
881

882
    uint16_t ASTLowerer::addValue(const std::size_t page_id, const Node& current)
3,807✔
883
    {
3,807✔
884
        const ValTableElem v(page_id);
3,807✔
885
        auto it = std::ranges::find(m_values, v);
3,807✔
886
        if (it == m_values.end())
3,807✔
887
        {
888
            m_values.push_back(v);
3,807✔
889
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,807✔
890
        }
3,807✔
891

892
        const auto distance = std::distance(m_values.begin(), it);
3,807✔
893
        if (std::cmp_less(distance, MaxValue16Bits))
3,807✔
894
            return static_cast<uint16_t>(distance);
3,807✔
895
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), current);
×
896
    }
3,807✔
897
}
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