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

ArkScript-lang / Ark / 23209646190

17 Mar 2026 06:16PM UTC coverage: 93.659% (-0.05%) from 93.706%
23209646190

push

github

SuperFola
feat(vm): add a new super instruction CALL_SYMBOL_BY_INDEX

17 of 19 new or added lines in 3 files covered. (89.47%)

132 existing lines in 8 files now uncovered.

9601 of 10251 relevant lines covered (93.66%)

275534.05 hits per line

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

96.24
/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp
1
#include <Ark/Compiler/Lowerer/ASTLowerer.hpp>
2

3
#include <cassert>
4
#include <ranges>
5
#include <utility>
6
#include <algorithm>
7
#include <fmt/core.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
    enum class CallType
19
    {
20
        Classic,
21
        SelfNotRecursive,
22
        Symbol,
23
        SymbolByIndex,
24
        Builtin
25
    };
26

27
    ASTLowerer::ASTLowerer(const unsigned debug) :
914✔
28
        Pass("ASTLowerer", debug)
457✔
29
    {}
1,371✔
30

31
    void ASTLowerer::addToTables(const std::vector<std::string>& symbols, const std::vector<ValTableElem>& constants)
15✔
32
    {
15✔
33
        std::ranges::copy(symbols, std::back_inserter(m_symbols));
15✔
34
        std::ranges::copy(constants, std::back_inserter(m_values));
15✔
35
    }
15✔
36

37
    void ASTLowerer::offsetPagesBy(const std::size_t offset)
15✔
38
    {
15✔
39
        m_start_page_at_offset = offset;
15✔
40
    }
15✔
41

42
    void ASTLowerer::process(Node& ast)
364✔
43
    {
364✔
44
        m_logger.traceStart("process");
364✔
45
        const Page global = createNewCodePage();
364✔
46

47
        // gather symbols, values, and start to create code segments
48
        compileExpression(
364✔
49
            ast,
364✔
50
            /* current_page */ global,
364✔
51
            /* is_result_unused= */ true,
52
            /* is_terminal= */ false);
53
        m_logger.traceEnd();
364✔
54
    }
364✔
55

56
    const std::vector<IR::Block>& ASTLowerer::intermediateRepresentation() const noexcept
322✔
57
    {
322✔
58
        return m_code_pages;
322✔
59
    }
60

61
    const std::vector<std::string>& ASTLowerer::symbols() const noexcept
633✔
62
    {
633✔
63
        return m_symbols;
633✔
64
    }
65

66
    const std::vector<ValTableElem>& ASTLowerer::values() const noexcept
633✔
67
    {
633✔
68
        return m_values;
633✔
69
    }
70

71
    std::optional<Instruction> ASTLowerer::getOperator(const std::string& name) noexcept
56,874✔
72
    {
56,874✔
73
        const auto it = std::ranges::find(Language::operators, name);
56,874✔
74
        if (it != Language::operators.end())
56,874✔
75
            return static_cast<Instruction>(std::distance(Language::operators.begin(), it) + FIRST_OPERATOR);
14,312✔
76
        return std::nullopt;
42,562✔
77
    }
56,874✔
78

79
    std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
37,764✔
80
    {
37,764✔
81
        const auto it = std::ranges::find_if(Builtins::builtins,
37,764✔
82
                                             [&name](const std::pair<std::string, Value>& element) -> bool {
3,215,155✔
83
                                                 return name == element.first;
3,177,391✔
84
                                             });
85
        if (it != Builtins::builtins.end())
37,764✔
86
            return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
3,800✔
87
        return std::nullopt;
33,964✔
88
    }
37,764✔
89

90
    std::optional<Instruction> ASTLowerer::getListInstruction(const std::string& name) noexcept
30,930✔
91
    {
30,930✔
92
        const auto it = std::ranges::find(Language::listInstructions, name);
30,930✔
93
        if (it != Language::listInstructions.end())
30,930✔
94
            return static_cast<Instruction>(std::distance(Language::listInstructions.begin(), it) + LIST);
7,828✔
95
        return std::nullopt;
23,102✔
96
    }
30,930✔
97

98
    bool ASTLowerer::isBreakpoint(const Node& node)
55,851✔
99
    {
55,851✔
100
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
55,851✔
101
            return node.constList().front().string() == "breakpoint";
22,227✔
102
        return false;
33,624✔
103
    }
55,851✔
104

105
    bool ASTLowerer::nodeProducesOutput(const Node& node)
61,213✔
106
    {
61,670✔
107
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
61,213✔
108
            // a 'begin' node produces a value if the last node in it produces a value
109
            return (node.constList()[0].keyword() == Keyword::Begin && node.constList().size() > 1 && nodeProducesOutput(node.constList().back())) ||
1,411✔
110
                // a function always produces a value ; even if it ends with a node not producing one, the VM returns nil
111
                node.constList()[0].keyword() == Keyword::Fun ||
477✔
112
                // a let/mut/set pushes the value that was assigned
113
                node.constList()[0].keyword() == Keyword::Let ||
137✔
114
                node.constList()[0].keyword() == Keyword::Mut ||
137✔
115
                node.constList()[0].keyword() == Keyword::Set ||
274✔
116
                // a condition produces a value if all its branches produce a value
117
                (node.constList()[0].keyword() == Keyword::If &&
137✔
118
                 nodeProducesOutput(node.constList()[2]) &&
133✔
119
                 (node.constList().size() == 3 || nodeProducesOutput(node.constList()[3])));
133✔
120
        // breakpoint do not produce values
121
        if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
60,736✔
122
        {
123
            const std::string& name = node.constList().front().string();
10,303✔
124
            return name != "breakpoint";
10,303✔
125
        }
10,303✔
126
        return true;  // any other node, function call, symbol, number...
50,433✔
127
    }
61,213✔
128

129
    bool ASTLowerer::isUnaryInst(const Instruction inst) noexcept
14,292✔
130
    {
14,292✔
131
        switch (inst)
14,292✔
132
        {
2,749✔
133
            case NOT: [[fallthrough]];
134
            case LEN: [[fallthrough]];
135
            case IS_EMPTY: [[fallthrough]];
136
            case TAIL: [[fallthrough]];
137
            case HEAD: [[fallthrough]];
138
            case IS_NIL: [[fallthrough]];
139
            case TO_NUM: [[fallthrough]];
140
            case TO_STR: [[fallthrough]];
141
            case TYPE:
142
                return true;
14,292✔
143

144
            default:
145
                return false;
11,543✔
146
        }
147
    }
14,292✔
148

149
    bool ASTLowerer::isTernaryInst(const Instruction inst) noexcept
23,445✔
150
    {
23,445✔
151
        switch (inst)
23,445✔
152
        {
426✔
153
            case AT_AT:
154
                return true;
23,445✔
155

156
            default:
157
                return false;
23,019✔
158
        }
159
    }
23,445✔
160

161
    bool ASTLowerer::isRepeatableOperation(const Instruction inst) noexcept
231✔
162
    {
231✔
163
        switch (inst)
231✔
164
        {
117✔
165
            case ADD: [[fallthrough]];
166
            case SUB: [[fallthrough]];
167
            case MUL: [[fallthrough]];
168
            case DIV:
169
                return true;
231✔
170

171
            default:
172
                return false;
114✔
173
        }
174
    }
231✔
175

176
    void ASTLowerer::warning(const std::string& message, const Node& node)
10✔
177
    {
10✔
178
        m_logger.warn("{}", Diagnostics::makeContextWithNode(message, node, m_logger.colorize()));
10✔
179
    }
10✔
180

181
    void ASTLowerer::buildAndThrowError(const std::string& message, const Node& node)
42✔
182
    {
42✔
183
        throw CodeError(message, CodeErrorContext(node.filename(), node.position()));
42✔
184
    }
42✔
185

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

190
        switch (kind)
8✔
191
        {
3✔
192
            case ErrorKind::InvalidNodeMacro:
193
                buildAndThrowError(fmt::format("Invalid node ; if it was computed by a macro, check that a node is returned"), node);
6✔
194
                break;
195

196
            case ErrorKind::InvalidNodeNoReturnValue:
197
                buildAndThrowError(fmt::format("Invalid node inside call to `{}'. {}", additional_ctx, invalid_node_msg), node);
4✔
198
                break;
199

200
            case ErrorKind::InvalidNodeInTailCallNoReturnValue:
201
                buildAndThrowError(fmt::format("Invalid node inside tail call to `{}'. {}", additional_ctx, invalid_node_msg), node);
2✔
202
                break;
203

204
            case ErrorKind::InvalidNodeInOperatorNoReturnValue:
205
                buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'. {}", additional_ctx, invalid_node_msg), node);
1✔
206
                break;
UNCOV
207
        }
×
208
    }
16✔
209

210
    void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
110,346✔
211
    {
110,346✔
212
        // register symbols
213
        if (x.nodeType() == NodeType::Symbol)
110,346✔
214
            compileSymbol(x, p, is_result_unused, /* can_use_ref= */ true);
35,854✔
215
        else if (x.nodeType() == NodeType::Field)
74,492✔
216
        {
217
            // the parser guarantees us that there is at least 2 elements (eg: a.b)
218
            compileSymbol(x.list()[0], p, is_result_unused, /* can_use_ref= */ true);
1,825✔
219
            for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
3,656✔
220
            {
221
                uint16_t i = addSymbol(*it);
1,831✔
222
                page(p).emplace_back(GET_FIELD, i);
1,831✔
223
            }
1,831✔
224
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,825✔
225
        }
1,825✔
226
        // register values
227
        else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
72,667✔
228
        {
229
            uint16_t i = addValue(x);
18,252✔
230

231
            if (!is_result_unused)
18,252✔
232
                page(p).emplace_back(LOAD_CONST, i);
18,252✔
233
        }
18,252✔
234
        // namespace nodes
235
        else if (x.nodeType() == NodeType::Namespace)
54,415✔
236
            compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
136✔
237
        else if (x.nodeType() == NodeType::List)
54,279✔
238
        {
239
            // empty code block should be nil
240
            if (x.constList().empty())
54,274✔
241
            {
UNCOV
242
                if (!is_result_unused)
×
243
                {
244
                    static const std::optional<uint16_t> nil = getBuiltin("nil");
100✔
UNCOV
245
                    page(p).emplace_back(BUILTIN, nil.value());
×
UNCOV
246
                }
×
UNCOV
247
            }
×
248
            // list instructions
249
            else if (const auto head = x.constList()[0]; head.nodeType() == NodeType::Symbol && getListInstruction(head.string()).has_value())
108,548✔
250
                compileListInstruction(x, p, is_result_unused);
3,914✔
251
            else if (head.nodeType() == NodeType::Symbol && head.string() == Language::Apply)
50,360✔
252
                compileApplyInstruction(x, p, is_result_unused);
189✔
253
            // registering structures
254
            else if (head.nodeType() == NodeType::Keyword)
50,171✔
255
            {
256
                switch (const Keyword keyword = head.keyword())
26,494✔
257
                {
3,063✔
258
                    case Keyword::If:
259
                        compileIf(x, p, is_result_unused, is_terminal);
3,063✔
260
                        break;
15,984✔
261

262
                    case Keyword::Set:
263
                        [[fallthrough]];
264
                    case Keyword::Let:
265
                        [[fallthrough]];
266
                    case Keyword::Mut:
267
                        compileLetMutSet(keyword, x, p, is_result_unused);
12,923✔
268
                        break;
16,750✔
269

270
                    case Keyword::Fun:
271
                        compileFunction(x, p, is_result_unused);
3,834✔
272
                        break;
9,190✔
273

274
                    case Keyword::Begin:
275
                    {
276
                        for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
26,555✔
277
                            compileExpression(
63,582✔
278
                                x.list()[i],
21,194✔
279
                                p,
21,194✔
280
                                // All the nodes in a 'begin' (except for the last one) are producing a result that we want to drop.
281
                                /* is_result_unused= */ (i != size - 1) || is_result_unused,
21,194✔
282
                                // If the 'begin' is a terminal node, only its last node is terminal.
283
                                /* is_terminal= */ is_terminal && (i == size - 1));
21,194✔
284
                        break;
5,317✔
285
                    }
1,309✔
286

287
                    case Keyword::While:
288
                        compileWhile(x, p);
1,309✔
289
                        break;
1,310✔
290

291
                    case Keyword::Import:
292
                        compilePluginImport(x, p);
2✔
293
                        break;
4✔
294

295
                    case Keyword::Del:
296
                        page(p).emplace_back(DEL, addSymbol(x.constList()[1]));
2✔
297
                        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
298
                        break;
2✔
299
                }
26,494✔
300
            }
26,435✔
301
            else
302
            {
303
                // If we are here, we should have a function name via the m_opened_vars.
304
                // Push arguments first, then function name, then call it.
305
                handleCalls(x, p, is_result_unused, is_terminal);
23,677✔
306
            }
307
        }
54,174✔
308
        else if (x.nodeType() != NodeType::Unused)
5✔
UNCOV
309
            buildAndThrowError(
×
UNCOV
310
                fmt::format(
×
UNCOV
311
                    "NodeType `{}' not handled in ASTLowerer::compileExpression. Please fill an issue on GitHub: https://github.com/ArkScript-lang/Ark",
×
UNCOV
312
                    typeToString(x)),
×
UNCOV
313
                x);
×
314
    }
110,346✔
315

316
    void ASTLowerer::compileSymbol(const Node& x, const Page p, const bool is_result_unused, const bool can_use_ref)
37,764✔
317
    {
37,764✔
318
        const std::string& name = x.string();
37,764✔
319

320
        if (const auto it_builtin = getBuiltin(name))
75,528✔
321
            page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
3,800✔
322
        else if (getOperator(name).has_value())
33,964✔
323
            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✔
324
        else
325
        {
326
            if (can_use_ref)
33,962✔
327
            {
328
                const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
33,877✔
329
                if (maybe_local_idx.has_value())
33,877✔
330
                    page(p).emplace_back(LOAD_FAST_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
12,655✔
331
                else
332
                    page(p).emplace_back(LOAD_FAST, addSymbol(x));
21,222✔
333
            }
33,877✔
334
            else
335
                page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
85✔
336
        }
337

338
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
37,762✔
339

340
        if (is_result_unused)
37,762✔
341
        {
342
            warning("Statement has no effect", x);
1✔
343
            page(p).emplace_back(POP);
1✔
344
            page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1✔
345
        }
1✔
346
    }
37,764✔
347

348
    void ASTLowerer::compileListInstruction(Node& x, const Page p, const bool is_result_unused)
3,914✔
349
    {
3,914✔
350
        const Node head = x.constList()[0];
3,914✔
351
        const std::string& name = head.string();
3,914✔
352
        const Instruction inst = getListInstruction(name).value();
3,914✔
353

354
        // length of at least 1 since we got a symbol name
355
        const auto argc = x.constList().size() - 1u;
3,914✔
356
        // error, can not use append/concat/pop (and their in place versions) with a <2 length argument list
357
        if (argc < 2 && APPEND <= inst && inst <= SET_AT_2_INDEX)
3,914✔
358
            buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), head);
6✔
359
        if (std::cmp_greater(argc, MaxValue16Bits))
3,908✔
360
            buildAndThrowError(fmt::format("Too many arguments ({}), exceeds {}", argc, MaxValue16Bits), x);
1✔
361
        if (argc != 3 && inst == SET_AT_INDEX)
3,907✔
362
            buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), head);
1✔
363
        if (argc != 4 && inst == SET_AT_2_INDEX)
3,906✔
364
            buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), head);
1✔
365

366
        // compile arguments in reverse order
367
        for (std::size_t i = x.constList().size() - 1u; i > 0; --i)
12,766✔
368
        {
369
            Node& node = x.list()[i];
8,861✔
370
            if (nodeProducesOutput(node))
8,861✔
371
                compileExpression(node, p, false, false);
8,860✔
372
            else
373
                makeError(ErrorKind::InvalidNodeNoReturnValue, node, name);
1✔
374
        }
8,861✔
375

376
        // put inst and number of arguments
377
        std::size_t inst_argc = 0;
3,904✔
378
        switch (inst)
3,904✔
379
        {
2,915✔
380
            case LIST:
381
                inst_argc = argc;
2,915✔
382
                break;
3,754✔
383

384
            case APPEND:
385
                [[fallthrough]];
386
            case APPEND_IN_PLACE:
387
                [[fallthrough]];
388
            case CONCAT:
389
                [[fallthrough]];
390
            case CONCAT_IN_PLACE:
391
                inst_argc = argc - 1;
839✔
392
                break;
845✔
393

394
            case POP_LIST:
395
                inst_argc = 0;
6✔
396
                break;
150✔
397

398
            case SET_AT_INDEX:
399
                [[fallthrough]];
400
            case SET_AT_2_INDEX:
401
                [[fallthrough]];
402
            case POP_LIST_IN_PLACE:
403
                inst_argc = is_result_unused ? 0 : 1;
144✔
404
                break;
144✔
405

406
            default:
UNCOV
407
                break;
×
408
        }
3,904✔
409
        page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
3,904✔
410
        page(p).back().setSourceLocation(head.filename(), head.position().start.line);
3,904✔
411

412
        if (!is_result_unused && (inst == APPEND_IN_PLACE || inst == CONCAT_IN_PLACE))
3,904✔
413
        {
414
            // Load the first argument which should be a symbol (or field),
415
            // that append!/concat! write to, so that we have its new value available.
416
            compileExpression(x.list()[1], p, false, false);
16✔
417
        }
16✔
418

419
        // append!, concat!, pop!, @= and @@= can push to the stack, but not using its returned value isn't an error
420
        if (is_result_unused && (inst == LIST || inst == APPEND || inst == CONCAT || inst == POP_LIST))
3,904✔
421
        {
422
            warning("Ignoring return value of function", x);
8✔
423
            page(p).emplace_back(POP);
8✔
424
        }
8✔
425
    }
3,923✔
426

427
    void ASTLowerer::compileApplyInstruction(Node& x, const Page p, const bool is_result_unused)
189✔
428
    {
189✔
429
        const Node head = x.constList()[0];
189✔
430
        const auto argc = x.constList().size() - 1u;
189✔
431

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

435
        const auto label_return = IR::Entity::Label(m_current_label++);
188✔
436
        page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
188✔
437

438
        for (Node& node : x.list() | std::ranges::views::drop(1))
564✔
439
        {
440
            if (nodeProducesOutput(node))
376✔
441
                compileExpression(node, p, false, false);
375✔
442
            else
443
                makeError(ErrorKind::InvalidNodeNoReturnValue, node, "apply");
1✔
444
        }
376✔
445
        page(p).emplace_back(APPLY);
187✔
446
        // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
447
        page(p).emplace_back(label_return);
187✔
448

449
        if (is_result_unused)
187✔
UNCOV
450
            page(p).emplace_back(POP);
×
451
    }
191✔
452

453
    void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
3,063✔
454
    {
3,063✔
455
        if (x.constList().size() == 1)
3,063✔
456
            buildAndThrowError("Invalid condition: missing 'cond' and 'then' nodes, expected (if cond then)", x);
2✔
457
        if (x.constList().size() == 2)
3,062✔
458
            buildAndThrowError(fmt::format("Invalid condition: missing 'then' node, expected (if {} then)", x.constList()[1].repr()), x);
1✔
459

460
        // compile condition
461
        compileExpression(x.list()[1], p, false, false);
3,061✔
462
        page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
3,061✔
463

464
        // jump only if needed to the "true" branch
465
        const auto label_then = IR::Entity::Label(m_current_label++);
3,061✔
466
        page(p).emplace_back(IR::Entity::GotoIf(label_then, true));
3,061✔
467

468
        // "false" branch code
469
        if (x.constList().size() == 4)  // we have an else clause
3,061✔
470
        {
471
            m_locals_locator.saveScopeLengthForBranch();
2,348✔
472
            compileExpression(x.list()[3], p, is_result_unused, is_terminal);
2,348✔
473
            page(p).back().setSourceLocation(x.constList()[3].filename(), x.constList()[3].position().start.line);
2,348✔
474
            m_locals_locator.dropVarsForBranch();
2,348✔
475
        }
2,348✔
476

477
        // when else is finished, jump to end
478
        const auto label_end = IR::Entity::Label(m_current_label++);
3,061✔
479
        page(p).emplace_back(IR::Entity::Goto(label_end));
3,061✔
480

481
        // absolute address to jump to if condition is true
482
        page(p).emplace_back(label_then);
3,061✔
483
        // if code
484
        m_locals_locator.saveScopeLengthForBranch();
3,061✔
485
        compileExpression(x.list()[2], p, is_result_unused, is_terminal);
3,061✔
486
        page(p).back().setSourceLocation(x.constList()[2].filename(), x.constList()[2].position().start.line);
3,061✔
487
        m_locals_locator.dropVarsForBranch();
3,061✔
488
        // set jump to end pos
489
        page(p).emplace_back(label_end);
3,061✔
490
    }
3,063✔
491

492
    void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused)
3,834✔
493
    {
3,834✔
494
        if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List)
3,836✔
495
            buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args);
1✔
496
        if (x.constList().size() != 3)
3,833✔
497
            makeError(ErrorKind::InvalidNodeMacro, x, "");
1✔
498

499
        // capture, if needed
500
        std::size_t capture_inst_count = 0;
3,832✔
501
        for (const auto& node : x.constList()[1].constList())
9,999✔
502
        {
503
            if (node.nodeType() == NodeType::Capture)
6,167✔
504
            {
505
                const uint16_t symbol_id = addSymbol(node);
227✔
506

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

513
                    page(p).emplace_back(RENAME_NEXT_CAPTURE, nqn_id);
14✔
514
                    page(p).emplace_back(CAPTURE, symbol_id);
14✔
515
                }
14✔
516
                else
517
                    page(p).emplace_back(CAPTURE, symbol_id);
213✔
518

519
                ++capture_inst_count;
227✔
520
            }
227✔
521
        }
6,167✔
522
        const bool is_closure = capture_inst_count > 0;
3,832✔
523

524
        m_locals_locator.createScope(
7,664✔
525
            is_closure
3,832✔
526
                ? LocalsLocator::ScopeType::Closure
527
                : LocalsLocator::ScopeType::Function);
528

529
        // create new page for function body
530
        const auto function_body_page = createNewCodePage();
3,832✔
531
        // save page_id into the constants table as PageAddr and load the const
532
        page(p).emplace_back(is_closure ? MAKE_CLOSURE : LOAD_CONST, addValue(function_body_page.index, x));
3,832✔
533

534
        // pushing arguments from the stack into variables in the new scope
535
        for (const auto& node : x.constList()[1].constList() | std::ranges::views::reverse)
9,999✔
536
        {
537
            if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg)
6,167✔
538
            {
539
                page(function_body_page).emplace_back(STORE, addSymbol(node));
5,085✔
540
                m_locals_locator.addLocal(node.string());
5,085✔
541
            }
5,085✔
542
            else if (node.nodeType() == NodeType::RefArg)
1,082✔
543
            {
544
                page(function_body_page).emplace_back(STORE_REF, addSymbol(node));
855✔
545
                m_locals_locator.addLocal(node.string());
855✔
546
            }
855✔
547
        }
6,167✔
548

549
        // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls.
550
        // This way we can continue to safely apply optimisations on
551
        // (let name (fun (e) (map lst (fun (e) (name e)))))
552
        // Otherwise, `name` would have been optimized to a CALL_CURRENT_PAGE, which would have returned the wrong page.
553
        if (x.isAnonymousFunction())
3,832✔
554
            m_opened_vars.emplace("#anonymous");
402✔
555
        // push body of the function
556
        compileExpression(x.list()[2], function_body_page, false, true);
3,832✔
557
        if (x.isAnonymousFunction())
3,832✔
558
            m_opened_vars.pop();
402✔
559

560
        // return last value on the stack
561
        page(function_body_page).emplace_back(RET);
3,832✔
562
        m_locals_locator.deleteScope();
3,832✔
563

564
        // if the computed function is unused, pop it
565
        if (is_result_unused)
3,832✔
566
        {
567
            warning("Unused declared function", x);
1✔
568
            page(p).emplace_back(POP);
1✔
569
        }
1✔
570
    }
3,834✔
571

572
    void ASTLowerer::compileLetMutSet(const Keyword n, Node& x, const Page p, const bool is_result_unused)
12,923✔
573
    {
12,923✔
574
        if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol)
12,930✔
UNCOV
575
            buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym);
×
576
        if (x.constList().size() != 3)
12,923✔
577
            makeError(ErrorKind::InvalidNodeMacro, x, "");
1✔
578

579
        const std::string name = x.constList()[1].string();
12,922✔
580
        uint16_t i = addSymbol(x.constList()[1]);
12,922✔
581

582
        if (!m_opened_vars.empty() && m_opened_vars.top() == name)
12,922✔
583
            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✔
584

585
        const bool is_function = x.constList()[2].isFunction();
12,921✔
586
        if (is_function)
12,921✔
587
        {
588
            m_opened_vars.push(name);
3,432✔
589
            x.list()[2].setFunctionKind(/* anonymous= */ false);
3,432✔
590
        }
3,432✔
591

592
        // put value before symbol id
593
        // starting at index = 2 because x is a (let|mut|set variable ...) node
594
        compileExpression(x.list()[2], p, false, false);
12,921✔
595

596
        if (n == Keyword::Let || n == Keyword::Mut)
12,916✔
597
        {
598
            page(p).emplace_back(STORE, i);
8,277✔
599
            m_locals_locator.addLocal(name);
8,277✔
600

601
            if (!is_result_unused)
8,277✔
602
                page(p).emplace_back(LOAD_FAST_BY_INDEX, 0);
2✔
603
        }
8,277✔
604
        else
605
        {
606
            page(p).emplace_back(SET_VAL, i);
4,639✔
607

608
            if (!is_result_unused)
4,639✔
609
                page(p).emplace_back(LOAD_FAST, i);
38✔
610
        }
611

612
        if (is_function)
12,916✔
613
            m_opened_vars.pop();
3,427✔
614
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
12,916✔
615
    }
12,924✔
616

617
    void ASTLowerer::compileWhile(Node& x, const Page p)
1,309✔
618
    {
1,309✔
619
        if (x.constList().size() != 3)
1,309✔
620
            makeError(ErrorKind::InvalidNodeMacro, x, "");
1✔
621

622
        m_locals_locator.createScope();
1,308✔
623
        page(p).emplace_back(CREATE_SCOPE);
1,308✔
624
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
1,308✔
625

626
        // save current position to jump there at the end of the loop
627
        const auto label_loop = IR::Entity::Label(m_current_label++);
1,308✔
628
        page(p).emplace_back(label_loop);
1,308✔
629
        // push condition
630
        compileExpression(x.list()[1], p, false, false);
1,308✔
631
        // absolute jump to end of block if condition is false
632
        const auto label_end = IR::Entity::Label(m_current_label++);
1,308✔
633
        page(p).emplace_back(IR::Entity::GotoIf(label_end, false));
1,308✔
634
        // push code to page
635
        compileExpression(x.list()[2], p, true, false);
1,308✔
636

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

643
        // absolute address to jump to if condition is false
644
        page(p).emplace_back(label_end);
1,308✔
645

646
        page(p).emplace_back(POP_SCOPE);
1,308✔
647
        m_locals_locator.deleteScope();
1,308✔
648
    }
1,309✔
649

650
    void ASTLowerer::compilePluginImport(const Node& x, const Page p)
2✔
651
    {
2✔
652
        std::string path;
2✔
653
        const Node package_node = x.constList()[1];
2✔
654
        for (std::size_t i = 0, end = package_node.constList().size(); i < end; ++i)
4✔
655
        {
656
            path += package_node.constList()[i].string();
2✔
657
            if (i + 1 != end)
2✔
UNCOV
658
                path += "/";
×
659
        }
2✔
660
        path += ".arkm";
2✔
661

662
        // register plugin path in the constants table
663
        uint16_t id = addValue(Node(NodeType::String, path));
2✔
664
        // add plugin instruction + id of the constant referring to the plugin path
665
        page(p).emplace_back(PLUGIN, id);
2✔
666
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
2✔
667
    }
2✔
668

669
    void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call)
8,852✔
670
    {
8,852✔
671
        const auto node = call.constList()[0];
8,852✔
672

673
        for (Node& value : std::ranges::drop_view(call.list(), 1))
24,584✔
674
        {
675
            if (nodeProducesOutput(value) || isBreakpoint(value))
15,732✔
676
            {
677
                // 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
678
                if (value.nodeType() == NodeType::Symbol && is_tail_call)
15,730✔
679
                    compileSymbol(value, p, /* is_result_unused= */ false, /* can_use_ref= */ false);
85✔
680
                else
681
                    compileExpression(value, p, /* is_result_unused= */ false, /* is_terminal= */ false);
15,645✔
682
            }
15,724✔
683
            else
684
                makeError(is_tail_call ? ErrorKind::InvalidNodeInTailCallNoReturnValue : ErrorKind::InvalidNodeNoReturnValue, value, node.repr());
2✔
685
        }
15,732✔
686
    }
8,860✔
687

688
    void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal)
23,648✔
689
    {
23,648✔
690
        const Node& node = x.constList()[0];
23,648✔
691
        bool matched = false;
23,648✔
692

693
        if (node.nodeType() == NodeType::Symbol)
23,648✔
694
        {
695
            if (node.string() == Language::And || node.string() == Language::Or)
22,913✔
696
            {
697
                matched = true;
511✔
698
                handleShortcircuit(x, p);
511✔
699
            }
511✔
700
            if (const auto maybe_operator = getOperator(node.string()); maybe_operator.has_value())
37,223✔
701
            {
702
                matched = true;
14,310✔
703
                if (maybe_operator.value() == BREAKPOINT)
14,310✔
704
                    is_result_unused = false;
17✔
705
                handleOperator(x, p, maybe_operator.value());
14,310✔
706
            }
14,310✔
707
        }
22,913✔
708

709
        if (!matched)
23,648✔
710
        {
711
            // if nothing else matched, then compile a function call
712
            if (handleFunctionCall(x, p, is_terminal))
8,844✔
713
                // if it returned true, we compiled a tail call, skip the POP at the end
714
                return;
113✔
715
        }
8,731✔
716

717
        if (is_result_unused)
23,535✔
718
            page(p).emplace_back(POP);
3,857✔
719
    }
23,648✔
720

721
    void ASTLowerer::handleShortcircuit(Node& x, const Page p)
511✔
722
    {
511✔
723
        const Node& node = x.constList()[0];
511✔
724
        const auto name = node.string();  // and / or
511✔
725
        const Instruction inst = name == Language::And ? SHORTCIRCUIT_AND : SHORTCIRCUIT_OR;
511✔
726

727
        // short circuit implementation
728
        if (x.constList().size() < 3)
511✔
729
            buildAndThrowError(
2✔
730
                fmt::format(
4✔
731
                    "Expected at least 2 arguments while compiling '{}', got {}",
2✔
732
                    name,
733
                    x.constList().size() - 1),
2✔
734
                x);
2✔
735

736
        if (!nodeProducesOutput(x.list()[1]))
509✔
737
            buildAndThrowError(
×
738
                fmt::format(
×
739
                    "Can not use `{}' inside a `{}' expression, as it doesn't return a value",
×
740
                    x.list()[1].repr(), name),
×
UNCOV
741
                x.list()[1]);
×
742
        compileExpression(x.list()[1], p, false, false);
509✔
743

744
        const auto label_shortcircuit = IR::Entity::Label(m_current_label++);
509✔
745
        auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, inst);
509✔
746
        page(p).emplace_back(shortcircuit_entity);
509✔
747

748
        for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
1,143✔
749
        {
750
            if (!nodeProducesOutput(x.list()[i]))
634✔
751
                buildAndThrowError(
1✔
752
                    fmt::format(
2✔
753
                        "Can not use `{}' inside a `{}' expression, as it doesn't return a value",
1✔
754
                        x.list()[i].repr(), name),
1✔
755
                    x.list()[i]);
1✔
756
            compileExpression(x.list()[i], p, false, false);
633✔
757
            if (i + 1 != end)
633✔
758
                page(p).emplace_back(shortcircuit_entity);
125✔
759
        }
633✔
760

761
        page(p).emplace_back(label_shortcircuit);
508✔
762
    }
514✔
763

764
    void ASTLowerer::handleOperator(Node& x, const Page p, const Instruction op)
14,310✔
765
    {
14,310✔
766
        constexpr std::size_t start_index = 1;
14,310✔
767
        const Node& node = x.constList()[0];
14,310✔
768
        const auto op_name = Language::operators[static_cast<std::size_t>(op - FIRST_OPERATOR)];
14,310✔
769

770

771
        // push arguments on current page
772
        std::size_t exp_count = 0;
14,310✔
773
        for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
40,403✔
774
        {
775
            const bool is_breakpoint = isBreakpoint(x.constList()[index]);
26,093✔
776
            if (nodeProducesOutput(x.constList()[index]) || is_breakpoint)
26,093✔
777
                compileExpression(x.list()[index], p, false, false);
26,092✔
778
            else
779
                makeError(ErrorKind::InvalidNodeInOperatorNoReturnValue, x.constList()[index], node.repr());
1✔
780

781
            if (!is_breakpoint)
26,092✔
782
                exp_count++;
26,090✔
783

784
            // in order to be able to handle things like (op A B C D...)
785
            // which should be transformed into A B op C op D op...
786
            if (exp_count >= 2 && !isTernaryInst(op) && !is_breakpoint)
26,092✔
787
                page(p).emplace_back(op);
11,574✔
788
        }
26,093✔
789

790
        if (isBreakpoint(x))
14,309✔
791
        {
792
            if (exp_count > 1)
17✔
793
                buildAndThrowError(fmt::format("`{}' expected at most one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
794
            page(p).emplace_back(op, exp_count);
16✔
795
        }
16✔
796
        else if (isUnaryInst(op))
14,292✔
797
        {
798
            if (exp_count != 1)
2,749✔
799
                buildAndThrowError(fmt::format("`{}' expected one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
800
            page(p).emplace_back(op);
2,748✔
801
        }
2,748✔
802
        else if (isTernaryInst(op))
11,543✔
803
        {
804
            if (exp_count != 3)
107✔
805
                buildAndThrowError(fmt::format("`{}' expected three arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
1✔
806
            page(p).emplace_back(op);
106✔
807
        }
106✔
808
        else if (exp_count <= 1)
11,436✔
809
            buildAndThrowError(fmt::format("`{}' expected two arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
2✔
810

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

815
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
14,296✔
816
    }
14,310✔
817

818
    bool ASTLowerer::handleFunctionCall(Node& x, const Page p, const bool is_terminal)
8,855✔
819
    {
8,855✔
820
        constexpr std::size_t start_index = 1;
8,855✔
821
        Node& node = x.list()[0];
8,855✔
822

823
        if (is_terminal && node.nodeType() == NodeType::Symbol && isFunctionCallingItself(node.string()))
8,855✔
824
        {
825
            pushFunctionCallArguments(x, p, /* is_tail_call= */ true);
113✔
826

827
            // jump to the top of the function
828
            page(p).emplace_back(TAIL_CALL_SELF);
113✔
829
            page(p).back().setSourceLocation(node.filename(), node.position().start.line);
113✔
830
            return true;  // skip the potential Instruction::POP at the end
113✔
831
        }
832

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

836
        const IR::Entity label_return = IR::Entity::Label(m_current_label++);
8,740✔
837
        page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
8,740✔
838
        page(p).back().setSourceLocation(x.filename(), x.position().start.line);
8,740✔
839

840
        const Page proc_page = createNewCodePage(/* temp= */ true);
8,740✔
841
        CallType call_type = CallType::Classic;
8,740✔
842
        std::optional<uint16_t> call_arg = std::nullopt;
8,740✔
843

844
        // compile the function resolution to a separate page
845
        if (node.nodeType() == NodeType::Symbol && isFunctionCallingItself(node.string()))
8,740✔
846
        {
847
            // The function is trying to call itself, but this isn't a tail call.
848
            // We can skip the LOAD_FAST function_name and directly push the current
849
            // function page, which will be quicker than a local variable resolution.
850
            // We set its argument to the symbol id of the function we are calling,
851
            // so that the VM knows the name of the last called function.
852
            call_type = CallType::SelfNotRecursive;
57✔
853
        }
57✔
854
        else
855
        {
856
            // closure chains have been handled (eg: closure.field.field.function)
857
            compileExpression(node, proc_page, false, false);  // storing proc
8,683✔
858

859
            if (page(proc_page).empty())
8,681✔
UNCOV
860
                buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x);
×
861
            else if (page(proc_page).back().inst() == GET_FIELD)
8,681✔
862
                // the last GET_FIELD instruction should push the closure environment with it
863
                page(proc_page).back().replaceInstruction(GET_FIELD_AS_CLOSURE);
731✔
864
            else if (page(proc_page).size() == 1)
7,950✔
865
            {
866
                const Instruction inst = page(proc_page).back().inst();
7,941✔
867
                const uint16_t arg = page(proc_page).back().primaryArg();
7,941✔
868

869
                if (inst == LOAD_FAST)
7,941✔
870
                {
871
                    call_type = CallType::Symbol;
5,612✔
872
                    call_arg = arg;
5,612✔
873
                    // we don't want to push any instruction, as we'll use an optimised instruction instead of CALL
874
                    page(proc_page).clear();
5,612✔
875
                }
5,612✔
876
                else if (inst == LOAD_FAST_BY_INDEX)
2,329✔
877
                {
878
                    call_type = CallType::SymbolByIndex;
432✔
879
                    call_arg = arg;
432✔
880
                    page(proc_page).clear();
432✔
881
                }
432✔
882
                else if (inst == BUILTIN && Builtins::builtins[arg].second.isFunction())
1,897✔
883
                {
884
                    call_type = CallType::Builtin;
1,874✔
885
                    call_arg = arg;
1,874✔
886
                    page(proc_page).clear();
1,874✔
887
                }
1,874✔
888
            }
7,941✔
889
        }
890

891
        // push proc from temp page
892
        for (const auto& inst : m_temp_pages.back())
10,264✔
893
            page(p).push_back(inst);
1,526✔
894
        m_temp_pages.pop_back();
8,738✔
895

896
        pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
8,738✔
897

898
        // number of arguments
899
        std::size_t args_count = 0;
8,731✔
900
        for (auto it = x.constList().begin() + start_index, it_end = x.constList().end(); it != it_end; ++it)
24,174✔
901
        {
902
            if (it->nodeType() != NodeType::Capture && !isBreakpoint(*it))
15,443✔
903
                args_count++;
15,439✔
904
        }
15,443✔
905

906
        // call the procedure
907
        switch (call_type)
8,731✔
908
        {
763✔
909
            case CallType::Classic:
910
                page(p).emplace_back(CALL, args_count);
763✔
911
                break;
820✔
912

913
            case CallType::SelfNotRecursive:
914
                page(p).emplace_back(CALL_CURRENT_PAGE, addSymbol(node), args_count);
57✔
915
                break;
5,669✔
916

917
            case CallType::Symbol:
918
                assert(call_arg.has_value() && "Expected a value for call_arg with CallType::Symbol");
5,612✔
919
                page(p).emplace_back(CALL_SYMBOL, call_arg.value(), args_count);
5,612✔
920
                break;
6,043✔
921

922
            case CallType::SymbolByIndex:
923
                assert(call_arg.has_value() && "Expected a value for call_arg with CallType::SymbolByIndex");
431✔
924
                page(p).emplace_back(CALL_SYMBOL_BY_INDEX, call_arg.value(), args_count);
431✔
925
                break;
2,299✔
926

927
            case CallType::Builtin:
928
                assert(call_arg.has_value() && "Expected a value for call_arg with CallType::Builtin");
1,868✔
929
                page(p).emplace_back(CALL_BUILTIN, call_arg.value(), args_count);
1,868✔
930
                break;
1,868✔
931
        }
8,731✔
932
        page(p).back().setSourceLocation(node.filename(), node.position().start.line);
8,731✔
933

934
        // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
935
        page(p).emplace_back(label_return);
8,731✔
936
        return false;  // we didn't compile a tail call
8,731✔
937
    }
8,857✔
938

939
    uint16_t ASTLowerer::addSymbol(const Node& sym)
42,300✔
940
    {
42,300✔
941
        // otherwise, add the symbol, and return its id in the table
942
        auto it = std::ranges::find(m_symbols, sym.string());
42,300✔
943
        if (it == m_symbols.end())
42,300✔
944
        {
945
            m_symbols.push_back(sym.string());
7,487✔
946
            it = m_symbols.begin() + static_cast<std::vector<std::string>::difference_type>(m_symbols.size() - 1);
7,487✔
947
        }
7,487✔
948

949
        const auto distance = std::distance(m_symbols.begin(), it);
42,300✔
950
        if (std::cmp_less(distance, MaxValue16Bits))
42,300✔
951
            return static_cast<uint16_t>(distance);
84,600✔
UNCOV
952
        buildAndThrowError(fmt::format("Too many symbols (exceeds {}), aborting compilation.", MaxValue16Bits), sym);
×
953
    }
42,300✔
954

955
    uint16_t ASTLowerer::addValue(const Node& x)
18,254✔
956
    {
18,254✔
957
        const ValTableElem v(x);
18,254✔
958
        auto it = std::ranges::find(m_values, v);
18,254✔
959
        if (it == m_values.end())
18,254✔
960
        {
961
            m_values.push_back(v);
4,024✔
962
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
4,024✔
963
        }
4,024✔
964

965
        const auto distance = std::distance(m_values.begin(), it);
18,254✔
966
        if (std::cmp_less(distance, MaxValue16Bits))
18,254✔
967
            return static_cast<uint16_t>(distance);
18,254✔
UNCOV
968
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), x);
×
969
    }
18,254✔
970

971
    uint16_t ASTLowerer::addValue(const std::size_t page_id, const Node& current)
3,832✔
972
    {
3,832✔
973
        const ValTableElem v(page_id);
3,832✔
974
        auto it = std::ranges::find(m_values, v);
3,832✔
975
        if (it == m_values.end())
3,832✔
976
        {
977
            m_values.push_back(v);
3,832✔
978
            it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
3,832✔
979
        }
3,832✔
980

981
        const auto distance = std::distance(m_values.begin(), it);
3,832✔
982
        if (std::cmp_less(distance, MaxValue16Bits))
3,832✔
983
            return static_cast<uint16_t>(distance);
3,832✔
UNCOV
984
        buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), current);
×
985
    }
3,832✔
986
}
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