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

ArkScript-lang / Ark / 20009525215

07 Dec 2025 07:57PM UTC coverage: 90.367% (-0.2%) from 90.556%
20009525215

Pull #613

github

web-flow
Merge 84d909ba4 into 0bb785969
Pull Request #613: Adding argument attributes 'mut' and 'ref'

45 of 62 new or added lines in 8 files covered. (72.58%)

5 existing lines in 1 file now uncovered.

8011 of 8865 relevant lines covered (90.37%)

180002.24 hits per line

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

93.46
/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) :
342✔
19
        m_logger("ASTLowerer", debug)
342✔
20
    {}
684✔
21

22
    void ASTLowerer::process(Node& ast)
257✔
23
    {
257✔
24
        m_logger.traceStart("process");
257✔
25
        m_code_pages.emplace_back();  // create empty page
257✔
26

27
        // gather symbols, values, and start to create code segments
28
        compileExpression(
257✔
29
            ast,
257✔
30
            /* current_page */ Page { .index = 0, .is_temp = false },
257✔
31
            /* is_result_unused */ false,
32
            /* is_terminal */ false);
33
        m_logger.traceEnd();
257✔
34
    }
257✔
35

36
    const std::vector<IR::Block>& ASTLowerer::intermediateRepresentation() const noexcept
219✔
37
    {
219✔
38
        return m_code_pages;
219✔
39
    }
40

41
    const std::vector<std::string>& ASTLowerer::symbols() const noexcept
432✔
42
    {
432✔
43
        return m_symbols;
432✔
44
    }
45

46
    const std::vector<ValTableElem>& ASTLowerer::values() const noexcept
432✔
47
    {
432✔
48
        return m_values;
432✔
49
    }
50

51
    std::optional<Instruction> ASTLowerer::getOperator(const std::string& name) noexcept
37,739✔
52
    {
37,739✔
53
        const auto it = std::ranges::find(Language::operators, name);
37,739✔
54
        if (it != Language::operators.end())
37,739✔
55
            return static_cast<Instruction>(std::distance(Language::operators.begin(), it) + FIRST_OPERATOR);
9,902✔
56
        return std::nullopt;
27,837✔
57
    }
37,739✔
58

59
    std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
23,929✔
60
    {
23,929✔
61
        const auto it = std::ranges::find_if(Builtins::builtins,
23,929✔
62
                                             [&name](const std::pair<std::string, Value>& element) -> bool {
1,483,802✔
63
                                                 return name == element.first;
1,459,873✔
64
                                             });
65
        if (it != Builtins::builtins.end())
23,929✔
66
            return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
2,532✔
67
        return std::nullopt;
21,397✔
68
    }
23,929✔
69

70
    std::optional<Instruction> ASTLowerer::getListInstruction(const std::string& name) noexcept
22,276✔
71
    {
22,276✔
72
        const auto it = std::ranges::find(Language::listInstructions, name);
22,276✔
73
        if (it != Language::listInstructions.end())
22,276✔
74
            return static_cast<Instruction>(std::distance(Language::listInstructions.begin(), it) + LIST);
5,934✔
75
        return std::nullopt;
16,342✔
76
    }
22,276✔
77

78
    bool ASTLowerer::nodeProducesOutput(const Node& node)
43,594✔
79
    {
43,594✔
80
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
43,594✔
81
            // a begin node produces a value if the last node in it produces a value
82
            return (node.constList()[0].keyword() == Keyword::Begin && node.constList().size() > 1 && nodeProducesOutput(node.constList().back())) ||
706✔
83
                // a function always produces a value ; even if it ends with a node not producing one, the VM returns nil
84
                node.constList()[0].keyword() == Keyword::Fun ||
403✔
85
                // a condition produces a value if all its branches produce a value
86
                (node.constList()[0].keyword() == Keyword::If &&
50✔
87
                 nodeProducesOutput(node.constList()[2]) &&
44✔
88
                 (node.constList().size() == 3 || nodeProducesOutput(node.constList()[3])));
44✔
89
        // in place list instruction, as well as assert, do not produce values
342✔
90
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
43,241✔
91
            return std::ranges::find(Language::UpdateRef, node.constList().front().string()) == Language::UpdateRef.end() &&
14,734✔
92
                node.constList().front().string() != "assert";
7,367✔
93
        return true;  // any other node, function call, symbol, number...
35,874✔
94
    }
43,594✔
95

96
    bool ASTLowerer::isUnaryInst(const Instruction inst) noexcept
9,900✔
97
    {
9,900✔
98
        switch (inst)
9,900✔
99
        {
2,092✔
100
            case NOT: [[fallthrough]];
101
            case LEN: [[fallthrough]];
102
            case EMPTY: [[fallthrough]];
103
            case TAIL: [[fallthrough]];
104
            case HEAD: [[fallthrough]];
105
            case ISNIL: [[fallthrough]];
106
            case TO_NUM: [[fallthrough]];
107
            case TO_STR: [[fallthrough]];
108
            case TYPE:
109
                return true;
9,900✔
110

111
            default:
112
                return false;
7,808✔
113
        }
114
    }
9,900✔
115

116
    bool ASTLowerer::isTernaryInst(const Instruction inst) noexcept
15,748✔
117
    {
15,748✔
118
        switch (inst)
15,748✔
119
        {
143✔
120
            case AT_AT:
121
                return true;
15,748✔
122

123
            default:
124
                return false;
15,605✔
125
        }
126
    }
15,748✔
127

128
    void ASTLowerer::warning(const std::string& message, const Node& node)
×
129
    {
×
130
        fmt::println("{} {}", fmt::styled("Warning", fmt::fg(fmt::color::dark_orange)), Diagnostics::makeContextWithNode(message, node));
×
131
    }
×
132

133
    void ASTLowerer::buildAndThrowError(const std::string& message, const Node& node)
38✔
134
    {
38✔
135
        throw CodeError(message, CodeErrorContext(node.filename(), node.position()));
38✔
136
    }
38✔
137

138
    void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
76,208✔
139
    {
76,208✔
140
        // register symbols
141
        if (x.nodeType() == NodeType::Symbol)
76,208✔
142
            compileSymbol(x, p, is_result_unused);
22,442✔
143
        else if (x.nodeType() == NodeType::Field)
53,766✔
144
        {
145
            // the parser guarantees us that there is at least 2 elements (eg: a.b)
146
            compileSymbol(x.list()[0], p, is_result_unused);
1,487✔
147
            for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
2,982✔
148
            {
149
                uint16_t i = addSymbol(*it);
1,495✔
150
                page(p).emplace_back(GET_FIELD, i);
1,495✔
151
            }
1,495✔
152
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,487✔
153
        }
1,487✔
154
        // register values
155
        else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
52,279✔
156
        {
157
            uint16_t i = addValue(x);
15,276✔
158

159
            if (!is_result_unused)
15,276✔
160
                page(p).emplace_back(LOAD_CONST, i);
15,276✔
161
        }
15,276✔
162
        // namespace nodes
163
        else if (x.nodeType() == NodeType::Namespace)
37,003✔
164
            compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
134✔
165
        else if (x.nodeType() == NodeType::Unused)
36,869✔
166
        {
167
            // do nothing, explicitly
168
        }
5✔
169
        // empty code block should be nil
170
        else if (x.constList().empty())
36,864✔
171
        {
172
            if (!is_result_unused)
×
173
            {
174
                static const std::optional<uint16_t> nil = getBuiltin("nil");
91✔
175
                page(p).emplace_back(BUILTIN, nil.value());
×
176
            }
×
177
        }
×
178
        // list instructions
179
        else if (const auto head = x.constList()[0]; head.nodeType() == NodeType::Symbol && getListInstruction(head.string()).has_value())
73,728✔
180
            compileListInstruction(x, p, is_result_unused);
2,967✔
181
        // registering structures
182
        else if (x.constList()[0].nodeType() == NodeType::Keyword)
33,897✔
183
        {
184
            switch (const Keyword keyword = x.constList()[0].keyword())
17,066✔
185
            {
1,929✔
186
                case Keyword::If:
187
                    compileIf(x, p, is_result_unused, is_terminal);
1,929✔
188
                    break;
10,030✔
189

190
                case Keyword::Set:
191
                    [[fallthrough]];
192
                case Keyword::Let:
193
                    [[fallthrough]];
194
                case Keyword::Mut:
195
                    compileLetMutSet(keyword, x, p);
8,103✔
196
                    break;
11,218✔
197

198
                case Keyword::Fun:
199
                    compileFunction(x, p, is_result_unused);
3,122✔
200
                    break;
6,060✔
201

202
                case Keyword::Begin:
203
                {
204
                    for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
15,962✔
205
                        compileExpression(
26,038✔
206
                            x.list()[i],
13,019✔
207
                            p,
13,019✔
208
                            // All the nodes in a begin (except for the last one) are producing a result that we want to drop.
209
                            (i != size - 1) || is_result_unused,
13,019✔
210
                            // If the begin is a terminal node, only its last node is terminal.
211
                            is_terminal && (i == size - 1));
13,019✔
212
                    break;
2,903✔
213
                }
965✔
214

215
                case Keyword::While:
216
                    compileWhile(x, p);
965✔
217
                    break;
966✔
218

219
                case Keyword::Import:
220
                    compilePluginImport(x, p);
2✔
221
                    break;
4✔
222

223
                case Keyword::Del:
224
                    page(p).emplace_back(DEL, addSymbol(x.constList()[1]));
2✔
225
                    page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
226
                    break;
2✔
227
            }
17,066✔
228
        }
17,011✔
229
        else if (x.nodeType() == NodeType::List)
16,831✔
230
        {
231
            // If we are here, we should have a function name via the m_opened_vars.
232
            // Push arguments first, then function name, then call it.
233
            handleCalls(x, p, is_result_unused, is_terminal);
16,831✔
234
        }
16,805✔
235
        else
236
            buildAndThrowError(
×
237
                fmt::format(
×
238
                    "NodeType `{}' not handled in ASTLowerer::compileExpression. Please fill an issue on GitHub: https://github.com/ArkScript-lang/Ark",
×
239
                    typeToString(x)),
×
240
                x);
×
241
    }
76,208✔
242

243
    void ASTLowerer::compileSymbol(const Node& x, const Page p, const bool is_result_unused)
23,929✔
244
    {
23,929✔
245
        const std::string& name = x.string();
23,929✔
246

247
        if (const auto it_builtin = getBuiltin(name))
47,858✔
248
            page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
2,532✔
249
        else if (getOperator(name).has_value())
21,397✔
250
            buildAndThrowError(fmt::format("Found a free standing operator: `{}`", name), x);
1✔
251
        else
252
        {
253
            const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
21,396✔
254
            if (maybe_local_idx.has_value())
21,396✔
255
                page(p).emplace_back(LOAD_SYMBOL_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
9,130✔
256
            else
257
                page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
12,266✔
258
        }
21,396✔
259

260
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
23,928✔
261

262
        if (is_result_unused)
23,928✔
263
        {
264
            warning("Statement has no effect", x);
×
265
            page(p).emplace_back(POP);
×
266
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
×
267
        }
×
268
    }
23,929✔
269

270
    void ASTLowerer::compileListInstruction(Node& x, const Page p, const bool is_result_unused)
2,967✔
271
    {
2,967✔
272
        const Node head = x.constList()[0];
2,967✔
273
        std::string name = x.constList()[0].string();
2,967✔
274
        Instruction inst = getListInstruction(name).value();
2,967✔
275

276
        // length of at least 1 since we got a symbol name
277
        const auto argc = x.constList().size() - 1u;
2,967✔
278
        // error, can not use append/concat/pop (and their in place versions) with a <2 length argument list
279
        if (argc < 2 && APPEND <= inst && inst <= POP)
2,967✔
280
            buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), head);
6✔
281
        if (inst <= POP && std::cmp_greater(argc, MaxValue16Bits))
2,961✔
282
            buildAndThrowError(fmt::format("Too many arguments ({}), exceeds {}", argc, MaxValue16Bits), x);
1✔
283
        if (argc != 3 && inst == SET_AT_INDEX)
2,960✔
284
            buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), head);
1✔
285
        if (argc != 4 && inst == SET_AT_2_INDEX)
2,959✔
286
            buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), head);
1✔
287

288
        // compile arguments in reverse order
289
        for (std::size_t i = x.constList().size() - 1u; i > 0; --i)
10,681✔
290
        {
291
            Node& node = x.list()[i];
7,723✔
292
            if (nodeProducesOutput(node))
7,723✔
293
                compileExpression(node, p, false, false);
7,722✔
294
            else
295
                buildAndThrowError(fmt::format("Invalid node inside call to {}", name), node);
1✔
296
        }
7,723✔
297

298
        // put inst and number of arguments
299
        std::size_t inst_argc = 0;
2,957✔
300
        switch (inst)
2,957✔
301
        {
2,205✔
302
            case LIST:
303
                inst_argc = argc;
2,205✔
304
                break;
2,918✔
305

306
            case APPEND:
307
            case APPEND_IN_PLACE:
308
            case CONCAT:
309
            case CONCAT_IN_PLACE:
310
                inst_argc = argc - 1;
713✔
311
                break;
731✔
312

313
            case POP_LIST:
314
            case POP_LIST_IN_PLACE:
315
                inst_argc = 0;
18✔
316
                break;
39✔
317

318
            default:
319
                break;
21✔
320
        }
2,957✔
321
        page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
2,957✔
322
        page(p).back().setSourceLocation(head.filename(), head.position().start.line);
2,957✔
323

324
        if (is_result_unused && name.back() != '!' && inst <= POP_LIST_IN_PLACE)  // in-place functions never push a value
2,957✔
325
        {
326
            warning("Ignoring return value of function", x);
×
327
            page(p).emplace_back(POP);
×
328
        }
×
329
    }
2,977✔
330

331
    void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
1,929✔
332
    {
1,929✔
333
        if (x.constList().size() == 1)
1,929✔
334
            buildAndThrowError("Invalid condition: missing 'cond' and 'then' nodes, expected (if cond then)", x);
2✔
335
        if (x.constList().size() == 2)
1,928✔
336
            buildAndThrowError(fmt::format("Invalid condition: missing 'then' node, expected (if {} then)", x.constList()[1].repr()), x);
1✔
337

338
        // compile condition
339
        compileExpression(x.list()[1], p, false, false);
1,927✔
340
        page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
1,927✔
341

342
        // jump only if needed to the "true" branch
343
        const auto label_then = IR::Entity::Label(m_current_label++);
1,927✔
344
        page(p).emplace_back(IR::Entity::GotoIf(label_then, true));
1,927✔
345

346
        // "false" branch code
347
        if (x.constList().size() == 4)  // we have an else clause
1,927✔
348
        {
349
            m_locals_locator.saveScopeLengthForBranch();
1,517✔
350
            compileExpression(x.list()[3], p, is_result_unused, is_terminal);
1,517✔
351
            page(p).back().setSourceLocation(x.constList()[3].filename(), x.constList()[3].position().start.line);
1,517✔
352
            m_locals_locator.dropVarsForBranch();
1,517✔
353
        }
1,517✔
354

355
        // when else is finished, jump to end
356
        const auto label_end = IR::Entity::Label(m_current_label++);
1,927✔
357
        page(p).emplace_back(IR::Entity::Goto(label_end));
1,927✔
358

359
        // absolute address to jump to if condition is true
360
        page(p).emplace_back(label_then);
1,927✔
361
        // if code
362
        m_locals_locator.saveScopeLengthForBranch();
1,927✔
363
        compileExpression(x.list()[2], p, is_result_unused, is_terminal);
1,927✔
364
        page(p).back().setSourceLocation(x.constList()[2].filename(), x.constList()[2].position().start.line);
1,927✔
365
        m_locals_locator.dropVarsForBranch();
1,927✔
366
        // set jump to end pos
367
        page(p).emplace_back(label_end);
1,927✔
368
    }
1,929✔
369

370
    void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused)
3,122✔
371
    {
3,122✔
372
        if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List)
3,124✔
373
            buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args);
1✔
374
        if (x.constList().size() != 3)
3,121✔
375
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
376

377
        // capture, if needed
378
        std::size_t capture_inst_count = 0;
3,120✔
379
        for (const auto& node : x.constList()[1].constList())
8,108✔
380
        {
381
            if (node.nodeType() == NodeType::Capture)
4,988✔
382
            {
383
                const uint16_t symbol_id = addSymbol(node);
227✔
384

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

391
                    page(p).emplace_back(RENAME_NEXT_CAPTURE, nqn_id);
14✔
392
                    page(p).emplace_back(CAPTURE, symbol_id);
14✔
393
                }
14✔
394
                else
395
                    page(p).emplace_back(CAPTURE, symbol_id);
213✔
396

397
                ++capture_inst_count;
227✔
398
            }
227✔
399
        }
4,988✔
400
        const bool is_closure = capture_inst_count > 0;
3,120✔
401

402
        m_locals_locator.createScope(
6,240✔
403
            is_closure
3,120✔
404
                ? LocalsLocator::ScopeType::Closure
405
                : LocalsLocator::ScopeType::Function);
406

407
        // create new page for function body
408
        m_code_pages.emplace_back();
3,120✔
409
        const auto function_body_page = Page { .index = m_code_pages.size() - 1, .is_temp = false };
3,120✔
410
        // save page_id into the constants table as PageAddr and load the const
411
        page(p).emplace_back(is_closure ? MAKE_CLOSURE : LOAD_CONST, addValue(function_body_page.index, x));
3,120✔
412

413
        // pushing arguments from the stack into variables in the new scope
414
        for (const auto& node : x.constList()[1].constList())
8,108✔
415
        {
416
            if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg)
4,988✔
417
            {
418
                page(function_body_page).emplace_back(STORE, addSymbol(node));
4,761✔
419
                m_locals_locator.addLocal(node.string());
4,761✔
420
            }
4,761✔
421
            else if (node.nodeType() == NodeType::RefArg)
227✔
422
            {
NEW
423
                page(function_body_page).emplace_back(STORE_REF, addSymbol(node));
×
NEW
424
                m_locals_locator.addLocal(node.string());
×
NEW
425
            }
×
426
        }
4,988✔
427

428
        // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls.
429
        // This way we can continue to safely apply optimisations on
430
        // (let name (fun (e) (map lst (fun (e) (name e)))))
431
        // Otherwise, `name` would have been optimized to a GET_CURRENT_PAGE_ADDRESS, which would have returned the wrong page.
432
        if (x.isAnonymousFunction())
3,120✔
433
            m_opened_vars.emplace("#anonymous");
365✔
434
        // push body of the function
435
        compileExpression(x.list()[2], function_body_page, false, true);
3,120✔
436
        if (x.isAnonymousFunction())
3,120✔
437
            m_opened_vars.pop();
365✔
438

439
        // return last value on the stack
440
        page(function_body_page).emplace_back(RET);
3,120✔
441
        m_locals_locator.deleteScope();
3,120✔
442

443
        // if the computed function is unused, pop it
444
        if (is_result_unused)
3,120✔
445
        {
446
            warning("Unused declared function", x);
×
447
            page(p).emplace_back(POP);
×
448
        }
×
449
    }
3,122✔
450

451
    void ASTLowerer::compileLetMutSet(const Keyword n, Node& x, const Page p)
8,103✔
452
    {
8,103✔
453
        if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol)
8,110✔
454
            buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym);
×
455
        if (x.constList().size() != 3)
8,103✔
456
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
457

458
        const std::string name = x.constList()[1].string();
8,102✔
459
        uint16_t i = addSymbol(x.constList()[1]);
8,102✔
460

461
        if (!m_opened_vars.empty() && m_opened_vars.top() == name)
8,102✔
462
            buildAndThrowError("Can not define a variable using the same name as the function it is defined inside", x);
1✔
463

464
        const bool is_function = x.constList()[2].isFunction();
8,101✔
465
        if (is_function)
8,101✔
466
        {
467
            m_opened_vars.push(name);
2,757✔
468
            x.list()[2].setFunctionKind(/* anonymous= */ false);
2,757✔
469
        }
2,757✔
470

471
        // put value before symbol id
472
        // starting at index = 2 because x is a (let|mut|set variable ...) node
473
        for (std::size_t idx = 2, end = x.constList().size(); idx < end; ++idx)
16,202✔
474
            compileExpression(x.list()[idx], p, false, false);
8,101✔
475

476
        if (n == Keyword::Let || n == Keyword::Mut)
8,096✔
477
        {
478
            page(p).emplace_back(STORE, i);
6,262✔
479
            m_locals_locator.addLocal(name);
6,262✔
480
        }
6,262✔
481
        else
482
            page(p).emplace_back(SET_VAL, i);
1,834✔
483

484
        if (is_function)
8,096✔
485
            m_opened_vars.pop();
2,752✔
486
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
8,096✔
487
    }
8,104✔
488

489
    void ASTLowerer::compileWhile(Node& x, const Page p)
965✔
490
    {
965✔
491
        if (x.constList().size() != 3)
965✔
492
            buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
1✔
493

494
        m_locals_locator.createScope();
964✔
495
        page(p).emplace_back(CREATE_SCOPE);
964✔
496
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
964✔
497

498
        // save current position to jump there at the end of the loop
499
        const auto label_loop = IR::Entity::Label(m_current_label++);
964✔
500
        page(p).emplace_back(label_loop);
964✔
501
        // push condition
502
        compileExpression(x.list()[1], p, false, false);
964✔
503
        // absolute jump to end of block if condition is false
504
        const auto label_end = IR::Entity::Label(m_current_label++);
964✔
505
        page(p).emplace_back(IR::Entity::GotoIf(label_end, false));
964✔
506
        // push code to page
507
        compileExpression(x.list()[2], p, true, false);
964✔
508

509
        // reset the scope at the end of the loop so that indices are still valid
510
        // otherwise, (while true { (let a 5) (print a) (let b 6) (print b) })
511
        // would print 5, 6, then only 6 as we emit LOAD_SYMBOL_FROM_INDEX 0 and b is the last in the scope
512
        // loop, jump to the condition
513
        page(p).emplace_back(IR::Entity::Goto(label_loop, RESET_SCOPE_JUMP));
964✔
514

515
        // absolute address to jump to if condition is false
516
        page(p).emplace_back(label_end);
964✔
517

518
        page(p).emplace_back(POP_SCOPE);
964✔
519
        m_locals_locator.deleteScope();
964✔
520
    }
965✔
521

522
    void ASTLowerer::compilePluginImport(const Node& x, const Page p)
2✔
523
    {
2✔
524
        std::string path;
2✔
525
        const Node package_node = x.constList()[1];
2✔
526
        for (std::size_t i = 0, end = package_node.constList().size(); i < end; ++i)
4✔
527
        {
528
            path += package_node.constList()[i].string();
2✔
529
            if (i + 1 != end)
2✔
530
                path += "/";
×
531
        }
2✔
532
        path += ".arkm";
2✔
533

534
        // register plugin path in the constants table
535
        uint16_t id = addValue(Node(NodeType::String, path));
2✔
536
        // add plugin instruction + id of the constant referring to the plugin path
537
        page(p).emplace_back(PLUGIN, id);
2✔
538
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
539
    }
2✔
540

541
    void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call)
6,558✔
542
    {
6,558✔
543
        const auto node = call.constList()[0];
6,558✔
544

545
        // push the arguments in reverse order because the function loads its arguments in the order they are defined:
546
        // (fun (a b c) ...) -> load 'a', then 'b', then 'c'
547
        // We have to push arguments in this order and load them in reverse, because we are using references internally,
548
        // which can cause problems for recursive functions that swap their arguments around.
549
        // Eg (let foo (fun (a b c) (if (> a 0) (foo (- a 1) c (+ b c)) 1))) (foo 12 0 1)
550
        // 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
551
        // value for argument b, but loaded it as a reference.
552
        for (Node& value : std::ranges::drop_view(call.list(), 1) | std::views::reverse)
18,025✔
553
        {
554
            if (nodeProducesOutput(value))
11,467✔
555
                compileExpression(value, p, false, false);
11,465✔
556
            else
557
            {
558
                std::string message;
2✔
559
                if (is_tail_call)
2✔
560
                    message = fmt::format("Invalid node inside tail call to `{}'", node.repr());
1✔
561
                else
562
                    message = fmt::format("Invalid node inside call to `{}'", node.repr());
1✔
563
                buildAndThrowError(message, value);
2✔
564
            }
2✔
565
        }
11,467✔
566
    }
6,564✔
567

568
    void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal)
16,830✔
569
    {
16,830✔
570
        constexpr std::size_t start_index = 1;
16,830✔
571

572
        Node& node = x.list()[0];
16,830✔
573
        const std::optional<Instruction> maybe_operator = node.nodeType() == NodeType::Symbol ? getOperator(node.string()) : std::nullopt;
16,830✔
574

575
        const std::optional<Instruction> maybe_shortcircuit =
16,830✔
576
            node.nodeType() == NodeType::Symbol
33,172✔
577
            ? (node.string() == Language::And
32,435✔
578
                   ? std::make_optional(Instruction::SHORTCIRCUIT_AND)
249✔
579
                   : (node.string() == Language::Or
16,093✔
580
                          ? std::make_optional(Instruction::SHORTCIRCUIT_OR)
119✔
581
                          : std::nullopt))
15,974✔
582
            : std::nullopt;
488✔
583

584
        if (maybe_shortcircuit.has_value())
16,830✔
585
        {
586
            // short circuit implementation
587
            if (x.constList().size() < 3)
368✔
588
                buildAndThrowError(
2✔
589
                    fmt::format(
4✔
590
                        "Expected at least 2 arguments while compiling '{}', got {}",
2✔
591
                        node.string(),
2✔
592
                        x.constList().size() - 1),
2✔
593
                    x);
2✔
594

595
            compileExpression(x.list()[1], p, false, false);
366✔
596

597
            const auto label_shortcircuit = IR::Entity::Label(m_current_label++);
366✔
598
            auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, maybe_shortcircuit.value());
366✔
599
            page(p).emplace_back(shortcircuit_entity);
366✔
600

601
            for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
469✔
602
            {
603
                compileExpression(x.list()[i], p, false, false);
103✔
604
                if (i + 1 != end)
103✔
605
                    page(p).emplace_back(shortcircuit_entity);
103✔
606
            }
103✔
607

608
            page(p).emplace_back(label_shortcircuit);
366✔
609
        }
366✔
610
        else if (!maybe_operator.has_value())
16,462✔
611
        {
612
            if (is_terminal && node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string())
6,561✔
613
            {
614
                pushFunctionCallArguments(x, p, /* is_tail_call= */ true);
84✔
615

616
                // jump to the top of the function
617
                page(p).emplace_back(JUMP, 0_u16);
84✔
618
                page(p).back().setSourceLocation(node.filename(), node.position().start.line);
84✔
619
                return;  // skip the potential Instruction::POP at the end
84✔
620
            }
621
            else
622
            {
623
                if (!nodeProducesOutput(node))
6,477✔
624
                    buildAndThrowError(fmt::format("Can not call `{}', as it doesn't return a value", node.repr()), node);
2✔
625

626
                m_temp_pages.emplace_back();
6,475✔
627
                const auto proc_page = Page { .index = m_temp_pages.size() - 1u, .is_temp = true };
6,475✔
628

629
                // compile the function resolution to a separate page
630
                if (node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string())
6,475✔
631
                {
632
                    // The function is trying to call itself, but this isn't a tail call.
633
                    // We can skip the LOAD_SYMBOL function_name and directly push the current
634
                    // function page, which will be quicker than a local variable resolution.
635
                    // We set its argument to the symbol id of the function we are calling,
636
                    // so that the VM knows the name of the last called function.
637
                    page(proc_page).emplace_back(GET_CURRENT_PAGE_ADDR, addSymbol(node));
57✔
638
                }
57✔
639
                else
640
                {
641
                    // closure chains have been handled (eg: closure.field.field.function)
642
                    compileExpression(node, proc_page, false, false);  // storing proc
6,418✔
643
                }
644

645
                if (m_temp_pages.back().empty())
6,475✔
646
                    buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x);
×
647

648
                const auto label_return = IR::Entity::Label(m_current_label++);
6,475✔
649
                page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
6,475✔
650
                page(p).back().setSourceLocation(x.filename(), x.position().start.line);
6,475✔
651

652
                pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
6,475✔
653
                // push proc from temp page
654
                for (const auto& inst : m_temp_pages.back())
13,445✔
655
                    page(p).push_back(inst);
6,975✔
656
                m_temp_pages.pop_back();
6,470✔
657

658
                // number of arguments
659
                std::size_t args_count = 0;
6,470✔
660
                for (auto it = x.constList().begin() + start_index, it_end = x.constList().end(); it != it_end; ++it)
17,708✔
661
                {
662
                    if (it->nodeType() != NodeType::Capture)
11,238✔
663
                        args_count++;
11,238✔
664
                }
11,238✔
665
                // call the procedure
666
                page(p).emplace_back(CALL, args_count);
6,470✔
667
                page(p).back().setSourceLocation(node.filename(), node.position().start.line);
6,470✔
668

669
                // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
670
                page(p).emplace_back(label_return);
6,470✔
671
            }
6,475✔
672
        }
6,470✔
673
        else  // operator
674
        {
675
            // retrieve operator
676
            auto op = maybe_operator.value();
9,901✔
677

678
            if (op == ASSERT)
9,901✔
679
                is_result_unused = false;
346✔
680

681
            // push arguments on current page
682
            std::size_t exp_count = 0;
9,901✔
683
            for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
27,740✔
684
            {
685
                if (nodeProducesOutput(x.constList()[index]))
17,839✔
686
                    compileExpression(x.list()[index], p, false, false);
17,838✔
687
                else
688
                    buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'", node.repr()), x.constList()[index]);
1✔
689

690
                if ((index + 1 < size && x.constList()[index + 1].nodeType() != NodeType::Capture) || index + 1 == size)
17,838✔
691
                    exp_count++;
17,838✔
692

693
                // in order to be able to handle things like (op A B C D...)
694
                // which should be transformed into A B op C op D op...
695
                if (exp_count >= 2 && !isTernaryInst(op))
17,838✔
696
                    page(p).emplace_back(op);
7,845✔
697
            }
17,838✔
698

699
            if (isUnaryInst(op))
9,900✔
700
            {
701
                if (exp_count != 1)
2,092✔
702
                    buildAndThrowError(fmt::format("Operator needs one argument, but was called with {}", exp_count), x.constList()[0]);
1✔
703
                page(p).emplace_back(op);
2,091✔
704
            }
2,091✔
705
            else if (isTernaryInst(op))
7,808✔
706
            {
707
                if (exp_count != 3)
48✔
708
                    buildAndThrowError(fmt::format("Operator needs three arguments, but was called with {}", exp_count), x.constList()[0]);
1✔
709
                page(p).emplace_back(op);
47✔
710
            }
47✔
711
            else if (exp_count <= 1)
7,760✔
712
                buildAndThrowError(fmt::format("Operator needs two arguments, but was called with {}", exp_count), x.constList()[0]);
2✔
713

714
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
9,896✔
715

716
            // need to check we didn't push the (op A B C D...) things for operators not supporting it
717
            if (exp_count > 2)
9,896✔
718
            {
719
                switch (op)
120✔
720
                {
111✔
721
                    // authorized instructions
722
                    case ADD: [[fallthrough]];
723
                    case SUB: [[fallthrough]];
724
                    case MUL: [[fallthrough]];
725
                    case DIV: [[fallthrough]];
726
                    case MOD: [[fallthrough]];
727
                    case AT_AT:
728
                        break;
120✔
729

730
                    default:
731
                        buildAndThrowError(
9✔
732
                            fmt::format(
18✔
733
                                "`{}' requires 2 arguments, but got {}.",
9✔
734
                                Language::operators[static_cast<std::size_t>(op - FIRST_OPERATOR)],
9✔
735
                                exp_count),
736
                            x);
9✔
737
                }
111✔
738
            }
111✔
739
        }
9,901✔
740

741
        if (is_result_unused)
16,723✔
742
            page(p).emplace_back(POP);
2,200✔
743
    }
16,851✔
744

745
    uint16_t ASTLowerer::addSymbol(const Node& sym)
26,924✔
746
    {
26,924✔
747
        // otherwise, add the symbol, and return its id in the table
748
        auto it = std::ranges::find(m_symbols, sym.string());
26,924✔
749
        if (it == m_symbols.end())
26,924✔
750
        {
751
            m_symbols.push_back(sym.string());
5,984✔
752
            it = m_symbols.begin() + static_cast<std::vector<std::string>::difference_type>(m_symbols.size() - 1);
5,984✔
753
        }
5,984✔
754

755
        const auto distance = std::distance(m_symbols.begin(), it);
26,924✔
756
        if (std::cmp_less(distance, MaxValue16Bits))
26,924✔
757
            return static_cast<uint16_t>(distance);
53,848✔
758
        buildAndThrowError(fmt::format("Too many symbols (exceeds {}), aborting compilation.", MaxValue16Bits), sym);
×
759
    }
26,924✔
760

761
    uint16_t ASTLowerer::addValue(const Node& x)
15,278✔
762
    {
15,278✔
763
        const ValTableElem v(x);
15,278✔
764
        auto it = std::ranges::find(m_values, v);
15,278✔
765
        if (it == m_values.end())
15,278✔
766
        {
767
            m_values.push_back(v);
3,003✔
768
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,003✔
769
        }
3,003✔
770

771
        const auto distance = std::distance(m_values.begin(), it);
15,278✔
772
        if (std::cmp_less(distance, MaxValue16Bits))
15,278✔
773
            return static_cast<uint16_t>(distance);
15,278✔
774
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), x);
×
775
    }
15,278✔
776

777
    uint16_t ASTLowerer::addValue(const std::size_t page_id, const Node& current)
3,120✔
778
    {
3,120✔
779
        const ValTableElem v(page_id);
3,120✔
780
        auto it = std::ranges::find(m_values, v);
3,120✔
781
        if (it == m_values.end())
3,120✔
782
        {
783
            m_values.push_back(v);
3,120✔
784
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,120✔
785
        }
3,120✔
786

787
        const auto distance = std::distance(m_values.begin(), it);
3,120✔
788
        if (std::cmp_less(distance, MaxValue16Bits))
3,120✔
789
            return static_cast<uint16_t>(distance);
3,120✔
790
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), current);
×
791
    }
3,120✔
792
}
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