ArkScript
A small, lisp-inspired, functional scripting language
ASTLowerer.cpp
Go to the documentation of this file.
2
3#include <limits>
4#include <ranges>
5#include <utility>
6#include <ranges>
7#include <algorithm>
8#include <fmt/core.h>
9#include <fmt/color.h>
10
11#include <Ark/Literals.hpp>
13
14namespace Ark::internal
15{
16 using namespace literals;
17
18 ASTLowerer::ASTLowerer(const unsigned debug) :
19 m_logger("ASTLowerer", debug)
20 {}
21
23 {
24 m_logger.traceStart("process");
25 m_code_pages.emplace_back(); // create empty page
26
27 // gather symbols, values, and start to create code segments
29 ast,
30 /* current_page */ Page { .index = 0, .is_temp = false },
31 /* is_result_unused */ false,
32 /* is_terminal */ false);
34 }
35
36 const std::vector<IR::Block>& ASTLowerer::intermediateRepresentation() const noexcept
37 {
38 return m_code_pages;
39 }
40
41 const std::vector<std::string>& ASTLowerer::symbols() const noexcept
42 {
43 return m_symbols;
44 }
45
46 const std::vector<ValTableElem>& ASTLowerer::values() const noexcept
47 {
48 return m_values;
49 }
50
51 std::optional<Instruction> ASTLowerer::getOperator(const std::string& name) noexcept
52 {
53 const auto it = std::ranges::find(Language::operators, name);
54 if (it != Language::operators.end())
55 return static_cast<Instruction>(std::distance(Language::operators.begin(), it) + FIRST_OPERATOR);
56 return std::nullopt;
57 }
58
59 std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
60 {
61 const auto it = std::ranges::find_if(Builtins::builtins,
62 [&name](const std::pair<std::string, Value>& element) -> bool {
63 return name == element.first;
64 });
65 if (it != Builtins::builtins.end())
66 return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
67 return std::nullopt;
68 }
69
70 std::optional<Instruction> ASTLowerer::getListInstruction(const std::string& name) noexcept
71 {
72 const auto it = std::ranges::find(Language::listInstructions, name);
73 if (it != Language::listInstructions.end())
74 return static_cast<Instruction>(std::distance(Language::listInstructions.begin(), it) + LIST);
75 return std::nullopt;
76 }
77
79 {
80 if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
81 // a begin node produces a value if the last node in it produces a value
82 return (node.constList()[0].keyword() == Keyword::Begin && node.constList().size() > 1 && nodeProducesOutput(node.constList().back())) ||
83 // a function always produces a value ; even if it ends with a node not producing one, the VM returns nil
84 node.constList()[0].keyword() == Keyword::Fun ||
85 // a condition produces a value if all its branches produce a value
86 (node.constList()[0].keyword() == Keyword::If &&
87 nodeProducesOutput(node.constList()[2]) &&
88 (node.constList().size() == 3 || nodeProducesOutput(node.constList()[3])));
89 // in place list instruction, as well as assert, do not produce values
90 if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
91 return std::ranges::find(Language::UpdateRef, node.constList().front().string()) == Language::UpdateRef.end() &&
92 node.constList().front().string() != "assert";
93 return true; // any other node, function call, symbol, number...
94 }
95
96 bool ASTLowerer::isUnaryInst(const Instruction inst) noexcept
97 {
98 switch (inst)
99 {
100 case NOT: [[fallthrough]];
101 case LEN: [[fallthrough]];
102 case EMPTY: [[fallthrough]];
103 case TAIL: [[fallthrough]];
104 case HEAD: [[fallthrough]];
105 case ISNIL: [[fallthrough]];
106 case TO_NUM: [[fallthrough]];
107 case TO_STR: [[fallthrough]];
108 case TYPE:
109 return true;
110
111 default:
112 return false;
113 }
114 }
115
116 bool ASTLowerer::isTernaryInst(const Instruction inst) noexcept
117 {
118 switch (inst)
119 {
120 case AT_AT:
121 return true;
122
123 default:
124 return false;
125 }
126 }
127
128 void ASTLowerer::warning(const std::string& message, const Node& node)
129 {
130 fmt::println("{} {}", fmt::styled("Warning", fmt::fg(fmt::color::dark_orange)), Diagnostics::makeContextWithNode(message, node));
131 }
132
133 void ASTLowerer::buildAndThrowError(const std::string& message, const Node& node)
134 {
135 throw CodeError(message, CodeErrorContext(node.filename(), node.line(), node.col(), node.repr()));
136 }
137
138 void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
139 {
140 // register symbols
141 if (x.nodeType() == NodeType::Symbol)
142 compileSymbol(x, p, is_result_unused);
143 else if (x.nodeType() == NodeType::Field)
144 {
145 // the parser guarantees us that there is at least 2 elements (eg: a.b)
146 compileSymbol(x.list()[0], p, is_result_unused);
147 for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
148 {
149 uint16_t i = addSymbol(*it);
150 page(p).emplace_back(GET_FIELD, i);
151 }
152 page(p).back().setSourceLocation(x.filename(), x.line());
153 }
154 // register values
155 else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
156 {
157 uint16_t i = addValue(x);
158
159 if (!is_result_unused)
160 page(p).emplace_back(LOAD_CONST, i);
161 }
162 // namespace nodes
163 else if (x.nodeType() == NodeType::Namespace)
164 compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal);
165 else if (x.nodeType() == NodeType::Unused)
166 {
167 // do nothing, explicitly
168 }
169 // empty code block should be nil
170 else if (x.constList().empty())
171 {
172 if (!is_result_unused)
173 {
174 static const std::optional<uint16_t> nil = getBuiltin("nil");
175 page(p).emplace_back(BUILTIN, nil.value());
176 }
177 }
178 // list instructions
179 else if (const auto head = x.constList()[0]; head.nodeType() == NodeType::Symbol && getListInstruction(head.string()).has_value())
180 compileListInstruction(x, p, is_result_unused);
181 // registering structures
182 else if (x.constList()[0].nodeType() == NodeType::Keyword)
183 {
184 switch (const Keyword keyword = x.constList()[0].keyword())
185 {
186 case Keyword::If:
187 compileIf(x, p, is_result_unused, is_terminal);
188 break;
189
190 case Keyword::Set:
191 [[fallthrough]];
192 case Keyword::Let:
193 [[fallthrough]];
194 case Keyword::Mut:
195 compileLetMutSet(keyword, x, p);
196 break;
197
198 case Keyword::Fun:
199 compileFunction(x, p, is_result_unused);
200 break;
201
202 case Keyword::Begin:
203 {
204 for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
206 x.list()[i],
207 p,
208 // All the nodes in a begin (except for the last one) are producing a result that we want to drop.
209 (i != size - 1) || is_result_unused,
210 // If the begin is a terminal node, only its last node is terminal.
211 is_terminal && (i == size - 1));
212 break;
213 }
214
215 case Keyword::While:
216 compileWhile(x, p);
217 break;
218
219 case Keyword::Import:
221 break;
222
223 case Keyword::Del:
224 page(p).emplace_back(DEL, addSymbol(x.constList()[1]));
225 page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].line());
226 break;
227 }
228 }
229 else if (x.nodeType() == NodeType::List)
230 {
231 // If we are here, we should have a function name via the m_opened_vars.
232 // Push arguments first, then function name, then call it.
233 handleCalls(x, p, is_result_unused, is_terminal);
234 }
235 else
237 fmt::format(
238 "NodeType `{}' not handled in ASTLowerer::compileExpression. Please fill an issue on GitHub: https://github.com/ArkScript-lang/Ark",
239 typeToString(x)),
240 x);
241 }
242
243 void ASTLowerer::compileSymbol(Node& x, const Page p, const bool is_result_unused)
244 {
245 const std::string& name = x.string();
246
247 if (const auto it_builtin = getBuiltin(name))
248 page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
249 else if (getOperator(name).has_value())
250 buildAndThrowError(fmt::format("Found a free standing operator: `{}`", name), x);
251 else
252 {
253 const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
254 if (maybe_local_idx.has_value())
255 page(p).emplace_back(LOAD_SYMBOL_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value()));
256 else
257 page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
258 }
259
260 if (is_result_unused)
261 {
262 warning("Statement has no effect", x);
263 page(p).emplace_back(POP);
264 }
265 }
266
267 void ASTLowerer::compileListInstruction(Node& x, const Page p, const bool is_result_unused)
268 {
269 const Node head = x.constList()[0];
270 std::string name = x.constList()[0].string();
271 Instruction inst = getListInstruction(name).value();
272
273 // length of at least 1 since we got a symbol name
274 const auto argc = x.constList().size() - 1u;
275 // error, can not use append/concat/pop (and their in place versions) with a <2 length argument list
276 if (argc < 2 && APPEND <= inst && inst <= POP)
277 buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), head);
278 if (inst <= POP && std::cmp_greater(argc, std::numeric_limits<uint16_t>::max()))
279 buildAndThrowError(fmt::format("Too many arguments ({}), exceeds 65'535", argc), x);
280 if (argc != 3 && inst == SET_AT_INDEX)
281 buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), head);
282 if (argc != 4 && inst == SET_AT_2_INDEX)
283 buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), head);
284
285 // compile arguments in reverse order
286 for (std::size_t i = x.constList().size() - 1u; i > 0; --i)
287 {
288 Node& node = x.list()[i];
289 if (nodeProducesOutput(node))
290 compileExpression(node, p, false, false);
291 else
292 buildAndThrowError(fmt::format("Invalid node inside call to {}", name), node);
293 }
294
295 // put inst and number of arguments
296 std::size_t inst_argc = 0;
297 switch (inst)
298 {
299 case LIST:
300 inst_argc = argc;
301 break;
302
303 case APPEND:
304 case APPEND_IN_PLACE:
305 case CONCAT:
306 case CONCAT_IN_PLACE:
307 inst_argc = argc - 1;
308 break;
309
310 case POP_LIST:
312 inst_argc = 0;
313 break;
314
315 default:
316 break;
317 }
318 page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
319 page(p).back().setSourceLocation(head.filename(), head.line());
320
321 if (is_result_unused && name.back() != '!' && inst <= POP_LIST_IN_PLACE) // in-place functions never push a value
322 {
323 warning("Ignoring return value of function", x);
324 page(p).emplace_back(POP);
325 }
326 }
327
328 void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal)
329 {
330 // compile condition
331 compileExpression(x.list()[1], p, false, false);
332 page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].line());
333
334 // jump only if needed to the "true" branch
335 const auto label_then = IR::Entity::Label(m_current_label++);
336 page(p).emplace_back(IR::Entity::GotoIf(label_then, true));
337
338 // "false" branch code
339 if (x.constList().size() == 4) // we have an else clause
340 {
342 compileExpression(x.list()[3], p, is_result_unused, is_terminal);
343 page(p).back().setSourceLocation(x.constList()[3].filename(), x.constList()[3].line());
345 }
346
347 // when else is finished, jump to end
348 const auto label_end = IR::Entity::Label(m_current_label++);
349 page(p).emplace_back(IR::Entity::Goto(label_end));
350
351 // absolute address to jump to if condition is true
352 page(p).emplace_back(label_then);
353 // if code
355 compileExpression(x.list()[2], p, is_result_unused, is_terminal);
356 page(p).back().setSourceLocation(x.constList()[2].filename(), x.constList()[2].line());
358 // set jump to end pos
359 page(p).emplace_back(label_end);
360 }
361
362 void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused)
363 {
364 if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List)
365 buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args);
366 if (x.constList().size() != 3)
367 buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
368
369 // capture, if needed
370 std::size_t capture_inst_count = 0;
371 for (const auto& node : x.constList()[1].constList())
372 {
373 if (node.nodeType() == NodeType::Capture)
374 {
375 page(p).emplace_back(CAPTURE, addSymbol(node));
376 ++capture_inst_count;
377 }
378 }
379 const bool is_closure = capture_inst_count > 0;
380
382 is_closure
385
386 // create new page for function body
387 m_code_pages.emplace_back();
388 const auto function_body_page = Page { .index = m_code_pages.size() - 1, .is_temp = false };
389 // save page_id into the constants table as PageAddr and load the const
390 page(p).emplace_back(is_closure ? MAKE_CLOSURE : LOAD_CONST, addValue(function_body_page.index, x));
391
392 // pushing arguments from the stack into variables in the new scope
393 for (const auto& node : x.constList()[1].constList())
394 {
395 if (node.nodeType() == NodeType::Symbol)
396 {
397 page(function_body_page).emplace_back(STORE, addSymbol(node));
398 m_locals_locator.addLocal(node.string());
399 }
400 }
401
402 // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls.
403 // This way we can continue to safely apply optimisations on
404 // (let name (fun (e) (map lst (fun (e) (name e)))))
405 // Otherwise, `name` would have been optimized to a GET_CURRENT_PAGE_ADDRESS, which would have returned the wrong page.
406 if (x.isAnonymousFunction())
407 m_opened_vars.push("#anonymous");
408 // push body of the function
409 compileExpression(x.list()[2], function_body_page, false, true);
410 if (x.isAnonymousFunction())
411 m_opened_vars.pop();
412
413 // return last value on the stack
414 page(function_body_page).emplace_back(RET);
416
417 // if the computed function is unused, pop it
418 if (is_result_unused)
419 {
420 warning("Unused declared function", x);
421 page(p).emplace_back(POP);
422 }
423 }
424
426 {
427 if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol)
428 buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym);
429 if (x.constList().size() != 3)
430 buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
431
432 const std::string name = x.constList()[1].string();
433 uint16_t i = addSymbol(x.constList()[1]);
434
435 const bool is_function = x.constList()[2].isFunction();
436 if (is_function)
437 {
438 m_opened_vars.push(name);
439 x.list()[2].setFunctionKind(/* anonymous= */ false);
440 }
441
442 // put value before symbol id
443 // starting at index = 2 because x is a (let|mut|set variable ...) node
444 for (std::size_t idx = 2, end = x.constList().size(); idx < end; ++idx)
445 compileExpression(x.list()[idx], p, false, false);
446
447 if (n == Keyword::Let || n == Keyword::Mut)
448 {
449 page(p).emplace_back(STORE, i);
451 }
452 else
453 page(p).emplace_back(SET_VAL, i);
454
455 if (is_function)
456 m_opened_vars.pop();
457 page(p).back().setSourceLocation(x.filename(), x.line());
458 }
459
461 {
462 if (x.constList().size() != 3)
463 buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x);
464
466 page(p).emplace_back(CREATE_SCOPE);
467 page(p).back().setSourceLocation(x.filename(), x.line());
468
469 // save current position to jump there at the end of the loop
470 const auto label_loop = IR::Entity::Label(m_current_label++);
471 page(p).emplace_back(label_loop);
472 // push condition
473 compileExpression(x.list()[1], p, false, false);
474 // absolute jump to end of block if condition is false
475 const auto label_end = IR::Entity::Label(m_current_label++);
476 page(p).emplace_back(IR::Entity::GotoIf(label_end, false));
477 // push code to page
478 compileExpression(x.list()[2], p, true, false);
479
480 // reset the scope at the end of the loop so that indices are still valid
481 // otherwise, (while true { (let a 5) (print a) (let b 6) (print b) })
482 // would print 5, 6, then only 6 as we emit LOAD_SYMBOL_FROM_INDEX 0 and b is the last in the scope
483 // loop, jump to the condition
484 page(p).emplace_back(IR::Entity::Goto(label_loop, RESET_SCOPE_JUMP));
485
486 // absolute address to jump to if condition is false
487 page(p).emplace_back(label_end);
488
489 page(p).emplace_back(POP_SCOPE);
491 }
492
494 {
495 std::string path;
496 const Node package_node = x.constList()[1];
497 for (std::size_t i = 0, end = package_node.constList().size(); i < end; ++i)
498 {
499 path += package_node.constList()[i].string();
500 if (i + 1 != end)
501 path += "/";
502 }
503 path += ".arkm";
504
505 // register plugin path in the constants table
506 uint16_t id = addValue(Node(NodeType::String, path));
507 // add plugin instruction + id of the constant referring to the plugin path
508 page(p).emplace_back(PLUGIN, id);
509 page(p).back().setSourceLocation(x.filename(), x.line());
510 }
511
512 void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call)
513 {
514 const auto node = call.constList()[0];
515
516 // push the arguments in reverse order because the function loads its arguments in the order they are defined:
517 // (fun (a b c) ...) -> load 'a', then 'b', then 'c'
518 // We have to push arguments in this order and load them in reverse, because we are using references internally,
519 // which can cause problems for recursive functions that swap their arguments around.
520 // Eg (let foo (fun (a b c) (if (> a 0) (foo (- a 1) c (+ b c)) 1))) (foo 12 0 1)
521 // On the second self-call, b and c would have the same value, since we set c to (+ b c), and we pushed c as the
522 // value for argument b, but loaded it as a reference.
523 for (Node& value : std::ranges::drop_view(call.list(), 1) | std::views::reverse)
524 {
525 if (nodeProducesOutput(value))
526 compileExpression(value, p, false, false);
527 else
528 {
529 std::string message;
530 if (is_tail_call)
531 message = fmt::format("Invalid node inside tail call to `{}'", node.repr());
532 else
533 message = fmt::format("Invalid node inside call to `{}'", node.repr());
534 buildAndThrowError(message, value);
535 }
536 }
537 }
538
539 void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal)
540 {
541 constexpr std::size_t start_index = 1;
542
543 Node& node = x.list()[0];
544 const std::optional<Instruction> maybe_operator = node.nodeType() == NodeType::Symbol ? getOperator(node.string()) : std::nullopt;
545
546 const std::optional<Instruction> maybe_shortcircuit =
547 node.nodeType() == NodeType::Symbol
548 ? (node.string() == Language::And
549 ? std::make_optional(Instruction::SHORTCIRCUIT_AND)
550 : (node.string() == Language::Or
551 ? std::make_optional(Instruction::SHORTCIRCUIT_OR)
552 : std::nullopt))
553 : std::nullopt;
554
555 if (maybe_shortcircuit.has_value())
556 {
557 // short circuit implementation
558 if (x.constList().size() < 3)
560 fmt::format(
561 "Expected at least 2 arguments while compiling '{}', got {}",
562 node.string(),
563 x.constList().size() - 1),
564 x);
565
566 compileExpression(x.list()[1], p, false, false);
567
568 const auto label_shortcircuit = IR::Entity::Label(m_current_label++);
569 auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, maybe_shortcircuit.value());
570 page(p).emplace_back(shortcircuit_entity);
571
572 for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
573 {
574 compileExpression(x.list()[i], p, false, false);
575 if (i + 1 != end)
576 page(p).emplace_back(shortcircuit_entity);
577 }
578
579 page(p).emplace_back(label_shortcircuit);
580 }
581 else if (!maybe_operator.has_value())
582 {
583 if (is_terminal && node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string())
584 {
585 pushFunctionCallArguments(x, p, /* is_tail_call= */ true);
586
587 // jump to the top of the function
588 page(p).emplace_back(JUMP, 0_u16);
589 page(p).back().setSourceLocation(node.filename(), node.line());
590 return; // skip the potential Instruction::POP at the end
591 }
592 else
593 {
594 m_temp_pages.emplace_back();
595 const auto proc_page = Page { .index = m_temp_pages.size() - 1u, .is_temp = true };
596
597 // compile the function resolution to a separate page
598 if (node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string())
599 {
600 // The function is trying to call itself, but this isn't a tail call.
601 // We can skip the LOAD_SYMBOL function_name and directly push the current
602 // function page, which will be quicker than a local variable resolution.
603 // We set its argument to the symbol id of the function we are calling,
604 // so that the VM knowns the name of the last called function.
605 page(proc_page).emplace_back(GET_CURRENT_PAGE_ADDR, addSymbol(node));
606 }
607 else
608 {
609 // closure chains have been handled (eg: closure.field.field.function)
610 compileExpression(node, proc_page, false, false); // storing proc
611 }
612
613 if (m_temp_pages.back().empty())
614 buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x);
615
616 const auto label_return = IR::Entity::Label(m_current_label++);
617 page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
618
619 pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
620 // push proc from temp page
621 for (const auto& inst : m_temp_pages.back())
622 page(p).push_back(inst);
623 m_temp_pages.pop_back();
624
625 // number of arguments
626 std::size_t args_count = 0;
627 for (auto it = x.constList().begin() + start_index, it_end = x.constList().end(); it != it_end; ++it)
628 {
629 if (it->nodeType() != NodeType::Capture)
630 args_count++;
631 }
632 // call the procedure
633 page(p).emplace_back(CALL, args_count);
634 page(p).back().setSourceLocation(node.filename(), node.line());
635
636 // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
637 page(p).emplace_back(label_return);
638 }
639 }
640 else // operator
641 {
642 // retrieve operator
643 auto op = maybe_operator.value();
644
645 if (op == ASSERT)
646 is_result_unused = false;
647
648 // push arguments on current page
649 std::size_t exp_count = 0;
650 for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
651 {
652 if (nodeProducesOutput(x.constList()[index]))
653 compileExpression(x.list()[index], p, false, false);
654 else
655 buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'", node.repr()), x.constList()[index]);
656
657 if ((index + 1 < size && x.constList()[index + 1].nodeType() != NodeType::Capture) || index + 1 == size)
658 exp_count++;
659
660 // in order to be able to handle things like (op A B C D...)
661 // which should be transformed into A B op C op D op...
662 if (exp_count >= 2 && !isTernaryInst(op))
663 page(p).emplace_back(op);
664 }
665
666 if (isUnaryInst(op))
667 {
668 if (exp_count != 1)
669 buildAndThrowError(fmt::format("Operator needs one argument, but was called with {}", exp_count), x.constList()[0]);
670 page(p).emplace_back(op);
671 }
672 else if (isTernaryInst(op))
673 {
674 if (exp_count != 3)
675 buildAndThrowError(fmt::format("Operator needs three arguments, but was called with {}", exp_count), x.constList()[0]);
676 page(p).emplace_back(op);
677 }
678 else if (exp_count <= 1)
679 buildAndThrowError(fmt::format("Operator needs two arguments, but was called with {}", exp_count), x.constList()[0]);
680
681 page(p).back().setSourceLocation(x.filename(), x.line());
682
683 // need to check we didn't push the (op A B C D...) things for operators not supporting it
684 if (exp_count > 2)
685 {
686 switch (op)
687 {
688 // authorized instructions
689 case ADD: [[fallthrough]];
690 case SUB: [[fallthrough]];
691 case MUL: [[fallthrough]];
692 case DIV: [[fallthrough]];
693 case MOD: [[fallthrough]];
694 case AT_AT:
695 break;
696
697 default:
699 fmt::format(
700 "`{}' requires 2 arguments, but got {}.",
701 Language::operators[static_cast<std::size_t>(op - FIRST_OPERATOR)],
702 exp_count),
703 x);
704 }
705 }
706 }
707
708 if (is_result_unused)
709 page(p).emplace_back(POP);
710 }
711
712 uint16_t ASTLowerer::addSymbol(const Node& sym)
713 {
714 // otherwise, add the symbol, and return its id in the table
715 auto it = std::ranges::find(m_symbols, sym.string());
716 if (it == m_symbols.end())
717 {
718 m_symbols.push_back(sym.string());
719 it = m_symbols.begin() + static_cast<std::vector<std::string>::difference_type>(m_symbols.size() - 1);
720 }
721
722 const auto distance = std::distance(m_symbols.begin(), it);
723 if (distance < std::numeric_limits<uint16_t>::max())
724 return static_cast<uint16_t>(distance);
725 buildAndThrowError("Too many symbols (exceeds 65'536), aborting compilation.", sym);
726 }
727
728 uint16_t ASTLowerer::addValue(const Node& x)
729 {
730 const ValTableElem v(x);
731 auto it = std::ranges::find(m_values, v);
732 if (it == m_values.end())
733 {
734 m_values.push_back(v);
735 it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
736 }
737
738 const auto distance = std::distance(m_values.begin(), it);
739 if (distance < std::numeric_limits<uint16_t>::max())
740 return static_cast<uint16_t>(distance);
741 buildAndThrowError("Too many values (exceeds 65'536), aborting compilation.", x);
742 }
743
744 uint16_t ASTLowerer::addValue(const std::size_t page_id, const Node& current)
745 {
746 const ValTableElem v(page_id);
747 auto it = std::ranges::find(m_values, v);
748 if (it == m_values.end())
749 {
750 m_values.push_back(v);
751 it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
752 }
753
754 const auto distance = std::distance(m_values.begin(), it);
755 if (distance < std::numeric_limits<uint16_t>::max())
756 return static_cast<uint16_t>(distance);
757 buildAndThrowError("Too many values (exceeds 65'536), aborting compilation.", current);
758 }
759}
Host the declaration of all the ArkScript builtins.
User defined literals for Ark internals.
const std::string & string() const
Definition Value.hpp:136
uint16_t addValue(const Node &x)
Register a given node in the value table.
void pushFunctionCallArguments(Node &call, Page p, bool is_tail_call)
uint16_t addSymbol(const Node &sym)
Register a given node in the symbol table.
static std::optional< Instruction > getListInstruction(const std::string &name) noexcept
Checking if a symbol is a list instruction.
std::vector< ValTableElem > m_values
void compileListInstruction(Node &x, Page p, bool is_result_unused)
static bool nodeProducesOutput(const Node &node)
std::vector< IR::Block > m_temp_pages
we need temporary code pages for some compilations passes
void compileLetMutSet(Keyword n, Node &x, Page p)
void handleCalls(Node &x, Page p, bool is_result_unused, bool is_terminal)
const std::vector< ValTableElem > & values() const noexcept
Return the value table pre-computed.
void compileIf(Node &x, Page p, bool is_result_unused, bool is_terminal)
void process(Node &ast)
Start the compilation.
const std::vector< IR::Block > & intermediateRepresentation() const noexcept
Return the IR blocks (one per scope)
static bool isUnaryInst(Instruction inst) noexcept
Check if a given instruction is unary (takes only one argument)
std::vector< IR::Block > m_code_pages
ASTLowerer(unsigned debug)
Construct a new ASTLowerer object.
void compileWhile(Node &x, Page p)
void compilePluginImport(Node &x, Page p)
std::vector< std::string > m_symbols
static void buildAndThrowError(const std::string &message, const Node &node)
Throw a nice error message.
static bool isTernaryInst(Instruction inst) noexcept
Check if a given instruction is ternary (takes three arguments)
void compileExpression(Node &x, Page p, bool is_result_unused, bool is_terminal)
Compile an expression (a node) recursively.
LocalsLocator m_locals_locator
std::stack< std::string > m_opened_vars
stack of vars we are currently declaring
void compileSymbol(Node &x, Page p, bool is_result_unused)
static void warning(const std::string &message, const Node &node)
Display a warning message.
void compileFunction(Node &x, Page p, bool is_result_unused)
static std::optional< uint16_t > getBuiltin(const std::string &name) noexcept
Checking if a symbol is a builtin.
static std::optional< Instruction > getOperator(const std::string &name) noexcept
Checking if a symbol is an operator.
IR::Block & page(const Page page) noexcept
helper functions to get a temp or finalized code page
const std::vector< std::string > & symbols() const noexcept
Return the symbol table pre-computed.
static Entity Goto(const Entity &label, Instruction inst=Instruction::JUMP)
Definition Entity.cpp:28
static Entity Label(label_t value)
Definition Entity.cpp:20
static Entity GotoIf(const Entity &label, bool cond)
Definition Entity.cpp:47
void saveScopeLengthForBranch()
Save the current scope length before entering a branch, so that we can ignore variable definitions in...
std::optional< std::size_t > lookupLastScopeByName(const std::string &name)
Search for a local in the current scope. Returns std::nullopt in case of closure scopes or if the var...
void dropVarsForBranch()
Drop potentially defined variables in the last saved branch.
void deleteScope()
Delete the last scope.
void addLocal(const std::string &name)
Register a local in the current scope, triggered by a STORE instruction. If the local already exists,...
void createScope(ScopeType type=ScopeType::Default)
Create a new scope.
void traceStart(std::string &&trace_name)
Definition Logger.hpp:90
A node of an Abstract Syntax Tree for ArkScript.
Definition Node.hpp:30
NodeType nodeType() const noexcept
Return the node type.
Definition Node.cpp:78
bool isAnonymousFunction() const noexcept
Check if a node is an anonymous function.
Definition Node.cpp:154
const std::string & filename() const noexcept
Return the filename in which this node was created.
Definition Node.cpp:174
const std::string & string() const noexcept
Return the string held by the value (if the node type allows it)
Definition Node.cpp:38
const std::vector< Node > & constList() const noexcept
Return the list of sub-nodes held by the node.
Definition Node.cpp:73
std::string repr() const noexcept
Compute a representation of the node without any comments or additional sugar, colors,...
Definition Node.cpp:189
const Namespace & constArkNamespace() const noexcept
Return the namespace held by the value (if the node type allows it)
Definition Node.cpp:58
std::size_t col() const noexcept
Get the column at which this node was created.
Definition Node.cpp:169
std::size_t line() const noexcept
Get the line at which this node was created.
Definition Node.cpp:164
std::vector< Node > & list() noexcept
Return the list of sub-nodes held by the node.
Definition Node.cpp:68
ARK_API std::string makeContextWithNode(const std::string &message, const internal::Node &node)
Helper used by the compiler to generate a colorized context from a node.
ARK_API const std::vector< std::pair< std::string, Value > > builtins
constexpr std::array< std::string_view, 9 > listInstructions
Definition Common.hpp:115
constexpr std::array< std::string_view, 24 > operators
Definition Common.hpp:149
constexpr std::string_view And
Definition Common.hpp:129
constexpr std::array UpdateRef
All the builtins that modify in place a variable.
Definition Common.hpp:108
constexpr std::string_view Or
Definition Common.hpp:130
std::string typeToString(const Node &node) noexcept
Definition Node.hpp:274
Keyword
The different keywords available.
Definition Common.hpp:75
Instruction
The different bytecodes are stored here.
CodeError thrown by the compiler (parser, macro processor, optimizer, and compiler itself)
std::shared_ptr< Node > ast
Definition Namespace.hpp:18
A Compiler Value class helper to handle multiple types.