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

ArkScript-lang / Ark / 29847311836

21 Jul 2026 04:09PM UTC coverage: 94.39% (+0.005%) from 94.385%
29847311836

Pull #705

github

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

440 of 463 new or added lines in 11 files covered. (95.03%)

4 existing lines in 1 file now uncovered.

10700 of 11336 relevant lines covered (94.39%)

1164645.14 hits per line

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

96.51
/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,112✔
10
        Pass("IRInliner", debug),
556✔
11
        m_current_label(0)
556✔
12
    {}
1,668✔
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)
382✔
15
    {
382✔
16
        m_logger.traceStart("process");
382✔
17
        m_symbols = symbols;
382✔
18
        m_values = values;
382✔
19
        m_current_label = last_label + 1;
382✔
20

21
        extractPagesMetadata(pages);
382✔
22

23
        // TODO: some pages could be removed if they are inlined everywhere!
24
        // TODO: we'll need to move some page index if a page is removed!
25
        for (const auto& block : pages)
8,393✔
26
        {
27
            IR::Block new_block {
16,022✔
28
                .metadata = {
56,077✔
29
                    .name = block.metadata.name,
8,011✔
30
                    .argument_count = block.metadata.argument_count,
8,011✔
31
                    .addr = block.metadata.addr,
8,011✔
32
                    .is_closure = block.metadata.is_closure,
8,011✔
33
                    .is_recursive = block.metadata.is_recursive,
8,011✔
34
                    .is_simple = block.metadata.is_simple },
8,011✔
35
                .data = {}
8,011✔
36
            };
37

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

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

48
                if (entity.inst() == CALL_SYMBOL || entity.inst() == CALL_SYMBOL_BY_INDEX)
289,491✔
49
                {
50
                    maybe_id = entity.relatedResourceId();
11,351✔
51
                    argc = entity.secondaryArg();
11,351✔
52
                }
11,351✔
53
                else if (entity.inst() == CALL)
278,140✔
54
                {
55
                    maybe_id = entity.relatedResourceId();
1,054✔
56
                    argc = entity.primaryArg();
1,054✔
57
                    kind = CallKind::Constant;
1,054✔
58
                }
1,054✔
59

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

64
                    // retrieve the return label of the call instruction, to know which PUSH_RETURN_ADDRESS instruction we'll have to remove
65
                    if (i + 1 < end)
3,137✔
66
                    {
67
                        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✔
68

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

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

78
                    inlineBlock(inlinee, new_block);
3,137✔
79
                }
3,137✔
80
                else
81
                    new_block.data.emplace_back(entity);
286,354✔
82
            }
289,491✔
83

84
            m_ir.emplace_back(new_block);
8,011✔
85
        }
8,011✔
86

87
        m_logger.traceEnd();
382✔
88
    }
382✔
89

90
    const std::vector<IR::Block>& IRInliner::intermediateRepresentation() const noexcept
382✔
91
    {
382✔
92
        return m_ir;
382✔
93
    }
94

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

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

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

121
        const uint16_t id = maybe_id.value();
11,379✔
122
        std::optional<BlockInfo> maybe_block = findBlockBy(kind, id);
11,379✔
123
        if (!maybe_block.has_value())
11,379✔
124
            return std::nullopt;
1,177✔
125

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

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

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

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

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

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

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

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

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

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

224
        // TODO: do a better inlining job (we have load ..., create scope, store ..., load ...)
225
        // TODO: decide if we want to keep create_scope, (inlinee), pop_scope
226
        destination.data.emplace_back(CREATE_SCOPE);
763✔
227

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

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

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

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

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

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

279
                const auto it_sym = std::ranges::find_if(m_symbols, [&name](const std::string& sym) -> bool {
1,194,059✔
280
                    return name == sym;
1,187,053✔
281
                });
282

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

292
                m_funcs.emplace_back(BlockInfo {
35,030✔
293
                    .constant_id = static_cast<long>(std::distance(m_values.begin(), it_val)),
7,006✔
294
                    .addr = i,
7,006✔
295
                    .name = pages[i].debugName(),
7,006✔
296
                    .symbol_id = it_sym == m_symbols.end()
14,012✔
NEW
297
                        ? std::nullopt
×
298
                        : std::make_optional(std::distance(m_symbols.begin(), it_sym)) });
7,006✔
299
            }
7,006✔
300
        }
8,011✔
301

302
        for (const IR::Block& page : pages)
8,393✔
303
        {
304
            for (const IR::Entity& entity : page.data)
297,502✔
305
            {
306
                switch (entity.inst())
289,491✔
307
                {
41,032✔
308
                    // use, primary, id
309
                    case CALL_SYMBOL: [[fallthrough]];
310
                    case LOAD_FAST: [[fallthrough]];
311
                    case LOAD_SYMBOL:
312
                        if (auto it = m_symbols_data.find(entity.primaryArg()); it != m_symbols_data.end())
51,029✔
313
                            it->second.use_count++;
9,997✔
314
                        break;
68,235✔
315

316
                    // use, attached symbol id
317
                    case CALL_SYMBOL_BY_INDEX: [[fallthrough]];
318
                    case LOAD_FAST_BY_INDEX:
319
                        if (auto maybe_id = entity.relatedResourceId(); maybe_id.has_value())
54,406✔
320
                        {
321
                            if (auto it = m_symbols_data.find(maybe_id.value()); it != m_symbols_data.end())
27,631✔
322
                                it->second.use_count++;
428✔
323
                        }
27,203✔
324
                        break;
65,396✔
325

326
                    // declaration, primary, id
327
                    case STORE: [[fallthrough]];
328
                    case STORE_REF: [[fallthrough]];
329
                    case SET_VAL:
330
                        if (auto it = m_symbols_data.find(entity.primaryArg()); it != m_symbols_data.end())
45,226✔
331
                            it->second.declarations_count++;
7,033✔
332
                        break;
221,256✔
333

334
                    default:
335
                        break;
183,063✔
336
                }
289,491✔
337
            }
289,491✔
338
        }
8,011✔
339
    }
382✔
340

341
    std::optional<BlockInfo> IRInliner::findBlockBy(const CallKind kind, const uint16_t id) const noexcept
11,379✔
342
    {
11,379✔
343
        const auto it = std::ranges::find_if(
11,379✔
344
            m_funcs,
11,379✔
345
            [id, kind](const BlockInfo& info) -> bool {
1,473,855✔
346
                switch (kind)
1,462,476✔
347
                {
1,459,276✔
348
                    case CallKind::Symbol:
349
                        return info.symbol_id.has_value() && std::cmp_equal(info.symbol_id.value(), id);
1,462,476✔
350

351
                    case CallKind::Constant:
352
                        return std::cmp_equal(info.constant_id, id);
3,200✔
NEW
353
                }
×
NEW
354
                return false;
×
355
            });
1,462,476✔
356

357
        if (it != m_funcs.end())
11,379✔
358
            return *it;
10,202✔
359
        return std::nullopt;
1,177✔
360
    }
11,379✔
361
}
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