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

ArkScript-lang / Ark / 29761103967

20 Jul 2026 04:47PM UTC coverage: 94.115% (-0.3%) from 94.385%
29761103967

Pull #705

github

web-flow
Merge d1df3d7ce into 00cfbe903
Pull Request #705: Feat/inliner

413 of 459 new or added lines in 11 files covered. (89.98%)

11 existing lines in 2 files now uncovered.

10667 of 11334 relevant lines covered (94.12%)

1164822.98 hits per line

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

93.25
/src/arkreactor/Compiler/IntermediateRepresentation/IRInliner.cpp
1
#include <Ark/Compiler/IntermediateRepresentation/IRInliner.hpp>
2

3
#include <algorithm>
4
#include <cassert>
5
#include <limits>
6

7
namespace Ark::internal
8
{
9
    IRInliner::IRInliner(const unsigned debug) :
1,108✔
10
        Pass("IRInliner", debug),
554✔
11
        m_current_label(0)
554✔
12
    {}
1,662✔
13

14
    void IRInliner::process(const std::vector<IR::Block>& pages, const std::vector<std::string>& symbols, const std::vector<ValTableElem>& values, const IR::label_t last_label)
380✔
15
    {
380✔
16
        m_logger.traceStart("process");
380✔
17
        m_symbols = symbols;
380✔
18
        m_values = values;
380✔
19
        m_current_label = last_label + 1;
380✔
20

21
        extractPagesMetadata(pages);
380✔
22

23
        // TODO: we'll need to move some page index if a page is removed!
24
        // TODO: some pages could be removed if they are inlined everywhere!
25
        // TODO: should we start from the end to inline as much as we can? or do we want to inline the user code first, then the stdlib?
26
        for (const auto& block : pages)
8,379✔
27
        {
28
            IR::Block new_block {
15,998✔
29
                .metadata = {
55,993✔
30
                    .name = block.metadata.name,
7,999✔
31
                    .argument_count = block.metadata.argument_count,
7,999✔
32
                    .addr = block.metadata.addr,
7,999✔
33
                    .is_closure = block.metadata.is_closure,
7,999✔
34
                    .is_recursive = block.metadata.is_recursive,
7,999✔
35
                    .is_simple = block.metadata.is_simple },
7,999✔
36
                .data = {}
7,999✔
37
            };
38

39
            // We only have to deal with CALL_SYMBOL, CALL_SYMBOL_BY_INDEX, which deal with symbols,
40
            // and CALL which can deal with constant ids (eg `((fun (a) (print a)) 5)`)
41
            for (std::size_t i = 0, end = block.data.size(); i < end; ++i)
297,412✔
42
            {
43
                const auto& entity = block.data[i];
289,413✔
44

45
                std::optional<uint16_t> maybe_id;
289,413✔
46
                std::size_t argc = std::numeric_limits<std::size_t>::max();
289,413✔
47
                CallKind kind = CallKind::Symbol;
289,413✔
48

49
                if (entity.inst() == CALL_SYMBOL || entity.inst() == CALL_SYMBOL_BY_INDEX)
289,413✔
50
                {
51
                    maybe_id = entity.originalSymbolId();
11,347✔
52
                    argc = entity.secondaryArg();
11,347✔
53
                }
11,347✔
54
                else if (entity.inst() == CALL)
278,066✔
55
                {
56
                    // TODO: find the function being called, check if it's a constant, then if we know it
57
                    maybe_id = {};
1,048✔
58
                    argc = entity.primaryArg();
1,048✔
59
                    kind = CallKind::Constant;
1,048✔
60
                }
1,048✔
61

62
                if (const auto maybe_block = blockToInlineInCall(kind, pages, maybe_id, block, argc); maybe_block.has_value())
578,826✔
63
                {
64
                    const IR::Block& inlinee = pages[maybe_block->addr];
3,137✔
65

66
                    // retrieve the return label of the call instruction, to know which PUSH_RETURN_ADDRESS instruction we'll have to remove
67
                    if (i + 1 < end)
3,137✔
68
                    {
69
                        assert(block.data[i + 1].kind() == IR::Kind::Label && "Expected a label right after the CALL instruction! The AST lowerer messed up somewhere");
3,137✔
70

71
                        const IR::label_t return_label = block.data[i + 1].label();
3,137✔
72
                        const std::size_t removed = std::erase_if(new_block.data, [return_label](const IR::Entity& e) -> bool {
1,062,980✔
73
                            return e.kind() == IR::Kind::Goto && e.inst() == PUSH_RETURN_ADDRESS && e.label() == return_label;
1,059,843✔
74
                        });
75

76
                        if (removed == 0)
3,137✔
NEW
77
                            throw std::runtime_error(fmt::format("No PUSH_RETURN_ADDRESS L{} instruction removed, even though one was expected", return_label));
×
78
                    }
3,137✔
79

80
                    inlineBlock(inlinee, new_block);
3,137✔
81
                }
3,137✔
82
                else
83
                    new_block.data.emplace_back(entity);
286,276✔
84
            }
289,413✔
85

86
            m_ir.emplace_back(new_block);
7,999✔
87
        }
7,999✔
88

89
        m_logger.traceEnd();
380✔
90
    }
380✔
91

92
    const std::vector<IR::Block>& IRInliner::intermediateRepresentation() const noexcept
380✔
93
    {
380✔
94
        return m_ir;
380✔
95
    }
96

97
    bool IRInliner::canBeInlined(const IR::Block& candidate, const IR::Block& source, const std::size_t argc) noexcept
10,144✔
98
    {
10,144✔
99
        const std::size_t candidate_inst_count = candidate.instructionCount(),
10,144✔
100
                          source_inst_count = source.instructionCount();
10,144✔
101

102
        if (candidate.metadata.is_closure ||
20,288✔
103
            candidate.metadata.is_recursive ||
10,144✔
104
            candidate.metadata.is_mutating_args ||
10,043✔
105
            candidate.metadata.argument_count != argc ||
9,775✔
106
            candidate.metadata.name.value_or(std::string(IR::AnonymousBlockName)) == IR::AnonymousBlockName ||
9,770✔
107
            std::cmp_greater_equal(candidate_inst_count + source_inst_count, MaxValue16Bits))
9,770✔
108
            return false;
374✔
109
        // TODO: create a proper constant and make this less arbitrary?
110
        return candidate.metadata.is_simple && candidate_inst_count < 24;
9,770✔
111
    }
10,144✔
112

113
    std::optional<BlockInfo> IRInliner::blockToInlineInCall(
289,413✔
114
        const CallKind kind,
115
        const std::vector<IR::Block>& pages,
116
        const std::optional<uint16_t> maybe_id,
117
        const IR::Block& current,
118
        const std::size_t argc) const noexcept
119
    {
289,413✔
120
        if (!maybe_id.has_value())
289,413✔
121
            return std::nullopt;
278,066✔
122

123
        const uint16_t id = maybe_id.value();
11,347✔
124
        std::optional<BlockInfo> maybe_block = findBlockBy(kind, id);
11,347✔
125
        if (!maybe_block.has_value())
11,347✔
126
            return std::nullopt;
1,149✔
127

128
        // If we are trying to inline a function call that seems to have multiple declarations,
129
        // abort. We can't be sure that we are inlining the correct version at the moment.
130
        if (kind == CallKind::Symbol && m_symbols_data.contains(id) && m_symbols_data.at(id).declarations_count != 1)
10,198✔
131
            return std::nullopt;
54✔
132

133
        const std::size_t block_addr = maybe_block->addr;
10,144✔
134
        if (canBeInlined(pages[block_addr], current, argc))
10,144✔
135
            return maybe_block;
3,137✔
136
        return std::nullopt;
7,007✔
137
    }
289,413✔
138

139
    std::optional<IR::Entity> IRInliner::isBuiltinProxy(const IR::Block& block)
3,137✔
140
    {
3,137✔
141
        /*
142
         Expected instructions to be a builtin proxy:
143
            STORE...
144
            PUSH_RETURN_ADDRESS
145
            LOAD_FAST_BY_INDEX...
146
            CALL_BUILTIN
147
            <label>
148
            RET
149
         */
150
        if (block.data.size() < 6 || (block.data.size() - 4) % 2 != 0)
3,137✔
151
            return std::nullopt;
297✔
152

153
        Instruction expected = STORE;
2,840✔
154
        std::size_t store_count = 0;
2,840✔
155
        std::size_t load_count = 0;
2,840✔
156
        IR::label_t label = 0;
2,840✔
157
        std::optional<IR::Entity> call_builtin;
2,840✔
158

159
        for (const auto& entity : block.data)
25,001✔
160
        {
161
            const Instruction inst = entity.inst();
22,161✔
162
            const bool is_label = entity.kind() == IR::Kind::Label;
22,161✔
163

164
            if (expected != inst)
22,161✔
165
            {
166
                if (expected == STORE)
15,909✔
167
                    expected = PUSH_RETURN_ADDRESS;
2,840✔
168
                else if (expected == PUSH_RETURN_ADDRESS)
13,069✔
169
                    expected = LOAD_FAST_BY_INDEX;
2,840✔
170
                else if (expected == LOAD_FAST_BY_INDEX)
10,229✔
171
                    expected = CALL_BUILTIN;
2,840✔
172
                else if (expected == CALL_BUILTIN)
7,389✔
173
                    expected = NOP;
2,840✔
174
                else if (expected == NOP)
4,549✔
175
                    expected = RET;
2,834✔
176
            }
15,909✔
177

178
            if (expected == inst)
22,161✔
179
            {
180
                if (expected == STORE)
18,606✔
181
                    ++store_count;
4,648✔
182
                else if (expected == LOAD_FAST_BY_INDEX)
13,958✔
183
                    ++load_count;
4,357✔
184
                else if (expected == CALL_BUILTIN)
9,601✔
185
                {
186
                    call_builtin = entity;
2,374✔
187
                    if (entity.secondaryArg() != store_count)
2,374✔
NEW
188
                        return std::nullopt;
×
189
                }
2,374✔
190
                else if (expected == PUSH_RETURN_ADDRESS)
7,227✔
191
                    label = entity.label();
2,397✔
192
            }
18,606✔
193
            else if (is_label && label != entity.label())
3,555✔
194
                return std::nullopt;
378✔
195
        }
22,161✔
196

197
        if (store_count == load_count)
2,462✔
198
            return call_builtin;
2,399✔
199
        return std::nullopt;
63✔
200
    }
3,137✔
201

202
    void IRInliner::inlineBlock(const IR::Block& inlinee, IR::Block& destination)
3,137✔
203
    {
3,137✔
204
        // todo: redo the filename lookup
205
        if (destination.metadata.addr == 0)
3,137✔
206
            m_logger.info("Inlining call to '{}' ({}) inside global scope", inlinee.debugName(), inlinee.metadataRepr());
47✔
207
        else
208
            m_logger.info(
12,360✔
209
                "Inlining call to '{}' ({} from '{}') inside '{}' @ {}, from '{}'",
210
                inlinee.debugName(),
3,090✔
211
                inlinee.metadataRepr(),
3,090✔
212
                inlinee.data.front().filename(),
3,090✔
213
                destination.debugName(),
3,090✔
214
                destination.metadata.addr,
3,090✔
215
                destination.data.front().filename());
3,090✔
216

217
        if (auto inst = isBuiltinProxy(inlinee); inst.has_value())
5,511✔
218
        {
219
            m_logger.info("  -> builtin proxy with args ({}, {})", inst->primaryArg(), inst->secondaryArg());
2,374✔
220
            destination.data
4,748✔
221
                .emplace_back(CALL_BUILTIN_WITHOUT_RETURN_ADDRESS, inst->primaryArg(), inst->secondaryArg())
2,374✔
222
                .setSourceLocation(inst->filename(), inst->sourceLine());
2,374✔
223
            return;
2,374✔
224
        }
225

226
        // TODO: do a better inlining job
227
        // TODO: decide if we want to keep create_scope, (inlinee), pop_scope
228
        destination.data.emplace_back(CREATE_SCOPE);
763✔
229

230
        // We need to create new, unique labels for the inlined code.
231
        // When we meet a label, we'll register it, and replace it with a new label
232
        // in the inlined code. That way, if we find it again later in the code,
233
        // we can use the correct value.
234
        std::unordered_map<IR::label_t, IR::label_t> old_to_new_label;
763✔
235

236
        for (const IR::Entity& entity : inlinee.data)
10,368✔
237
        {
238
            if (entity.inst() == RET)
9,605✔
239
                break;
763✔
240

241
            if (entity.hasLabel())
8,842✔
242
            {
243
                IR::Entity labelled_entity = entity;
2,566✔
244
                if (auto it = old_to_new_label.find(entity.label()); it != old_to_new_label.end())
5,132✔
245
                    labelled_entity.replaceLabel(it->second);
1,283✔
246
                else
247
                {
248
                    labelled_entity.replaceLabel(m_current_label);
1,283✔
249
                    old_to_new_label[entity.label()] = m_current_label++;
1,283✔
250
                }
251

252
                destination.data.emplace_back(labelled_entity);
2,566✔
253
            }
2,566✔
254
            else if (entity.inst() == LOAD_FAST_BY_INDEX)
6,276✔
255
                destination.data
4,978✔
256
                    .emplace_back(LOAD_FAST, entity.originalSymbolId().value())
2,489✔
257
                    .setSourceLocation(entity.filename(), entity.sourceLine());
2,489✔
258
            else if (entity.inst() == CALL_SYMBOL_BY_INDEX)
3,787✔
NEW
259
                destination.data
×
NEW
260
                    .emplace_back(CALL_SYMBOL, entity.originalSymbolId().value(), entity.secondaryArg())
×
NEW
261
                    .setSourceLocation(entity.filename(), entity.sourceLine());
×
262
            else
263
                destination.data.emplace_back(entity);
3,787✔
264
        }
9,605✔
265

266
        destination.data.emplace_back(POP_SCOPE, 1);
763✔
267
    }
3,137✔
268

269
    void IRInliner::extractPagesMetadata(const std::vector<IR::Block>& pages)
380✔
270
    {
380✔
271
        for (std::size_t i = 0, end = pages.size(); i < end; ++i)
8,379✔
272
        {
273
            const std::string& name = pages[i].debugName();
7,999✔
274
            if (name != IR::AnonymousBlockName)
7,999✔
275
            {
276
                const auto it_val = std::ranges::find_if(m_values, [i](const ValTableElem& elem) -> bool {
1,500,561✔
277
                    return elem.type == ValTableElemType::PageAddr && std::get<std::size_t>(elem.value) == i;
1,493,559✔
278
                });
279
                assert(it_val != m_values.end() && "Could not find a constant referencing the current page!");
7,002✔
280

281
                const auto it_sym = std::ranges::find_if(m_symbols, [&name](const std::string& sym) -> bool {
1,194,051✔
282
                    return name == sym;
1,187,049✔
283
                });
284

285
                if (it_sym != m_symbols.end())
7,002✔
286
                {
287
                    m_symbols_data[std::distance(m_symbols.begin(), it_sym)] = SymbolData {
14,004✔
288
                        .name = name,
7,002✔
289
                        .declarations_count = 0,
290
                        .use_count = 0
291
                    };
292
                }
7,002✔
293

294
                m_funcs.emplace_back(BlockInfo {
35,010✔
295
                    .constant_id = static_cast<long>(std::distance(m_values.begin(), it_val)),
7,002✔
296
                    .addr = i,
7,002✔
297
                    .name = pages[i].debugName(),
7,002✔
298
                    .symbol_id = it_sym == m_symbols.end()
14,004✔
NEW
299
                        ? std::nullopt
×
300
                        : std::make_optional(std::distance(m_symbols.begin(), it_sym)) });
7,002✔
301
            }
7,002✔
302
        }
7,999✔
303

304
        for (const IR::Block& page : pages)
8,379✔
305
        {
306
            for (const IR::Entity& entity : page.data)
297,412✔
307
            {
308
                switch (entity.inst())
289,413✔
309
                {
41,030✔
310
                    // use, primary, id
311
                    case CALL_SYMBOL: [[fallthrough]];
312
                    case LOAD_FAST: [[fallthrough]];
313
                    case LOAD_SYMBOL:
314
                        if (auto it = m_symbols_data.find(entity.primaryArg()); it != m_symbols_data.end())
51,025✔
315
                            it->second.use_count++;
9,995✔
316
                        break;
68,229✔
317

318
                    // use, attached symbol id
319
                    case CALL_SYMBOL_BY_INDEX: [[fallthrough]];
320
                    case LOAD_FAST_BY_INDEX:
321
                        if (auto maybe_id = entity.originalSymbolId(); maybe_id.has_value())
54,398✔
322
                        {
323
                            if (auto it = m_symbols_data.find(maybe_id.value()); it != m_symbols_data.end())
27,623✔
324
                                it->second.use_count++;
424✔
325
                        }
27,199✔
326
                        break;
65,386✔
327

328
                    // declaration, primary, id
329
                    case STORE: [[fallthrough]];
330
                    case STORE_REF: [[fallthrough]];
331
                    case SET_VAL:
332
                        if (auto it = m_symbols_data.find(entity.primaryArg()); it != m_symbols_data.end())
45,214✔
333
                            it->second.declarations_count++;
7,027✔
334
                        break;
38,187✔
335

336
                    // declaration, secondary, id
337
                    case SET_VAL_FROM:
NEW
338
                        if (auto it = m_symbols_data.find(entity.secondaryArg()); it != m_symbols_data.end())
×
NEW
339
                            it->second.use_count++;
×
NEW
340
                        break;
×
341

342
                    // declaration, attached symbol id
343
                    case SET_VAL_FROM_INDEX:
NEW
344
                        if (auto maybe_id = entity.originalSymbolId(); maybe_id.has_value())
×
345
                        {
NEW
346
                            if (auto it = m_symbols_data.find(maybe_id.value()); it != m_symbols_data.end())
×
NEW
347
                                it->second.declarations_count++;
×
NEW
348
                        }
×
349
                        break;
182,997✔
350

351
                    default:
352
                        break;
182,997✔
353
                }
289,413✔
354
            }
289,413✔
355
        }
7,999✔
356
    }
380✔
357

358
    std::optional<BlockInfo> IRInliner::findBlockBy(const CallKind kind, const uint16_t id) const noexcept
11,347✔
359
    {
11,347✔
360
        const auto it = std::ranges::find_if(
11,347✔
361
            m_funcs,
11,347✔
362
            [id, kind](const BlockInfo& info) -> bool {
1,470,619✔
363
                switch (kind)
1,459,272✔
364
                {
1,459,272✔
365
                    case CallKind::Symbol:
366
                        return info.symbol_id.has_value() && std::cmp_equal(info.symbol_id.value(), id);
1,459,272✔
367

368
                    case CallKind::Constant:
NEW
369
                        return std::cmp_equal(info.constant_id, id);
×
NEW
370
                }
×
NEW
371
                return false;
×
372
            });
1,459,272✔
373

374
        if (it != m_funcs.end())
11,347✔
375
            return *it;
10,198✔
376
        return std::nullopt;
1,149✔
377
    }
11,347✔
378
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc