ArkScript
A small, lisp-inspired, functional scripting language
IRInliner.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cassert>
5#include <limits>
6
7namespace Ark::internal
8{
9 IRInliner::IRInliner(const unsigned debug) :
10 Pass("IRInliner", debug),
11 m_current_label(0)
12 {}
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)
15 {
16 m_logger.traceStart("process");
17 m_symbols = symbols;
18 m_values = values;
19 m_current_label = last_label + 1;
20
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)
26 {
27 IR::Block new_block = IR::Block::InitWithMetadata(block);
28
29 // We only have to deal with CALL_SYMBOL, CALL_SYMBOL_BY_INDEX, which deal with symbols,
30 // and CALL which can deal with constant ids (eg `((fun (a) (print a)) 5)`)
31 for (std::size_t i = 0, end = block.data.size(); i < end; ++i)
32 {
33 const auto& entity = block.data[i];
34
35 std::optional<uint16_t> maybe_id;
36 std::size_t argc = std::numeric_limits<std::size_t>::max();
38
39 if (entity.inst() == CALL_SYMBOL || entity.inst() == CALL_SYMBOL_BY_INDEX)
40 {
41 maybe_id = entity.relatedResourceId();
42 argc = entity.secondaryArg();
43 }
44 else if (entity.inst() == CALL)
45 {
46 maybe_id = entity.relatedResourceId();
47 argc = entity.primaryArg();
48 kind = CallKind::Constant;
49 }
50
51 if (const auto maybe_block = blockToInlineInCall(kind, pages, maybe_id, block, argc); maybe_block.has_value())
52 {
53 const IR::Block& inlinee = pages[maybe_block->addr];
54
55 // retrieve the return label of the call instruction, to know which PUSH_RETURN_ADDRESS instruction we'll have to remove
56 if (i + 1 < end)
57 {
58 assert(block.data[i + 1].kind() == IR::Kind::Label && "Expected a label right after the CALL instruction! The AST lowerer messed up somewhere");
59
60 const IR::label_t return_label = block.data[i + 1].label();
61 const std::size_t removed = std::erase_if(new_block.data, [return_label](const IR::Entity& e) -> bool {
62 return e.kind() == IR::Kind::Goto && e.inst() == PUSH_RETURN_ADDRESS && e.label() == return_label;
63 });
64
65 if (removed == 0)
66 throw std::runtime_error(fmt::format("No PUSH_RETURN_ADDRESS L{} instruction removed, even though one was expected", return_label));
67 }
68
69 inlineBlock(inlinee, new_block);
70 }
71 else
72 new_block.data.emplace_back(entity);
73 }
74
75 m_ir.emplace_back(new_block);
76 }
77
79 }
80
81 const std::vector<IR::Block>& IRInliner::intermediateRepresentation() const noexcept
82 {
83 return m_ir;
84 }
85
86 bool IRInliner::canBeInlined(const IR::Block& candidate, const IR::Block& source, const std::size_t argc) noexcept
87 {
88 const std::size_t candidate_inst_count = candidate.instructionCount(),
89 source_inst_count = source.instructionCount();
90
91 if (candidate.metadata.is_closure ||
92 candidate.metadata.is_recursive ||
93 candidate.metadata.is_mutating_args ||
94 candidate.metadata.argument_count != argc ||
95 candidate.metadata.name.value_or(std::string(IR::AnonymousBlockName)) == IR::AnonymousBlockName ||
96 std::cmp_greater_equal(candidate_inst_count + source_inst_count, MaxValue16Bits))
97 return false;
98 // TODO: create a proper constant and make this less arbitrary?
99 return candidate.metadata.is_simple && candidate_inst_count < 24;
100 }
101
102 std::optional<BlockInfo> IRInliner::blockToInlineInCall(
103 const CallKind kind,
104 const std::vector<IR::Block>& pages,
105 const std::optional<uint16_t> maybe_id,
106 const IR::Block& current,
107 const std::size_t argc) const noexcept
108 {
109 if (!maybe_id.has_value())
110 return std::nullopt;
111
112 const uint16_t id = maybe_id.value();
113 std::optional<BlockInfo> maybe_block = findBlockBy(kind, id);
114 if (!maybe_block.has_value())
115 return std::nullopt;
116
117 // If we are trying to inline a function call that seems to have multiple declarations,
118 // abort. We can't be sure that we are inlining the correct version at the moment.
119 if (kind == CallKind::Symbol && m_symbols_data.contains(id) && m_symbols_data.at(id).declarations_count != 1)
120 return std::nullopt;
121
122 const std::size_t block_addr = maybe_block->addr;
123 if (canBeInlined(pages[block_addr], current, argc))
124 return maybe_block;
125 return std::nullopt;
126 }
127
128 std::optional<IR::Entity> IRInliner::isBuiltinProxy(const IR::Block& block)
129 {
130 /*
131 Expected instructions to be a builtin proxy:
132 STORE...
133 PUSH_RETURN_ADDRESS
134 LOAD_FAST_BY_INDEX...
135 CALL_BUILTIN
136 <label>
137 RET
138 */
139 if (block.data.size() < 6 || (block.data.size() - 4) % 2 != 0)
140 return std::nullopt;
141
142 Instruction expected = STORE;
143 std::size_t store_count = 0;
144 std::size_t load_count = 0;
145 IR::label_t label = 0;
146 std::optional<IR::Entity> call_builtin;
147
148 for (const auto& entity : block.data)
149 {
150 const Instruction inst = entity.inst();
151 const bool is_label = entity.kind() == IR::Kind::Label;
152
153 if (expected != inst)
154 {
155 if (expected == STORE)
156 expected = PUSH_RETURN_ADDRESS;
157 else if (expected == PUSH_RETURN_ADDRESS)
158 expected = LOAD_FAST_BY_INDEX;
159 else if (expected == LOAD_FAST_BY_INDEX)
160 expected = CALL_BUILTIN;
161 else if (expected == CALL_BUILTIN)
162 expected = NOP;
163 else if (expected == NOP)
164 expected = RET;
165 }
166
167 if (expected == inst)
168 {
169 if (expected == STORE)
170 ++store_count;
171 else if (expected == LOAD_FAST_BY_INDEX)
172 ++load_count;
173 else if (expected == CALL_BUILTIN)
174 {
175 call_builtin = entity;
176 if (entity.secondaryArg() != store_count)
177 return std::nullopt;
178 }
179 else if (expected == PUSH_RETURN_ADDRESS)
180 label = entity.label();
181 }
182 else if (is_label && label != entity.label())
183 return std::nullopt;
184 }
185
186 if (store_count == load_count)
187 return call_builtin;
188 return std::nullopt;
189 }
190
191 void IRInliner::inlineBlock(const IR::Block& inlinee, IR::Block& destination)
192 {
193 // todo: redo the filename lookup
194 if (destination.metadata.addr == 0)
195 m_logger.info("Inlining call to '{}' ({}) inside global scope", inlinee.debugName(), inlinee.metadataRepr());
196 else
198 "Inlining call to '{}' ({} from '{}') inside '{}' @ {}, from '{}'",
199 inlinee.debugName(),
200 inlinee.metadataRepr(),
201 inlinee.data.front().filename(),
202 destination.debugName(),
203 destination.metadata.addr,
204 destination.data.front().filename());
205
206 if (auto inst = isBuiltinProxy(inlinee); inst.has_value())
207 {
208 m_logger.info(" -> builtin proxy with args ({}, {})", inst->primaryArg(), inst->secondaryArg());
209 destination.data
210 .emplace_back(CALL_BUILTIN_WITHOUT_RETURN_ADDRESS, inst->primaryArg(), inst->secondaryArg())
211 .setSourceLocation(inst->filename(), inst->sourceLine());
212 return;
213 }
214
215 // TODO: do a better inlining job (we have load ..., create scope, store ..., load ...)
216 // TODO: decide if we want to keep create_scope, (inlinee), pop_scope
217 destination.data.emplace_back(CREATE_SCOPE);
218
219 // We need to create new, unique labels for the inlined code.
220 // When we meet a label, we'll register it, and replace it with a new label
221 // in the inlined code. That way, if we find it again later in the code,
222 // we can use the correct value.
223 std::unordered_map<IR::label_t, IR::label_t> old_to_new_label;
224
225 for (const IR::Entity& entity : inlinee.data)
226 {
227 if (entity.inst() == RET)
228 break;
229
230 if (entity.hasLabel())
231 {
232 IR::Entity labelled_entity = entity;
233 if (auto it = old_to_new_label.find(entity.label()); it != old_to_new_label.end())
234 labelled_entity.replaceLabel(it->second);
235 else
236 {
237 labelled_entity.replaceLabel(m_current_label);
238 old_to_new_label[entity.label()] = m_current_label++;
239 }
240
241 destination.data.emplace_back(labelled_entity);
242 }
243 else if (entity.inst() == LOAD_FAST_BY_INDEX)
244 destination.data
245 .emplace_back(LOAD_FAST, entity.relatedResourceId().value())
246 .setSourceLocation(entity.filename(), entity.sourceLine());
247 else if (entity.inst() == CALL_SYMBOL_BY_INDEX)
248 destination.data
249 .emplace_back(CALL_SYMBOL, entity.relatedResourceId().value(), entity.secondaryArg())
250 .setSourceLocation(entity.filename(), entity.sourceLine());
251 else
252 destination.data.emplace_back(entity);
253 }
254
255 destination.data.emplace_back(POP_SCOPE, 1);
256 }
257
258 void IRInliner::extractPagesMetadata(const std::vector<IR::Block>& pages)
259 {
260 for (std::size_t i = 0, end = pages.size(); i < end; ++i)
261 {
262 const std::string& name = pages[i].debugName();
263 if (name != IR::AnonymousBlockName)
264 {
265 const auto it_val = std::ranges::find_if(m_values, [i](const ValTableElem& elem) -> bool {
266 return elem.type == ValTableElemType::PageAddr && std::get<std::size_t>(elem.value) == i;
267 });
268 assert(it_val != m_values.end() && "Could not find a constant referencing the current page!");
269
270 const auto it_sym = std::ranges::find_if(m_symbols, [&name](const std::string& sym) -> bool {
271 return name == sym;
272 });
273
274 if (it_sym != m_symbols.end())
275 {
276 m_symbols_data[std::distance(m_symbols.begin(), it_sym)] = SymbolData {
277 .name = name,
278 .declarations_count = 0,
279 .use_count = 0
280 };
281 }
282
283 m_funcs.emplace_back(BlockInfo {
284 .constant_id = static_cast<long>(std::distance(m_values.begin(), it_val)),
285 .addr = i,
286 .name = pages[i].debugName(),
287 .symbol_id = it_sym == m_symbols.end()
288 ? std::nullopt
289 : std::make_optional(std::distance(m_symbols.begin(), it_sym)) });
290 }
291 }
292
293 for (const IR::Block& page : pages)
294 {
295 for (const IR::Entity& entity : page.data)
296 {
297 switch (entity.inst())
298 {
299 // use, primary, id
300 case CALL_SYMBOL: [[fallthrough]];
301 case LOAD_FAST: [[fallthrough]];
302 case LOAD_SYMBOL:
303 if (auto it = m_symbols_data.find(entity.primaryArg()); it != m_symbols_data.end())
304 it->second.use_count++;
305 break;
306
307 // use, attached symbol id
308 case CALL_SYMBOL_BY_INDEX: [[fallthrough]];
309 case LOAD_FAST_BY_INDEX:
310 if (auto maybe_id = entity.relatedResourceId(); maybe_id.has_value())
311 {
312 if (auto it = m_symbols_data.find(maybe_id.value()); it != m_symbols_data.end())
313 it->second.use_count++;
314 }
315 break;
316
317 // declaration, primary, id
318 case STORE: [[fallthrough]];
319 case STORE_REF: [[fallthrough]];
320 case SET_VAL:
321 if (auto it = m_symbols_data.find(entity.primaryArg()); it != m_symbols_data.end())
322 it->second.declarations_count++;
323 break;
324
325 default:
326 break;
327 }
328 }
329 }
330 }
331
332 std::optional<BlockInfo> IRInliner::findBlockBy(const CallKind kind, const uint16_t id) const noexcept
333 {
334 const auto it = std::ranges::find_if(
335 m_funcs,
336 [id, kind](const BlockInfo& info) -> bool {
337 switch (kind)
338 {
339 case CallKind::Symbol:
340 return info.symbol_id.has_value() && std::cmp_equal(info.symbol_id.value(), id);
341
342 case CallKind::Constant:
343 return std::cmp_equal(info.constant_id, id);
344 }
345 return false;
346 });
347
348 if (it != m_funcs.end())
349 return *it;
350 return std::nullopt;
351 }
352}
Try to inline IR blocks.
std::vector< IR::Block > m_ir
Definition IRInliner.hpp:68
void extractPagesMetadata(const std::vector< IR::Block > &pages)
Extract metadata from the IR entities pages, to have a name, constant id, and potentially symbol id p...
IRInliner(unsigned debug)
Create a new IRInliner.
Definition IRInliner.cpp:9
IR::label_t m_current_label
Definition IRInliner.hpp:73
static bool canBeInlined(const IR::Block &candidate, const IR::Block &source, std::size_t argc) noexcept
Check if a block can be inlined in another one.
Definition IRInliner.cpp:86
static std::optional< IR::Entity > isBuiltinProxy(const IR::Block &block)
Check if an IR block represents a builtin proxy, and return its CALL instruction if it is.
void process(const std::vector< IR::Block > &pages, const std::vector< std::string > &symbols, const std::vector< ValTableElem > &values, IR::label_t last_label)
Attempt to inline IR blocks to avoid function calls when possible.
Definition IRInliner.cpp:14
std::optional< BlockInfo > findBlockBy(CallKind kind, uint16_t id) const noexcept
Search for a block by one of its IDs.
std::optional< BlockInfo > blockToInlineInCall(CallKind kind, const std::vector< IR::Block > &pages, std::optional< uint16_t > maybe_id, const IR::Block &current, std::size_t argc) const noexcept
See if a block can be inlined in the current call site.
std::vector< ValTableElem > m_values
Definition IRInliner.hpp:70
void inlineBlock(const IR::Block &inlinee, IR::Block &destination)
Perform the inlining.
std::vector< BlockInfo > m_funcs
Definition IRInliner.hpp:71
std::vector< std::string > m_symbols
Definition IRInliner.hpp:69
const std::vector< IR::Block > & intermediateRepresentation() const noexcept
Return the IR blocks (one per scope)
Definition IRInliner.cpp:81
std::unordered_map< long, SymbolData > m_symbols_data
Definition IRInliner.hpp:72
uint16_t secondaryArg() const
Return the second argument of the IR Entity.
Definition Entity.hpp:188
label_t label() const
Return the label of the IR Entity.
Definition Entity.hpp:160
bool hasLabel() const
Check if the Entity has a label attached.
Definition Entity.hpp:133
Instruction inst() const
Return the underlying instruction of the IR Entity.
Definition Entity.hpp:174
std::size_t sourceLine() const
Definition Entity.hpp:211
void replaceLabel(label_t replacement)
Definition Entity.cpp:30
uint16_t primaryArg() const
Return the primary argument of the IR Entity (can be 0 if the argument isn't used)
Definition Entity.hpp:181
const std::string & filename() const
Definition Entity.hpp:209
std::optional< uint16_t > relatedResourceId() const
Return the related constant/symbol id an IR Entity refers to (only populated for LOAD_FAST_BY_INDEX,...
Definition Entity.hpp:218
void info(const char *fmt, Args &&... args)
Write an info level log using fmtlib.
Definition Logger.hpp:63
void traceStart(std::string &&trace_name)
Definition Logger.hpp:109
An interface to describe compiler passes.
Definition Pass.hpp:24
std::size_t label_t
Definition Entity.hpp:35
constexpr std::string_view AnonymousBlockName
Definition Entity.hpp:42
Instruction
The different bytecodes are stored here.
constexpr uint16_t MaxValue16Bits
Definition Constants.hpp:81
std::optional< std::size_t > symbol_id
Definition IRInliner.hpp:30
Block of IR entities, with attached metadata.
Definition Entity.hpp:241
static Block InitWithMetadata(const Block &source)
Create a new empty IR::Block with the same metadata as the source block.
Definition Entity.hpp:262
std::string metadataRepr() const
Definition Entity.hpp:275
struct Ark::internal::IR::Block::Metadata metadata
std::string debugName() const
Definition Entity.hpp:270
A Compiler Value class helper to handle multiple types.
std::variant< double, std::string, std::size_t > value