ArkScript
A small, lisp-inspired, functional scripting language
IRCompiler.cpp
Go to the documentation of this file.
2
3#include <chrono>
4#include <utility>
5#include <optional>
6#include <unordered_map>
7#include <Proxy/Picosha2.hpp>
8#include <fmt/ostream.h>
9
10#include <Ark/Constants.hpp>
15
16namespace Ark::internal
17{
18 using namespace literals;
19
20 IRCompiler::IRCompiler(const unsigned debug) :
21 Pass("IRCompiler", debug)
22 {}
23
24 void IRCompiler::process(const std::vector<IR::Block>& pages, const std::vector<std::string>& symbols, const std::vector<ValTableElem>& values)
25 {
26 m_logger.traceStart("process");
28 pushSymbolTable(symbols);
29 pushValueTable(values);
30
31 // compute a list of unique filenames
32 for (const auto& page : pages)
33 {
34 for (const auto& inst : page.data)
35 {
36 if (std::ranges::find(m_filenames, inst.filename()) == m_filenames.end() && inst.hasValidSourceLocation())
37 m_filenames.push_back(inst.filename());
38 }
39 }
40
42 pushInstLocTable(pages);
43
44 m_ir = pages;
45 compile();
46
47 if (m_ir.empty())
48 {
49 // code segment with a single instruction
51 m_bytecode.push_back(0_u8);
52 m_bytecode.push_back(1_u8);
53
54 m_bytecode.push_back(0_u8);
55 m_bytecode.push_back(HALT);
56 m_bytecode.push_back(0_u8);
57 m_bytecode.push_back(0_u8);
58 }
59
60 // generate a hash of the tables + bytecode
61 std::vector<unsigned char> hash_out(picosha2::k_digest_size);
62 picosha2::hash256(m_bytecode.begin() + bytecode::HeaderSize, m_bytecode.end(), hash_out);
63 m_bytecode.insert(m_bytecode.begin() + bytecode::HeaderSize, hash_out.begin(), hash_out.end());
64
66 }
67
68 void IRCompiler::dumpToStream(std::ostream& stream) const
69 {
70 std::size_t index = 0;
71 for (const auto& block : m_ir)
72 {
73 if (index == 0)
74 // global scope
75 fmt::println(stream, "global");
76 else
77 {
78 fmt::println(
79 stream,
80 "page_{} ({} ({} argument{}) {}, {} instructions)",
81 index,
82 block.debugName(),
83 block.metadata.argument_count,
84 block.metadata.argument_count == 1 ? "" : "s",
85 block.metadataRepr(),
86 block.data.size());
87 }
88
89 for (const auto& entity : block.data)
90 {
91 switch (entity.kind())
92 {
93 case IR::Kind::Label:
94 fmt::println(stream, ".L{}:", entity.label());
95 break;
96
97 case IR::Kind::Goto:
98 fmt::println(stream, "\t{} L{}", InstructionNames[entity.inst()], entity.label());
99 break;
100
102 fmt::println(stream, "\t{} L{}, {}", InstructionNames[entity.inst()], entity.label(), entity.primaryArg());
103 break;
104
105 case IR::Kind::Opcode:
106 fmt::println(stream, "\t{} {}", InstructionNames[entity.inst()], entity.primaryArg());
107 break;
108
110 fmt::println(stream, "\t{} {}, {}", InstructionNames[entity.inst()], entity.primaryArg(), entity.secondaryArg());
111 break;
112
114 fmt::println(stream, "\t{} {}, {}, {}", InstructionNames[entity.inst()], entity.primaryArg(), entity.secondaryArg(), entity.tertiaryArg());
115 break;
116 }
117 }
118
119 fmt::println(stream, "");
120 ++index;
121 }
122 }
123
124 const bytecode_t& IRCompiler::bytecode() const noexcept
125 {
126 return m_bytecode;
127 }
128
130 {
131 // push the different code segments
132 for (std::size_t i = 0, end = m_ir.size(); i < end; ++i)
133 {
134 IR::Block& page = m_ir[i];
135 // just in case we got too far, always add a HALT to be sure the
136 // VM won't do anything crazy
137 page.data.emplace_back(HALT);
138
139 // push number of elements
140 const std::size_t page_size = page.instructionCount();
141 if (std::cmp_greater(page_size, MaxValue16Bits))
142 {
143 std::string message;
144 if (i == 0)
145 message = fmt::format("Global scope exceeds the maximum number of instructions ({})", MaxValue16Bits);
146 else if (page.metadata.name.has_value())
147 message = fmt::format("Function {} exceeds the maximum number of instructions ({})", page.metadata.name.value(), MaxValue16Bits);
148 else
149 message = fmt::format("Anonymous function at page {} exceeds the maximum number of instructions ({})", i, MaxValue16Bits);
150
151 throw std::overflow_error(message);
152 }
153
156
157 // register labels position
158 uint16_t pos = 0;
159 std::unordered_map<IR::label_t, uint16_t> label_to_position;
160 for (const auto& inst : page.data)
161 {
162 switch (inst.kind())
163 {
164 case IR::Kind::Label:
165 label_to_position[inst.label()] = pos;
166 break;
167
168 default:
169 ++pos;
170 }
171 }
172
173 for (const auto& inst : page.data)
174 {
175 switch (inst.kind())
176 {
177 case IR::Kind::Goto:
178 pushWord(Word(inst.inst(), label_to_position[inst.label()]));
179 break;
180
182 pushWord(Word(inst.inst(), inst.primaryArg(), label_to_position[inst.label()]));
183 break;
184
185 case IR::Kind::Opcode:
186 [[fallthrough]];
188 [[fallthrough]];
190 pushWord(inst.bytecode());
191 break;
192
193 default:
194 break;
195 }
196 }
197 }
198 }
199
200 void IRCompiler::pushWord(const Word& word)
201 {
202 m_bytecode.push_back(word.opcode);
203 m_bytecode.push_back(word.byte_1);
204 m_bytecode.push_back(word.byte_2);
205 m_bytecode.push_back(word.byte_3);
206 }
207
209 {
210 /*
211 Generating headers:
212 - lang name (to be sure we are executing an ArkScript file)
213 on 4 bytes (ark + padding)
214 - version (major: 2 bytes, minor: 2 bytes, patch: 2 bytes)
215 - timestamp (8 bytes, unix format)
216 */
217
218 m_bytecode.push_back('a');
219 m_bytecode.push_back('r');
220 m_bytecode.push_back('k');
221 m_bytecode.push_back(0_u8);
222
223 // push version
224 for (const int n : std::array { ARK_VERSION_MAJOR, ARK_VERSION_MINOR, ARK_VERSION_PATCH })
226
227 // push timestamp
228 const long long timestamp = std::chrono::duration_cast<std::chrono::seconds>(
229 std::chrono::system_clock::now().time_since_epoch())
230 .count();
231 for (long i = 0; i < 8; ++i)
232 {
233 const long shift = 8 * (7 - i);
234 const auto ts_byte = static_cast<uint8_t>((timestamp & (0xffLL << shift)) >> shift);
235 m_bytecode.push_back(ts_byte);
236 }
237 }
238
239 void IRCompiler::pushSymbolTable(const std::vector<std::string>& symbols)
240 {
241 const std::size_t symbol_size = symbols.size();
242 if (std::cmp_greater(symbol_size, MaxValue16Bits))
243 throw std::overflow_error(fmt::format("Too many symbols: {}, exceeds the maximum size of {}", symbol_size, MaxValue16Bits));
244
245 m_bytecode.push_back(SYM_TABLE_START);
247
248 for (const auto& sym : symbols)
249 {
250 // push the string, null terminated
251 std::ranges::transform(sym, std::back_inserter(m_bytecode), [](const char i) {
252 return static_cast<uint8_t>(i);
253 });
254 m_bytecode.push_back(0_u8);
255 }
256 }
257
258 void IRCompiler::pushValueTable(const std::vector<ValTableElem>& values)
259 {
260 const std::size_t value_size = values.size();
261 if (std::cmp_greater(value_size, MaxValue16Bits))
262 throw std::overflow_error(fmt::format("Too many values: {}, exceeds the maximum size of {}", value_size, MaxValue16Bits));
263
264 m_bytecode.push_back(VAL_TABLE_START);
266
267 for (const ValTableElem& val : values)
268 {
269 switch (val.type)
270 {
272 {
273 m_bytecode.push_back(NUMBER_TYPE);
274 const auto n = std::get<double>(val.value);
275 const auto [exponent, mantissa] = ieee754::serialize(n);
276 serializeToVecLE(exponent, m_bytecode);
277 serializeToVecLE(mantissa, m_bytecode);
278 break;
279 }
280
282 {
283 m_bytecode.push_back(STRING_TYPE);
284 auto t = std::get<std::string>(val.value);
285 std::ranges::transform(t, std::back_inserter(m_bytecode), [](const char i) {
286 return static_cast<uint8_t>(i);
287 });
288 break;
289 }
290
292 {
293 m_bytecode.push_back(FUNC_TYPE);
294 const std::size_t addr = std::get<std::size_t>(val.value);
296 break;
297 }
298 }
299
300 m_bytecode.push_back(0_u8);
301 }
302 }
303
305 {
306 if (std::cmp_greater(m_filenames.size(), MaxValue16Bits))
307 throw std::overflow_error(fmt::format("Too many filenames: {}, exceeds the maximum size of {}", m_filenames.size(), MaxValue16Bits));
308
310 // push number of elements
312
313 for (const auto& name : m_filenames)
314 {
315 std::ranges::transform(name, std::back_inserter(m_bytecode), [](const char i) {
316 return static_cast<uint8_t>(i);
317 });
318 m_bytecode.push_back(0_u8);
319 }
320 }
321
322 void IRCompiler::pushInstLocTable(const std::vector<IR::Block>& pages)
323 {
324 std::vector<internal::InstLoc> locations;
325 for (std::size_t i = 0, end = pages.size(); i < end; ++i)
326 {
327 const auto& page = pages[i];
328 uint16_t ip = 0;
329
330 for (const auto& inst : page.data)
331 {
332 if (inst.hasValidSourceLocation())
333 {
334 // we are guaranteed to have a value since we listed all existing filenames in IRCompiler::process before,
335 // thus we do not have to check if std::ranges::find returned a valid iterator.
336 auto file_id = static_cast<uint16_t>(std::distance(m_filenames.begin(), std::ranges::find(m_filenames, inst.filename())));
337
338 std::optional<internal::InstLoc> prev = std::nullopt;
339 if (!locations.empty())
340 prev = locations.back();
341
342 // skip redundant instruction location
343 if (!(prev.has_value() && prev->filename_id == file_id && prev->line == inst.sourceLine() && prev->page_pointer == i))
344 locations.push_back(
345 { .page_pointer = static_cast<uint16_t>(i),
346 .inst_pointer = ip,
347 .filename_id = file_id,
348 .line = static_cast<uint32_t>(inst.sourceLine()) });
349 }
350
351 if (inst.kind() != IR::Kind::Label)
352 ++ip;
353 }
354 }
355
357 serializeOn2BytesToVecBE(locations.size(), m_bytecode);
358
359 for (const auto& loc : locations)
360 {
361 serializeOn2BytesToVecBE(loc.page_pointer, m_bytecode);
362 serializeOn2BytesToVecBE(loc.inst_pointer, m_bytecode);
363 serializeOn2BytesToVecBE(loc.filename_id, m_bytecode);
364 serializeToVecBE(loc.line, m_bytecode);
365 }
366 }
367}
Constants used by ArkScript.
constexpr int ARK_VERSION_MAJOR
Definition Constants.hpp:18
constexpr int ARK_VERSION_PATCH
Definition Constants.hpp:20
constexpr int ARK_VERSION_MINOR
Definition Constants.hpp:19
Compile the intermediate representation to bytecode.
User defined literals for Ark internals.
void pushInstLocTable(const std::vector< IR::Block > &pages)
IRCompiler(unsigned debug)
Create a new IRCompiler.
void dumpToStream(std::ostream &stream) const
Dump the IR given to process to an output stream.
const bytecode_t & bytecode() const noexcept
Return the constructed bytecode object.
std::vector< std::string > m_filenames
void pushWord(const Word &word)
Push a word (4 bytes) to the m_bytecode.
std::vector< IR::Block > m_ir
void pushFileHeader() noexcept
Push the file headers (magic, version used, timestamp)
void process(const std::vector< IR::Block > &pages, const std::vector< std::string > &symbols, const std::vector< ValTableElem > &values)
Turn a given IR into bytecode.
void pushValueTable(const std::vector< ValTableElem > &values)
void pushSymbolTable(const std::vector< std::string > &symbols)
void traceStart(std::string &&trace_name)
Definition Logger.hpp:109
An interface to describe compiler passes.
Definition Pass.hpp:24
constexpr std::size_t HeaderSize
Definition Common.hpp:39
DecomposedDouble serialize(const double n)
void serializeToVecBE(std::integral auto number, std::vector< uint8_t > &out)
void serializeToVecLE(std::integral auto number, std::vector< uint8_t > &out)
void serializeOn2BytesToVecBE(std::integral auto number, std::vector< uint8_t > &out)
constexpr std::array InstructionNames
constexpr uint16_t MaxValue16Bits
Definition Constants.hpp:81
std::vector< uint8_t > bytecode_t
Definition Common.hpp:22
std::optional< std::string > name
Definition Entity.hpp:246
Block of IR entities, with attached metadata.
Definition Entity.hpp:241
std::size_t instructionCount() const
Definition Entity.hpp:292
struct Ark::internal::IR::Block::Metadata metadata
A Compiler Value class helper to handle multiple types.
uint8_t opcode
Instruction opcode.
Definition Word.hpp:20