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

ArkScript-lang / Ark / 22281039592

22 Feb 2026 04:37PM UTC coverage: 93.5% (+0.04%) from 93.464%
22281039592

Pull #641

github

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

180 of 187 new or added lines in 10 files covered. (96.26%)

8 existing lines in 2 files now uncovered.

9250 of 9893 relevant lines covered (93.5%)

267283.45 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) :
440✔
19
        m_logger("ASTLowerer", debug)
440✔
20
    {}
880✔
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)
347✔
34
    {
347✔
35
        m_logger.traceStart("process");
347✔
36
        const Page global = createNewCodePage();
347✔
37

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

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

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

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

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

70
    std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
36,178✔
71
    {
36,178✔
72
        const auto it = std::ranges::find_if(Builtins::builtins,
36,178✔
73
                                             [&name](const std::pair<std::string, Value>& element) -> bool {
3,079,970✔
74
                                                 return name == element.first;
3,043,792✔
75
                                             });
76
        if (it != Builtins::builtins.end())
36,178✔
77
            return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
3,652✔
78
        return std::nullopt;
32,526✔
79
    }
36,178✔
80

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

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

96
    bool ASTLowerer::nodeProducesOutput(const Node& node)
58,130✔
97
    {
58,130✔
98
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
58,130✔
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]) &&
534✔
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)
58,128✔
109
            return std::ranges::find(Language::UpdateRef, node.constList().front().string()) == Language::UpdateRef.end() &&
19,271✔
110
                node.constList().front().string() != "breakpoint";
9,635✔
111
        return true;  // any other node, function call, symbol, number...
48,052✔
112
    }
58,130✔
113

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

129
            default:
130
                return false;
10,709✔
131
        }
132
    }
13,432✔
133

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

141
            default:
142
                return false;
21,455✔
143
        }
144
    }
21,673✔
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)
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)
105,552✔
196
    {
105,552✔
197
        // register symbols
198
        if (x.nodeType() == NodeType::Symbol)
105,552✔
199
            compileSymbol(x, p, is_result_unused, /* can_use_ref= */ true);
34,280✔
200
        else if (x.nodeType() == NodeType::Field)
71,272✔
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)
69,459✔
213
        {
214
            uint16_t i = addValue(x);
17,235✔
215

216
            if (!is_result_unused)
17,235✔
217
                page(p).emplace_back(LOAD_CONST, i);
17,235✔
218
        }
17,235✔
219
        // namespace nodes
220
        else if (x.nodeType() == NodeType::Namespace)
52,224✔
221
            compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
136✔
222
        else if (x.nodeType() == NodeType::List)
52,088✔
223
        {
224
            // empty code block should be nil
225
            if (x.constList().empty())
52,083✔
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())
104,166✔
235
                compileListInstruction(x, p, is_result_unused);
3,716✔
236
            else if (head.nodeType() == NodeType::Symbol && head.string() == Language::Apply)
48,367✔
237
                compileApplyInstruction(x, p, is_result_unused);
189✔
238
            // registering structures
239
            else if (head.nodeType() == NodeType::Keyword)
48,178✔
240
            {
241
                switch (const Keyword keyword = head.keyword())
25,645✔
242
                {
2,948✔
243
                    case Keyword::If:
244
                        compileIf(x, p, is_result_unused, is_terminal);
2,948✔
245
                        break;
15,438✔
246

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

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

259
                    case Keyword::Begin:
260
                    {
261
                        for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
25,582✔
262
                            compileExpression(
40,816✔
263
                                x.list()[i],
20,408✔
264
                                p,
20,408✔
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,408✔
267
                                // If the 'begin' is a terminal node, only its last node is terminal.
268
                                /* is_terminal= */ is_terminal && (i == size - 1));
20,408✔
269
                        break;
5,129✔
270
                    }
1,232✔
271

272
                    case Keyword::While:
273
                        compileWhile(x, p);
1,232✔
274
                        break;
1,233✔
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,645✔
285
            }
25,585✔
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,533✔
291
            }
292
        }
51,980✔
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
    }
105,552✔
300

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

305
        if (const auto it_builtin = getBuiltin(name))
72,356✔
306
            page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
3,652✔
307
        else if (getOperator(name).has_value())
32,526✔
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,524✔
312
            {
313
                const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
32,439✔
314
                if (maybe_local_idx.has_value())
32,439✔
315
                    page(p).emplace_back(LOAD_FAST_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
12,224✔
316
                else
317
                    page(p).emplace_back(LOAD_FAST, addSymbol(x));
20,215✔
318
            }
32,439✔
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);
36,176✔
324

325
        if (is_result_unused)
36,176✔
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
    }
36,178✔
332

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

339
        // length of at least 1 since we got a symbol name
340
        const auto argc = x.constList().size() - 1u;
3,716✔
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,716✔
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,710✔
345
            buildAndThrowError(fmt::format("Too many arguments ({}), exceeds {}", argc, MaxValue16Bits), x);
1✔
346
        if (argc != 3 && inst == SET_AT_INDEX)
3,709✔
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,708✔
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,183✔
353
        {
354
            Node& node = x.list()[i];
8,476✔
355
            if (nodeProducesOutput(node))
8,476✔
356
                compileExpression(node, p, false, false);
8,475✔
357
            else
358
                makeError(ErrorKind::InvalidNodeNoReturnValue, node, name);
1✔
359
        }
8,476✔
360

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

369
            case APPEND:
370
            case APPEND_IN_PLACE:
371
            case CONCAT:
372
            case CONCAT_IN_PLACE:
373
                inst_argc = argc - 1;
812✔
374
                break;
827✔
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,706✔
384
        page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
3,706✔
385
        page(p).back().setSourceLocation(head.filename(), head.position().start.line);
3,706✔
386

387
        if (is_result_unused && name.back() != '!' && inst <= POP_LIST_IN_PLACE)  // in-place functions never push a value
3,706✔
388
        {
389
            warning("Ignoring return value of function", x);
×
390
            page(p).emplace_back(POP);
×
391
        }
×
392
    }
3,725✔
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,948✔
421
    {
2,948✔
422
        if (x.constList().size() == 1)
2,948✔
423
            buildAndThrowError("Invalid condition: missing 'cond' and 'then' nodes, expected (if cond then)", x);
2✔
424
        if (x.constList().size() == 2)
2,947✔
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,946✔
429
        page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
2,946✔
430

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

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

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

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

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

466
        // capture, if needed
467
        std::size_t capture_inst_count = 0;
3,793✔
468
        for (const auto& node : x.constList()[1].constList())
9,860✔
469
        {
470
            if (node.nodeType() == NodeType::Capture)
6,067✔
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,067✔
489
        const bool is_closure = capture_inst_count > 0;
3,793✔
490

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

496
        // create new page for function body
497
        const auto function_body_page = createNewCodePage();
3,793✔
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,793✔
500

501
        // pushing arguments from the stack into variables in the new scope
502
        for (const auto& node : x.constList()[1].constList())
9,860✔
503
        {
504
            if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg)
6,067✔
505
            {
506
                page(function_body_page).emplace_back(STORE, addSymbol(node));
4,985✔
507
                m_locals_locator.addLocal(node.string());
4,985✔
508
            }
4,985✔
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,067✔
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,793✔
521
            m_opened_vars.emplace("#anonymous");
402✔
522
        // push body of the function
523
        compileExpression(x.list()[2], function_body_page, false, true);
3,793✔
524
        if (x.isAnonymousFunction())
3,793✔
525
            m_opened_vars.pop();
402✔
526

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

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

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

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

549
        if (!m_opened_vars.empty() && m_opened_vars.top() == name)
12,491✔
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,490✔
553
        if (is_function)
12,490✔
554
        {
555
            m_opened_vars.push(name);
3,393✔
556
            x.list()[2].setFunctionKind(/* anonymous= */ false);
3,393✔
557
        }
3,393✔
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,980✔
562
            compileExpression(x.list()[idx], p, false, false);
12,490✔
563

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

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

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

582
        m_locals_locator.createScope();
1,231✔
583
        page(p).emplace_back(CREATE_SCOPE);
1,231✔
584
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,231✔
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,231✔
588
        page(p).emplace_back(label_loop);
1,231✔
589
        // push condition
590
        compileExpression(x.list()[1], p, false, false);
1,231✔
591
        // absolute jump to end of block if condition is false
592
        const auto label_end = IR::Entity::Label(m_current_label++);
1,231✔
593
        page(p).emplace_back(IR::Entity::GotoIf(label_end, false));
1,231✔
594
        // push code to page
595
        compileExpression(x.list()[2], p, true, false);
1,231✔
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,231✔
602

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

606
        page(p).emplace_back(POP_SCOPE);
1,231✔
607
        m_locals_locator.deleteScope();
1,231✔
608
    }
1,232✔
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,595✔
630
    {
8,595✔
631
        const auto node = call.constList()[0];
8,595✔
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,786✔
641
        {
642
            // FIXME: in (foo a b (breakpoint (< c 0)) c), we will push c before the breakpoint
643
            if (nodeProducesOutput(value) || isBreakpoint(value))
15,191✔
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)
15,189✔
647
                    compileSymbol(value, p, false, /* can_use_ref= */ false);
85✔
648
                else
649
                    compileExpression(value, p, false, false);
15,104✔
650
            }
15,182✔
651
            else
652
                makeError(is_tail_call ? ErrorKind::InvalidNodeInTailCallNoReturnValue : ErrorKind::InvalidNodeNoReturnValue, value, node.repr());
2✔
653
        }
15,191✔
654
    }
8,604✔
655

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

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

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

685
        if (is_result_unused)
22,390✔
686
            page(p).emplace_back(POP);
3,438✔
687
    }
22,502✔
688

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

695
        // short circuit implementation
696
        if (x.constList().size() < 3)
486✔
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]))
484✔
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);
483✔
711

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

716
        for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
1,065✔
717
        {
718
            if (!nodeProducesOutput(x.list()[i]))
582✔
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);
581✔
725
            if (i + 1 != end)
581✔
726
                page(p).emplace_back(shortcircuit_entity);
99✔
727
        }
581✔
728

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

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

738

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

749
            if (!is_breakpoint)
24,346✔
750
                exp_count++;
24,344✔
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,346✔
755
                page(p).emplace_back(op);
10,792✔
756
        }
24,347✔
757

758
        if (isBreakpoint(x))
13,447✔
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,432✔
765
        {
766
            if (exp_count != 1)
2,723✔
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,722✔
769
        }
2,722✔
770
        else if (isTernaryInst(op))
10,709✔
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,654✔
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,442✔
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,434✔
784
    }
13,448✔
785

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

791
        if (is_terminal && node.nodeType() == NodeType::Symbol && isFunctionCallingItself(node.string()))
8,598✔
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,486✔
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,484✔
805

806
        // compile the function resolution to a separate page
807
        if (node.nodeType() == NodeType::Symbol && isFunctionCallingItself(node.string()))
8,484✔
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,427✔
820
        }
821

822
        if (m_temp_pages.back().empty())
8,484✔
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,484✔
826
        page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
8,484✔
827
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
8,484✔
828

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

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

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

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

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

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

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

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

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