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

ArkScript-lang / Ark / 22157739607

18 Feb 2026 09:10PM UTC coverage: 93.312% (-0.2%) from 93.464%
22157739607

Pull #641

github

web-flow
Merge 832fcd44e into 672fb743f
Pull Request #641: Feat/various improvements

138 of 167 new or added lines in 7 files covered. (82.63%)

3 existing lines in 2 files now uncovered.

9223 of 9884 relevant lines covered (93.31%)

265167.15 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
#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) :
435✔
19
        m_logger("ASTLowerer", debug)
435✔
20
    {}
870✔
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)
342✔
34
    {
342✔
35
        m_logger.traceStart("process");
342✔
36
        const Page global = createNewCodePage();
342✔
37

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

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

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

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

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

70
    std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
35,757✔
71
    {
35,757✔
72
        const auto it = std::ranges::find_if(Builtins::builtins,
35,757✔
73
                                             [&name](const std::pair<std::string, Value>& element) -> bool {
3,042,493✔
74
                                                 return name == element.first;
3,006,736✔
75
                                             });
76
        if (it != Builtins::builtins.end())
35,757✔
77
            return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
3,669✔
78
        return std::nullopt;
32,088✔
79
    }
35,757✔
80

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

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

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

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

129
            default:
130
                return false;
10,578✔
131
        }
132
    }
13,263✔
133

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

141
            default:
142
                return false;
21,189✔
143
        }
144
    }
21,407✔
145

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

156
            default:
157
                return false;
62✔
158
        }
159
    }
175✔
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)
43✔
167
    {
43✔
168
        throw CodeError(message, CodeErrorContext(node.filename(), node.position()));
43✔
169
    }
43✔
170

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

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

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

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

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

195
    void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
104,262✔
196
    {
104,262✔
197
        // register symbols
198
        if (x.nodeType() == NodeType::Symbol)
104,262✔
199
            compileSymbol(x, p, is_result_unused, /* can_use_ref= */ true);
33,859✔
200
        else if (x.nodeType() == NodeType::Field)
70,403✔
201
        {
202
            // the parser guarantees us that there is at least 2 elements (eg: a.b)
203
            compileSymbol(x.list()[0], p, is_result_unused, /* can_use_ref= */ true);
1,813✔
204
            for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
3,632✔
205
            {
206
                uint16_t i = addSymbol(*it);
1,819✔
207
                page(p).emplace_back(GET_FIELD, i);
1,819✔
208
            }
1,819✔
209
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,813✔
210
        }
1,813✔
211
        // register values
212
        else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
68,590✔
213
        {
214
            uint16_t i = addValue(x);
16,969✔
215

216
            if (!is_result_unused)
16,969✔
217
                page(p).emplace_back(LOAD_CONST, i);
16,969✔
218
        }
16,969✔
219
        // namespace nodes
220
        else if (x.nodeType() == NodeType::Namespace)
51,621✔
221
            compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
135✔
222
        else if (x.nodeType() == NodeType::List)
51,486✔
223
        {
224
            // empty code block should be nil
225
            if (x.constList().empty())
51,481✔
226
            {
227
                if (!is_result_unused)
×
228
                {
229
                    static const std::optional<uint16_t> nil = getBuiltin("nil");
103✔
230
                    page(p).emplace_back(BUILTIN, nil.value());
×
231
                }
×
232
            }
×
233
            // list instructions
234
            else if (const auto head = x.constList()[0]; head.nodeType() == NodeType::Symbol && getListInstruction(head.string()).has_value())
102,962✔
235
                compileListInstruction(x, p, is_result_unused);
3,673✔
236
            else if (head.nodeType() == NodeType::Symbol && head.string() == Language::Apply)
47,808✔
237
                compileApplyInstruction(x, p, is_result_unused);
189✔
238
            // registering structures
239
            else if (head.nodeType() == NodeType::Keyword)
47,619✔
240
            {
241
                switch (const Keyword keyword = head.keyword())
25,355✔
242
                {
2,909✔
243
                    case Keyword::If:
244
                        compileIf(x, p, is_result_unused, is_terminal);
2,909✔
245
                        break;
15,246✔
246

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

255
                    case Keyword::Fun:
256
                        compileFunction(x, p, is_result_unused);
3,762✔
257
                        break;
8,879✔
258

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

272
                    case Keyword::While:
273
                        compileWhile(x, p);
1,219✔
274
                        break;
1,220✔
275

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

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

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

305
        if (const auto it_builtin = getBuiltin(name))
71,514✔
306
            page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
3,669✔
307
        else if (getOperator(name).has_value())
32,088✔
308
            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✔
309
        else
310
        {
311
            if (can_use_ref)
32,086✔
312
            {
313
                const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
32,001✔
314
                if (maybe_local_idx.has_value())
32,001✔
315
                    page(p).emplace_back(LOAD_FAST_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
12,074✔
316
                else
317
                    page(p).emplace_back(LOAD_FAST, addSymbol(x));
19,927✔
318
            }
32,001✔
319
            else
320
                page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
85✔
321
        }
322

323
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
35,755✔
324

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

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

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

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

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

369
            case APPEND:
370
            case APPEND_IN_PLACE:
371
            case CONCAT:
372
            case CONCAT_IN_PLACE:
373
                inst_argc = argc - 1;
810✔
374
                break;
825✔
375

376
            case POP_LIST:
377
            case POP_LIST_IN_PLACE:
378
                inst_argc = 0;
15✔
379
                break;
105✔
380

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

486
                ++capture_inst_count;
227✔
487
            }
227✔
488
        }
6,007✔
489
        const bool is_closure = capture_inst_count > 0;
3,760✔
490

491
        m_locals_locator.createScope(
7,520✔
492
            is_closure
3,760✔
493
                ? LocalsLocator::ScopeType::Closure
494
                : LocalsLocator::ScopeType::Function);
495

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

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

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

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

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

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

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

549
        if (!m_opened_vars.empty() && m_opened_vars.top() == name)
12,338✔
550
            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✔
551

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

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

564
        if (n == Keyword::Let || n == Keyword::Mut)
12,332✔
565
        {
566
            page(p).emplace_back(STORE, i);
7,967✔
567
            m_locals_locator.addLocal(name);
7,967✔
568
        }
7,967✔
569
        else
570
            page(p).emplace_back(SET_VAL, i);
4,365✔
571

572
        if (is_function)
12,332✔
573
            m_opened_vars.pop();
3,355✔
574
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
12,332✔
575
    }
12,340✔
576

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

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

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

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

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

606
        page(p).emplace_back(POP_SCOPE);
1,218✔
607
        m_locals_locator.deleteScope();
1,218✔
608
    }
1,219✔
609

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

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

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

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

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

661
        if (node.nodeType() == NodeType::Symbol)
22,233✔
662
        {
663
            if (node.string() == Language::And || node.string() == Language::Or)
21,509✔
664
            {
665
                matched = true;
478✔
666
                handleShortcircuit(x, p);
478✔
667
            }
478✔
668
            if (const auto maybe_operator = getOperator(node.string()); maybe_operator.has_value())
34,788✔
669
            {
670
                matched = true;
13,279✔
671
                if (maybe_operator.value() == BREAKPOINT)
13,279✔
672
                    is_result_unused = false;
15✔
673
                handleOperator(x, p, maybe_operator.value());
13,279✔
674
            }
13,279✔
675
        }
21,509✔
676

677
        if (!matched)
22,233✔
678
        {
679
            // if nothing else matched, then compile a function call
680
            if (handleFunctionCall(x, p, is_terminal))
8,494✔
681
                // if it returned true, we compiled a tail call, skip the POP at the end
682
                return;
112✔
683
        }
8,382✔
684

685
        if (is_result_unused)
22,121✔
686
            page(p).emplace_back(POP);
3,408✔
687
    }
22,233✔
688

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

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

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

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

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

729
        page(p).emplace_back(label_shortcircuit);
474✔
730
    }
482✔
731

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

738

739
        // push arguments on current page
740
        std::size_t exp_count = 0;
13,279✔
741
        for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
37,322✔
742
        {
743
            const bool is_breakpoint = isBreakpoint(x.constList()[index]);
24,043✔
744
            if (nodeProducesOutput(x.constList()[index]) || is_breakpoint)
24,043✔
745
                compileExpression(x.list()[index], p, false, false);
24,042✔
746
            else
747
                makeError(ErrorKind::InvalidNodeInOperatorNoReturnValue, x.constList()[index], node.repr());
1✔
748

749
            if (!is_breakpoint)
24,042✔
750
                exp_count++;
24,040✔
751

752
            // in order to be able to handle things like (op A B C D...)
753
            // which should be transformed into A B op C op D op...
754
            if (exp_count >= 2 && !isTernaryInst(op) && !is_breakpoint)
24,042✔
755
                page(p).emplace_back(op);
10,657✔
756
        }
24,043✔
757

758
        if (isBreakpoint(x))
13,278✔
759
        {
760
            if (exp_count > 1)
15✔
761
                buildAndThrowError(fmt::format("`{}' expected at most one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
762
            page(p).emplace_back(op, exp_count);
14✔
763
        }
14✔
764
        else if (isUnaryInst(op))
13,263✔
765
        {
766
            if (exp_count != 1)
2,685✔
767
                buildAndThrowError(fmt::format("`{}' expected one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
768
            page(p).emplace_back(op);
2,684✔
769
        }
2,684✔
770
        else if (isTernaryInst(op))
10,578✔
771
        {
772
            if (exp_count != 3)
55✔
773
                buildAndThrowError(fmt::format("`{}' expected three arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
774
            page(p).emplace_back(op);
54✔
775
        }
54✔
776
        else if (exp_count <= 1)
10,523✔
777
            buildAndThrowError(fmt::format("`{}' expected two arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
2✔
778

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

783
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
13,265✔
784
    }
13,279✔
785

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

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

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

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

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

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

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

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

829
        pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
8,392✔
830
        // push proc from temp page
831
        for (const auto& inst : m_temp_pages.back())
17,526✔
832
            page(p).push_back(inst);
9,142✔
833
        m_temp_pages.pop_back();
8,384✔
834

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

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

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

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

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

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

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

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