ArkScript
A small, lisp-inspired, functional scripting language
ASTLowerer.cpp
Go to the documentation of this file.
2
3#include <cassert>
4#include <ranges>
5#include <utility>
6#include <algorithm>
7#include <fmt/core.h>
8#include <fmt/ranges.h>
9
14
15namespace Ark::internal
16{
17 using namespace literals;
18
19 enum class CallType
20 {
21 Classic,
23 Symbol,
26 };
27
28 ASTLowerer::ASTLowerer(const unsigned debug) :
29 Pass("ASTLowerer", debug)
30 {}
31
32 void ASTLowerer::addToTables(const std::vector<std::string>& symbols, const std::vector<ValTableElem>& constants)
33 {
34 std::ranges::copy(symbols, std::back_inserter(m_symbols));
35 std::ranges::copy(constants, std::back_inserter(m_values));
36 }
37
38 void ASTLowerer::offsetPagesBy(const std::size_t offset)
39 {
41 }
42
44 {
45 m_logger.traceStart("process");
46 const Page global = createNewCodePage();
47
48 // gather symbols, values, and start to create code segments
50 ast,
51 /* current_page */ global,
52 /* is_result_unused= */ true,
53 /* is_terminal= */ false,
54 /* can_use_ref= */ true);
56 }
57
58 const std::vector<IR::Block>& ASTLowerer::intermediateRepresentation() const noexcept
59 {
60 return m_code_pages;
61 }
62
63 const std::vector<std::string>& ASTLowerer::symbols() const noexcept
64 {
65 return m_symbols;
66 }
67
68 const std::vector<ValTableElem>& ASTLowerer::values() const noexcept
69 {
70 return m_values;
71 }
72
73 std::optional<Instruction> ASTLowerer::getOperator(const std::string& name) noexcept
74 {
75 const auto it = std::ranges::find(Language::operators, name);
76 if (it != Language::operators.end())
77 return static_cast<Instruction>(std::distance(Language::operators.begin(), it) + FirstOperator);
78 return std::nullopt;
79 }
80
81 std::optional<uint16_t> ASTLowerer::getBuiltin(const std::string& name) noexcept
82 {
83 const auto it = std::ranges::find_if(Builtins::builtins,
84 [&name](const std::pair<std::string, Value>& element) -> bool {
85 return name == element.first;
86 });
87 if (it != Builtins::builtins.end())
88 return static_cast<uint16_t>(std::distance(Builtins::builtins.begin(), it));
89 return std::nullopt;
90 }
91
92 std::optional<Instruction> ASTLowerer::getListInstruction(const std::string& name) noexcept
93 {
94 const auto it = std::ranges::find(Language::listInstructions, name);
95 if (it != Language::listInstructions.end())
96 return static_cast<Instruction>(std::distance(Language::listInstructions.begin(), it) + LIST);
97 return std::nullopt;
98 }
99
101 {
102 if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
103 return node.constList().front().string() == "breakpoint";
104 return false;
105 }
106
108 {
109 if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Keyword)
110 // a 'begin' node produces a value if the last node in it produces a value
111 return (node.constList()[0].keyword() == Keyword::Begin && node.constList().size() > 1 && nodeProducesOutput(node.constList().back())) ||
112 // a function always produces a value ; even if it ends with a node not producing one, the VM returns nil
113 node.constList()[0].keyword() == Keyword::Fun ||
114 // a let/mut/set pushes the value that was assigned
115 node.constList()[0].keyword() == Keyword::Let ||
116 node.constList()[0].keyword() == Keyword::Mut ||
117 node.constList()[0].keyword() == Keyword::Set ||
118 // a condition produces a value if all its branches produce a value
119 (node.constList()[0].keyword() == Keyword::If &&
120 nodeProducesOutput(node.constList()[2]) &&
121 (node.constList().size() == 3 || nodeProducesOutput(node.constList()[3])));
122 // breakpoint do not produce values
123 if (node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol)
124 {
125 const std::string& name = node.constList().front().string();
126 return name != "breakpoint";
127 }
128 return true; // any other node, function call, symbol, number...
129 }
130
131 bool ASTLowerer::isUnaryInst(const Instruction inst) noexcept
132 {
133 switch (inst)
134 {
135 case NOT: [[fallthrough]];
136 case LEN: [[fallthrough]];
137 case IS_EMPTY: [[fallthrough]];
138 case TAIL: [[fallthrough]];
139 case HEAD: [[fallthrough]];
140 case IS_NIL: [[fallthrough]];
141 case TO_NUM: [[fallthrough]];
142 case TO_STR: [[fallthrough]];
143 case TYPE:
144 return true;
145
146 default:
147 return false;
148 }
149 }
150
151 bool ASTLowerer::isTernaryInst(const Instruction inst) noexcept
152 {
153 switch (inst)
154 {
155 case AT_AT:
156 return true;
157
158 default:
159 return false;
160 }
161 }
162
164 {
165 switch (inst)
166 {
167 case ADD: [[fallthrough]];
168 case SUB: [[fallthrough]];
169 case MUL: [[fallthrough]];
170 case DIV:
171 return true;
172
173 default:
174 return false;
175 }
176 }
177
178 void ASTLowerer::warning(const std::string& message, const Node& node)
179 {
181 }
182
183 void ASTLowerer::buildAndThrowError(const std::string& message, const Node& node)
184 {
185 throw CodeError(message, CodeErrorContext(node.filename(), node.position()));
186 }
187
188 void ASTLowerer::makeError(const ErrorKind kind, const Node& node, const std::string& additional_ctx)
189 {
190 const std::string invalid_node_msg = "The given node doesn't return a value, and thus can't be used as an expression.";
191
192 switch (kind)
193 {
195 buildAndThrowError(fmt::format("Invalid node ; if it was computed by a macro, check that a node is returned"), node);
196 break;
197
199 buildAndThrowError(fmt::format("Invalid node inside call to `{}'. {}", additional_ctx, invalid_node_msg), node);
200 break;
201
203 buildAndThrowError(fmt::format("Invalid node inside tail call to `{}'. {}", additional_ctx, invalid_node_msg), node);
204 break;
205
207 buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'. {}", additional_ctx, invalid_node_msg), node);
208 break;
209 }
210 }
211
212 void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal, const bool can_use_ref)
213 {
214 // register symbols
215 if (x.nodeType() == NodeType::Symbol)
216 compileSymbol(x, p, is_result_unused, /* can_use_ref= */ can_use_ref);
217 else if (x.nodeType() == NodeType::Field)
218 {
219 // the parser guarantees us that there is at least 2 elements (eg: a.b)
220 compileSymbol(x.list()[0], p, is_result_unused, /* can_use_ref= */ true);
221 for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it)
222 {
223 uint16_t i = addSymbol(*it);
224 page(p).emplace_back(GET_FIELD, i);
225 }
226 page(p).back().setSourceLocation(x.filename(), x.position().start.line);
227 }
228 // register values
229 else if (x.nodeType() == NodeType::String || x.nodeType() == NodeType::Number)
230 {
231 uint16_t i = addValue(x);
232
233 if (!is_result_unused)
234 page(p).emplace_back(LOAD_CONST, i);
235 }
236 // namespace nodes
237 else if (x.nodeType() == NodeType::Namespace)
238 compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal, can_use_ref);
239 else if (x.nodeType() == NodeType::List)
240 {
241 // empty code block should be nil
242 if (x.constList().empty())
243 {
244 if (!is_result_unused)
245 {
246 static const std::optional<uint16_t> nil = getBuiltin("nil");
247 page(p).emplace_back(BUILTIN, nil.value());
248 }
249 }
250 // list instructions
251 else if (const auto head = x.constList()[0]; head.nodeType() == NodeType::Symbol && getListInstruction(head.string()).has_value())
252 compileListInstruction(x, p, is_result_unused);
253 else if (head.nodeType() == NodeType::Symbol && head.string() == Language::Apply)
254 compileApplyInstruction(x, p, is_result_unused);
255 // registering structures
256 else if (head.nodeType() == NodeType::Keyword)
257 {
258 switch (const Keyword keyword = head.keyword())
259 {
260 case Keyword::If:
261 compileIf(x, p, is_result_unused, is_terminal, can_use_ref);
262 break;
263
264 case Keyword::Set:
265 [[fallthrough]];
266 case Keyword::Let:
267 [[fallthrough]];
268 case Keyword::Mut:
269 compileLetMutSet(keyword, x, p, is_result_unused);
270 break;
271
272 case Keyword::Fun:
273 compileFunction(x, p, is_result_unused);
274 break;
275
276 case Keyword::Begin:
277 {
278 const bool ends_on_breakpoint = isBreakpoint(x.list().back());
279
280 for (std::size_t i = 1, size = x.list().size(); i < size; ++i)
281 {
282 // All the nodes in a 'begin' (except for the last one) are producing a result that we want to drop.
283 const bool unused = is_result_unused || (ends_on_breakpoint ? i + 2 != size : i + 1 != size);
284
286 x.list()[i],
287 p,
288 /* is_result_unused= */ unused,
289 // If the 'begin' is a terminal node, only its last node is terminal.
290 /* is_terminal= */ is_terminal && (ends_on_breakpoint ? i + 2 == size : i + 1 == size),
291 /* can_use_ref= */ can_use_ref);
292 }
293 break;
294 }
295
296 case Keyword::While:
297 compileWhile(x, p);
298 break;
299
300 case Keyword::Import:
302 break;
303
304 case Keyword::Del:
305 page(p).emplace_back(DEL, addSymbol(x.constList()[1]));
306 page(p).back().setSourceLocation(x.filename(), x.position().start.line);
307 break;
308 }
309 }
310 else
311 {
312 // If we are here, we should have a function name via the m_opened_vars.
313 handleCalls(x, p, is_result_unused, is_terminal, can_use_ref);
314 }
315 }
316 else if (x.nodeType() != NodeType::Unused)
318 fmt::format(
319 "NodeType `{}' not handled in ASTLowerer::compileExpression. Please fill an issue on GitHub: https://github.com/ArkScript-lang/Ark",
320 typeToString(x)),
321 x);
322 }
323
324 void ASTLowerer::compileSymbol(const Node& x, const Page p, const bool is_result_unused, const bool can_use_ref)
325 {
326 const std::string& name = x.string();
327
328 if (const auto it_builtin = getBuiltin(name))
329 page(p).emplace_back(Instruction::BUILTIN, it_builtin.value());
330 else if (std::ranges::find(Language::UpdateRef, name) != Language::UpdateRef.end())
331 buildAndThrowError(fmt::format("`{}' updates a list in-place, and can not be used as a value. Prefer using their copy alternative (without the `!` at the end) when possible", name), x);
332 else if (name == Language::And || name == Language::Or)
333 buildAndThrowError(fmt::format("`{}' can not be used as a value like `+', where (let add +) (add 1 2) would be valid", name), x);
334 else if (getOperator(name).has_value())
335 buildAndThrowError(fmt::format("Found a freestanding operator: `{}`. It can not be used as value like `+', where (let add +) (add 1 2) would be valid", name), x);
336 else
337 {
338 if (can_use_ref)
339 {
340 const std::optional<std::size_t> maybe_local_idx = m_locals_locator.lookupLastScopeByName(name);
341 const uint16_t symbol_id = addSymbol(x);
342 if (maybe_local_idx.has_value())
343 page(p).emplace_back(LOAD_FAST_BY_INDEX, static_cast<uint16_t>(maybe_local_idx.value())).setRelatedResourceId(symbol_id);
344 else
345 page(p).emplace_back(LOAD_FAST, symbol_id);
346 }
347 else
348 page(p).emplace_back(LOAD_SYMBOL, addSymbol(x));
349 }
350
351 page(p).back().setSourceLocation(x.filename(), x.position().start.line);
352
353 if (is_result_unused)
354 {
355 warning("Statement has no effect", x);
356 page(p).emplace_back(POP);
357 page(p).back().setSourceLocation(x.filename(), x.position().start.line);
358 }
359 }
360
361 void ASTLowerer::compileListInstruction(Node& x, const Page p, const bool is_result_unused)
362 {
363 const Node head = x.constList()[0];
364 const std::string& name = head.string();
365 const Instruction inst = getListInstruction(name).value();
366
367 // length of at least 1 since we got a symbol name
368 const auto argc = x.constList().size() - 1u;
369 // error, can not use append/concat/pop (and their in place versions) with a <2 length argument list
370 if (argc < 2 && APPEND <= inst && inst <= SET_AT_2_INDEX)
371 buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), head);
372 if (std::cmp_greater(argc, MaxValue16Bits))
373 buildAndThrowError(fmt::format("Too many arguments ({}), exceeds {}", argc, MaxValue16Bits), x);
374 if (argc != 2 && (inst == POP_LIST || inst == POP_LIST_IN_PLACE))
375 buildAndThrowError(fmt::format("Expected 2 arguments (list, index) for {}, got {}", name, argc), head);
376 if (argc != 3 && inst == SET_AT_INDEX)
377 buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), head);
378 if (argc != 4 && inst == SET_AT_2_INDEX)
379 buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), head);
380
381 // compile arguments in reverse order
382 for (std::size_t i = x.constList().size() - 1u; i > 0; --i)
383 {
384 Node& node = x.list()[i];
385 if (nodeProducesOutput(node))
386 compileExpression(node, p, false, false, true);
387 else
389 }
390
391 // put inst and number of arguments
392 std::size_t inst_argc = 0;
393 switch (inst)
394 {
395 case LIST:
396 inst_argc = argc;
397 break;
398
399 case APPEND:
400 [[fallthrough]];
401 case APPEND_IN_PLACE:
402 [[fallthrough]];
403 case CONCAT:
404 [[fallthrough]];
405 case CONCAT_IN_PLACE:
406 inst_argc = argc - 1;
407 break;
408
409 case POP_LIST:
410 inst_argc = 0;
411 break;
412
413 case SET_AT_INDEX:
414 [[fallthrough]];
415 case SET_AT_2_INDEX:
416 [[fallthrough]];
417 case POP_LIST_IN_PLACE:
418 inst_argc = is_result_unused ? 0 : 1;
419 break;
420
421 default:
422 break;
423 }
424 page(p).emplace_back(inst, static_cast<uint16_t>(inst_argc));
425 page(p).back().setSourceLocation(head.filename(), head.position().start.line);
426
427 if (!is_result_unused && (inst == APPEND_IN_PLACE || inst == CONCAT_IN_PLACE))
428 {
429 // Load the first argument which should be a symbol (or field),
430 // that append!/concat! write to, so that we have its new value available.
431 compileExpression(x.list()[1], p, false, false, true);
432 }
433
434 // append!, concat!, pop!, @= and @@= can push to the stack, but not using its returned value isn't an error
435 if (is_result_unused && (inst == LIST || inst == APPEND || inst == CONCAT || inst == POP_LIST))
436 {
437 warning("Ignoring return value of function", x);
438 page(p).emplace_back(POP);
439 }
440 }
441
442 void ASTLowerer::compileApplyInstruction(Node& x, const Page p, const bool is_result_unused)
443 {
444 const Node head = x.constList()[0];
445 const auto argc = x.constList().size() - 1u;
446
447 if (argc != 2)
448 buildAndThrowError(fmt::format("Expected 2 arguments (function, arguments) for apply, got {}", argc), head);
449
450 const auto label_return = IR::Entity::Label(m_current_label++);
451 page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
452
453 for (Node& node : x.list() | std::ranges::views::drop(1))
454 {
455 if (nodeProducesOutput(node))
456 compileExpression(node, p, false, false, true);
457 else
459 }
460 page(p).emplace_back(APPLY);
461 // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
462 page(p).emplace_back(label_return);
463
464 if (is_result_unused)
465 page(p).emplace_back(POP);
466 }
467
468 void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal, const bool can_use_ref)
469 {
470 if (x.constList().size() == 1)
471 buildAndThrowError("Invalid condition: missing 'cond' and 'then' nodes, expected (if cond then)", x);
472 if (x.constList().size() == 2)
473 buildAndThrowError(fmt::format("Invalid condition: missing 'then' node, expected (if {} then)", x.constList()[1].repr()), x);
474
475 // compile condition
476 compileExpression(x.list()[1], p, false, false, true);
477 page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].position().start.line);
478
479 // jump only if needed to the "true" branch
480 const auto label_then = IR::Entity::Label(m_current_label++);
481 page(p).emplace_back(IR::Entity::GotoIf(label_then, true));
482
483 bool created_vars = false;
484
485 // "false" branch code
486 if (x.constList().size() == 4) // we have an else clause
487 {
489 compileExpression(x.list()[3], p, is_result_unused, is_terminal, can_use_ref);
490 page(p).back().setSourceLocation(x.constList()[3].filename(), x.constList()[3].position().start.line);
491 created_vars = m_locals_locator.dropVarsForBranch();
492 }
493 else
494 {
495 Node tmp = Node(NodeType::List);
496 compileExpression(tmp, p, is_result_unused, is_terminal, true);
497 }
498
499 // when else is finished, jump to end
500 const auto label_end = IR::Entity::Label(m_current_label++);
501 page(p).emplace_back(IR::Entity::Goto(label_end));
502
503 // absolute address to jump to if condition is true
504 page(p).emplace_back(label_then);
505 // if code
507 compileExpression(x.list()[2], p, is_result_unused, is_terminal, can_use_ref);
508 page(p).back().setSourceLocation(x.constList()[2].filename(), x.constList()[2].position().start.line);
509 created_vars = created_vars || m_locals_locator.dropVarsForBranch();
510 // set jump to end pos
511 page(p).emplace_back(label_end);
512
513 // if we have at least one branch that introduced a new variable,
514 // we have to mark the last variable in the current scope as unreachable
515 // to avoid generating bad indices for LOAD_FAST_BY_INDEX
516 if (created_vars)
518 }
519
520 void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused)
521 {
522 if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List)
523 buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args);
524 if (x.constList().size() != 3)
526
527 // capture, if needed
528 std::size_t capture_inst_count = 0;
529 for (const auto& node : x.constList()[1].constList())
530 {
531 if (node.nodeType() == NodeType::Capture)
532 {
533 const uint16_t symbol_id = addSymbol(node);
534
535 // We have an unqualified name that isn't the captured name
536 // This means we need to rename the captured value
537 if (const auto& maybe_nqn = node.getUnqualifiedName(); maybe_nqn.has_value() && maybe_nqn.value() != node.string())
538 {
539 const uint16_t nqn_id = addSymbol(Node(NodeType::Symbol, maybe_nqn.value()));
540
541 page(p).emplace_back(RENAME_NEXT_CAPTURE, nqn_id);
542 page(p).emplace_back(CAPTURE, symbol_id);
543 }
544 else
545 page(p).emplace_back(CAPTURE, symbol_id);
546
547 ++capture_inst_count;
548 }
549 }
550 const bool is_closure = capture_inst_count > 0;
551
553 is_closure
556
557 std::optional<std::string> page_name = std::nullopt;
558 if (!m_opened_vars.empty() && !x.isAnonymousFunction())
559 page_name = m_opened_vars.top().name;
560
561 // create new page for function body
562 const Page function_body_page = createNewCodePage(
563 { .closure = is_closure,
564 .name = page_name });
565 bool mutate_at_least_one_arg = false;
566 // save page_id into the constants table as PageAddr and load the const
567 page(p).emplace_back(is_closure ? MAKE_CLOSURE : LOAD_CONST, addValue(function_body_page.index, x));
568
569 std::size_t arg_count = 0;
570 // pushing arguments from the stack into variables in the new scope
571 for (const auto& node : x.constList()[1].constList() | std::ranges::views::reverse)
572 {
573 if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg)
574 {
575 page(function_body_page).emplace_back(STORE, addSymbol(node));
576 m_locals_locator.addLocal(node.string());
577 arg_count++;
578
579 mutate_at_least_one_arg = node.nodeType() == NodeType::MutArg || mutate_at_least_one_arg;
580 }
581 else if (node.nodeType() == NodeType::RefArg)
582 {
583 page(function_body_page).emplace_back(STORE_REF, addSymbol(node));
584 m_locals_locator.addLocal(node.string());
585 arg_count++;
586 }
587 }
588
589 // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls.
590 // This way we can continue to safely apply optimisations on
591 // (let name (fun (e) (map lst (fun (e) (name e)))))
592 // Otherwise, `name` would have been optimised to a CALL_CURRENT_PAGE, which would have returned the wrong page.
593 if (x.isAnonymousFunction())
594 m_opened_vars.emplace(std::string(IR::AnonymousBlockName), arg_count);
595 // push body of the function
596 compileExpression(x.list()[2], function_body_page, false, true, true);
597 if (x.isAnonymousFunction())
598 m_opened_vars.pop();
599
600 // needed for the IRInliner ; the scope has to be dropped AFTER we set the metadata, as we need it
601 setFunctionMetadata(function_body_page, arg_count, mutate_at_least_one_arg);
602
603 // return last value on the stack
604 page(function_body_page).emplace_back(RET);
606
607 // if the computed function is unused, pop it
608 if (is_result_unused)
609 {
610 warning("Unused declared function", x);
611 page(p).emplace_back(POP);
612 }
613 }
614
615 void ASTLowerer::setFunctionMetadata(const Page p, const std::size_t arg_count, const bool mutates_args)
616 {
617 bool is_recursive = false;
618 bool is_simple = true;
619
620 for (const IR::Entity& e : page(p))
621 {
622 if (e.inst() == TAIL_CALL_SELF || e.inst() == CALL_CURRENT_PAGE)
623 is_recursive = true;
624 if (e.inst() == APPLY || e.inst() == CALL || e.inst() == CALL_SYMBOL || e.inst() == CALL_SYMBOL_BY_INDEX ||
625 e.inst() == MAKE_CLOSURE)
626 is_simple = false;
627 }
628
629 block(p).metadata.argument_count = arg_count;
630 block(p).metadata.is_recursive = is_recursive;
631 block(p).metadata.is_simple = is_simple;
632 block(p).metadata.is_mutating_args = mutates_args;
633 }
634
635 void ASTLowerer::compileLetMutSet(const Keyword n, Node& x, const Page p, const bool is_result_unused)
636 {
637 if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol)
638 buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym);
639 if (x.constList().size() != 3)
641
642 const std::string name = x.constList()[1].string();
643 uint16_t i = addSymbol(x.constList()[1]);
644
645 if (!m_opened_vars.empty() && m_opened_vars.top().name == name)
646 buildAndThrowError("Can not define a variable using the same name as the function it is defined inside. You need to rename the function or the variable", x);
647
648 const bool is_function = x.constList()[2].isFunction();
649 if (is_function)
650 {
651 std::size_t arg_count = 0;
652 if (x.constList()[2].nodeType() == NodeType::List && x.constList()[2].constList().size() >= 2 &&
653 x.constList()[2].constList()[1].nodeType() == NodeType::List)
654 {
655 for (const auto& node : x.constList()[2].constList()[1].constList())
656 {
657 if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg || node.nodeType() == NodeType::RefArg)
658 arg_count++;
659 }
660 }
661 m_opened_vars.push(Var(name, arg_count));
662 x.list()[2].setFunctionKind(/* anonymous= */ false);
663 }
664
665 // put value before symbol id
666 // starting at index = 2 because x is a (let|mut|set variable ...) node
667 compileExpression(x.list()[2], p, false, false, true);
668
669 if (n == Keyword::Let || n == Keyword::Mut)
670 {
671 page(p).emplace_back(STORE, i);
673 }
674 else
675 page(p).emplace_back(SET_VAL, i);
676
677 if (!is_result_unused)
678 page(p).emplace_back(LOAD_SYMBOL, i);
679
680 if (is_function)
681 m_opened_vars.pop();
682 page(p).back().setSourceLocation(x.filename(), x.position().start.line);
683 }
684
686 {
687 if (x.constList().size() != 3)
689
691 page(p).emplace_back(CREATE_SCOPE);
692 page(p).back().setSourceLocation(x.filename(), x.position().start.line);
693
694 // save current position to jump there at the end of the loop
695 const auto label_loop = IR::Entity::Label(m_current_label++);
696 page(p).emplace_back(label_loop);
697 // push condition
698 compileExpression(x.list()[1], p, false, false, true);
699 // absolute jump to end of block if condition is false
700 const auto label_end = IR::Entity::Label(m_current_label++);
701 page(p).emplace_back(IR::Entity::GotoIf(label_end, false));
702 // push code to page
703 compileExpression(x.list()[2], p, true, false, true);
704
705 // reset the scope at the end of the loop so that indices are still valid
706 // otherwise, (while true { (let a 5) (print a) (let b 6) (print b) })
707 // would print 5, 6, then only 6 as we emit LOAD_FAST_BY_INDEX 0 and b is the last in the scope
708 // loop, jump to the condition
709 page(p).emplace_back(IR::Entity::Goto(label_loop, RESET_SCOPE_JUMP));
710
711 // absolute address to jump to if condition is false
712 page(p).emplace_back(label_end);
713
714 page(p).emplace_back(POP_SCOPE);
716 }
717
719 {
720 std::string path;
721 const Node package_node = x.constList()[1];
722 for (std::size_t i = 0, end = package_node.constList().size(); i < end; ++i)
723 {
724 path += package_node.constList()[i].string();
725 if (i + 1 != end)
726 path += "/";
727 }
728 path += ".arkm";
729
730 // register plugin path in the constants table
731 uint16_t id = addValue(Node(NodeType::String, path));
732 // add plugin instruction + id of the constant referring to the plugin path
733 page(p).emplace_back(PLUGIN, id);
734 page(p).back().setSourceLocation(x.filename(), x.position().start.line);
735 }
736
737 void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call)
738 {
739 const auto node = call.constList()[0];
740
741 for (Node& value : std::ranges::drop_view(call.list(), 1))
742 {
743 if (nodeProducesOutput(value) || isBreakpoint(value))
744 {
745 if (value.nodeType() == NodeType::Symbol)
746 {
747 // we have to disallow usage of references in tail calls, because if we shuffle arguments around while using refs, they will end up with the same value
748 if (is_tail_call)
749 compileSymbol(value, p, /* is_result_unused= */ false, /* can_use_ref= */ false);
750 else
751 compileSymbol(value, p, /* is_result_unused= */ false, /* can_use_ref= */ true);
752 }
753 else
754 compileExpression(value, p, /* is_result_unused= */ false, /* is_terminal= */ false, /* can_use_ref= */ false);
755 }
756 else
758 }
759 }
760
761 void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal, const bool can_use_ref)
762 {
763 const Node& node = x.constList()[0];
764 bool matched = false;
765
766 if (node.nodeType() == NodeType::Symbol)
767 {
768 if (node.string() == Language::And || node.string() == Language::Or)
769 {
770 matched = true;
771 handleShortcircuit(x, p, can_use_ref);
772 }
773 if (const auto maybe_operator = getOperator(node.string()); maybe_operator.has_value())
774 {
775 matched = true;
776 if (maybe_operator.value() == BREAKPOINT)
777 is_result_unused = false;
778 handleOperator(x, p, maybe_operator.value());
779 }
780 }
781
782 if (!matched)
783 {
784 // if nothing else matched, then compile a function call
785 if (handleFunctionCall(x, p, is_terminal))
786 // if it returned true, we compiled a tail call, skip the POP at the end
787 return;
788 }
789
790 if (is_result_unused)
791 page(p).emplace_back(POP);
792 }
793
794 void ASTLowerer::handleShortcircuit(Node& x, const Page p, const bool can_use_ref)
795 {
796 const Node& node = x.constList()[0];
797 const auto name = node.string(); // and / or
798 const Instruction inst = name == Language::And ? SHORTCIRCUIT_AND : SHORTCIRCUIT_OR;
799
800 // short circuit implementation
801 if (x.constList().size() < 3)
803 fmt::format(
804 "Expected at least 2 arguments while compiling '{}', got {}",
805 name,
806 x.constList().size() - 1),
807 x);
808
809 if (!nodeProducesOutput(x.list()[1]))
811 fmt::format(
812 "Can not use `{}' inside a `{}' expression, as it doesn't return a value",
813 x.list()[1].repr(), name),
814 x.list()[1]);
815 compileExpression(x.list()[1], p, false, false, can_use_ref);
816
817 const auto label_shortcircuit = IR::Entity::Label(m_current_label++);
818 auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, inst);
819 page(p).emplace_back(shortcircuit_entity);
820
821 for (std::size_t i = 2, end = x.constList().size(); i < end; ++i)
822 {
823 if (!nodeProducesOutput(x.list()[i]))
825 fmt::format(
826 "Can not use `{}' inside a `{}' expression, as it doesn't return a value",
827 x.list()[i].repr(), name),
828 x.list()[i]);
829 compileExpression(x.list()[i], p, false, false, can_use_ref);
830 if (i + 1 != end)
831 page(p).emplace_back(shortcircuit_entity);
832 }
833
834 page(p).emplace_back(label_shortcircuit);
835 }
836
838 {
839 constexpr std::size_t start_index = 1;
840 const Node& node = x.constList()[0];
841 const auto op_name = Language::operators[static_cast<std::size_t>(op - FirstOperator)];
842
843 // push arguments on current page
844 std::size_t exp_count = 0;
845 for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index)
846 {
847 const bool is_breakpoint = isBreakpoint(x.constList()[index]);
848 if (nodeProducesOutput(x.constList()[index]) || is_breakpoint)
849 compileExpression(x.list()[index], p, false, false, true);
850 else
852
853 if (!is_breakpoint)
854 exp_count++;
855
856 // in order to be able to handle things like (op A B C D...)
857 // which should be transformed into A B op C op D op...
858 if (exp_count >= 2 && !isTernaryInst(op) && !is_breakpoint)
859 page(p).emplace_back(op);
860 }
861
862 if (isBreakpoint(x))
863 {
864 if (exp_count > 1)
865 buildAndThrowError(fmt::format("`{}' expected at most one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
866 page(p).emplace_back(op, exp_count);
867 }
868 else if (isUnaryInst(op))
869 {
870 if (exp_count != 1)
871 buildAndThrowError(fmt::format("`{}' expected one argument, but was called with {}", op_name, exp_count), x.constList()[0]);
872 page(p).emplace_back(op);
873 }
874 else if (isTernaryInst(op))
875 {
876 if (exp_count != 3)
877 buildAndThrowError(fmt::format("`{}' expected three arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
878 page(p).emplace_back(op);
879 }
880 else if (exp_count <= 1)
881 buildAndThrowError(fmt::format("`{}' expected two arguments, but was called with {}", op_name, exp_count), x.constList()[0]);
882
883 // need to check we didn't push the (op A B C D...) things for operators not supporting it
884 if (exp_count > 2 && !isRepeatableOperation(op) && !isTernaryInst(op))
885 buildAndThrowError(fmt::format("`{}' requires 2 arguments, but got {}.", op_name, exp_count), x);
886
887 page(p).back().setSourceLocation(x.filename(), x.position().start.line);
888 }
889
890 bool ASTLowerer::handleFunctionCall(Node& x, const Page p, const bool is_terminal)
891 {
892 constexpr std::size_t start_index = 1;
893 Node& node = x.list()[0];
894
895 // number of arguments
896 std::size_t args_count = 0;
897 for (auto it = x.constList().begin() + start_index, it_end = x.constList().end(); it != it_end; ++it)
898 {
899 if (it->nodeType() != NodeType::Capture && !isBreakpoint(*it))
900 args_count++;
901 }
902
903 if (is_terminal && node.nodeType() == NodeType::Symbol && isFunctionCallingItself(node.string()))
904 {
905 pushFunctionCallArguments(x, p, /* is_tail_call= */ true);
906
907 if (const std::size_t expected_arg_count = m_opened_vars.top().argument_count; args_count != expected_arg_count)
908 {
909 std::vector<std::string> arg_names;
910 if (expected_arg_count > 0)
911 {
912 arg_names.reserve(expected_arg_count + 1);
913 arg_names.emplace_back("");
914 for (std::size_t i = 0; i < expected_arg_count; ++i)
915 arg_names.emplace_back(1, static_cast<char>('a' + i));
916 }
917
919 fmt::format(
920 "When performing tail-call `{}', received {} argument{}, but expected {}: `({}{})'",
921 x.repr(),
922 args_count,
923 args_count > 1 ? "s" : "",
924 expected_arg_count,
925 node.string(),
926 fmt::join(arg_names, " ")),
927 x);
928 }
929
930 // jump to the top of the function
931 page(p).emplace_back(TAIL_CALL_SELF);
932 page(p).back().setSourceLocation(node.filename(), node.position().start.line);
933 return true; // skip the potential Instruction::POP at the end
934 }
935
936 if (!nodeProducesOutput(node))
937 buildAndThrowError(fmt::format("Can not call `{}', as it doesn't return a value", node.repr()), node);
938
939 const IR::Entity label_return = IR::Entity::Label(m_current_label++);
940 page(p).emplace_back(IR::Entity::Goto(label_return, PUSH_RETURN_ADDRESS));
941 page(p).back().setSourceLocation(x.filename(), x.position().start.line);
942
943 const Page proc_page = createNewCodePage({ .temp = true });
944 CallType call_type = CallType::Classic;
945 std::optional<uint16_t> call_arg = std::nullopt;
946
947 // compile the function resolution to a separate page
949 {
950 // The function is trying to call itself, but this isn't a tail call.
951 // We can skip the LOAD_FAST function_name and directly push the current
952 // function page, which will be quicker than a local variable resolution.
953 // We set its argument to the symbol id of the function we are calling,
954 // so that the VM knows the name of the last called function.
955 call_type = CallType::SelfNotRecursive;
956 }
957 else
958 {
959 // closure chains have been handled (eg: closure.field.field.function)
960 compileExpression(node, proc_page, false, false, true);
961
962 if (page(proc_page).empty())
963 buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x);
964 else if (page(proc_page).back().inst() == GET_FIELD)
965 // the last GET_FIELD instruction should push the closure environment with it
966 page(proc_page).back().replaceInstruction(GET_FIELD_AS_CLOSURE);
967 else if (page(proc_page).size() == 1)
968 {
969 const Instruction inst = page(proc_page).back().inst();
970 const uint16_t arg = page(proc_page).back().primaryArg();
971
972 if (inst == LOAD_FAST)
973 {
974 call_type = CallType::Symbol;
975 call_arg = arg;
976 // we don't want to push any instruction, as we'll use an optimised instruction instead of CALL
977 page(proc_page).clear();
978 }
979 else if (inst == LOAD_FAST_BY_INDEX)
980 {
981 call_type = CallType::SymbolByIndex;
982 page(proc_page).clear();
983 }
984 else if (inst == BUILTIN && Builtins::builtins[arg].second.isFunction())
985 {
986 call_type = CallType::Builtin;
987 call_arg = arg;
988 page(proc_page).clear();
989 }
990 else if (inst == LOAD_CONST)
991 call_arg = arg;
992 }
993 }
994
995 // push proc from temp page
996 for (const auto& inst : page(proc_page))
997 page(p).push_back(inst);
998 m_temp_pages.pop_back();
999
1000 pushFunctionCallArguments(x, p, /* is_tail_call= */ false);
1001
1002 // call the procedure
1003 switch (call_type)
1004 {
1005 case CallType::Classic:
1006 page(p).emplace_back(CALL, args_count).setRelatedResourceId(call_arg);
1007 break;
1008
1010 page(p).emplace_back(CALL_CURRENT_PAGE, addSymbol(node), args_count);
1011 break;
1012
1013 case CallType::Symbol:
1014 assert(call_arg.has_value() && "Expected a value for call_arg with CallType::Symbol");
1015 page(p).emplace_back(CALL_SYMBOL, call_arg.value(), args_count).setRelatedResourceId(call_arg.value());
1016 break;
1017
1019 {
1020 const Page temp_page = createNewCodePage({ .temp = true });
1021 compileExpression(node, temp_page, false, false, true);
1022 assert(page(temp_page).size() == 1 && page(temp_page).back().inst() == LOAD_FAST_BY_INDEX);
1023 page(p).emplace_back(CALL_SYMBOL_BY_INDEX, page(temp_page).back().primaryArg(), args_count).setRelatedResourceId(page(temp_page).back().relatedResourceId());
1024 m_temp_pages.pop_back();
1025 break;
1026 }
1027
1028 case CallType::Builtin:
1029 assert(call_arg.has_value() && "Expected a value for call_arg with CallType::Builtin");
1030 page(p).emplace_back(CALL_BUILTIN, call_arg.value(), args_count);
1031 break;
1032 }
1033 page(p).back().setSourceLocation(node.filename(), node.position().start.line);
1034
1035 // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP)
1036 page(p).emplace_back(label_return);
1037 return false; // we didn't compile a tail call
1038 }
1039
1040 uint16_t ASTLowerer::addSymbol(const Node& sym)
1041 {
1042 // otherwise, add the symbol, and return its id in the table
1043 auto it = std::ranges::find(m_symbols, sym.string());
1044 if (it == m_symbols.end())
1045 {
1046 m_symbols.push_back(sym.string());
1047 it = m_symbols.begin() + static_cast<std::vector<std::string>::difference_type>(m_symbols.size() - 1);
1048 }
1049
1050 const auto distance = std::distance(m_symbols.begin(), it);
1051 if (std::cmp_less(distance, MaxValue16Bits))
1052 return static_cast<uint16_t>(distance);
1053 buildAndThrowError(fmt::format("Too many symbols (exceeds {}), aborting compilation.", MaxValue16Bits), sym);
1054 }
1055
1056 uint16_t ASTLowerer::addValue(const Node& x)
1057 {
1058 const ValTableElem v(x);
1059 auto it = std::ranges::find(m_values, v);
1060 if (it == m_values.end())
1061 {
1062 m_values.push_back(v);
1063 it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
1064 }
1065
1066 const auto distance = std::distance(m_values.begin(), it);
1067 if (std::cmp_less(distance, MaxValue16Bits))
1068 return static_cast<uint16_t>(distance);
1069 buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), x);
1070 }
1071
1072 uint16_t ASTLowerer::addValue(const std::size_t page_id, const Node& current)
1073 {
1074 const ValTableElem v(page_id);
1075 auto it = std::ranges::find(m_values, v);
1076 if (it == m_values.end())
1077 {
1078 m_values.push_back(v);
1079 it = m_values.begin() + static_cast<std::vector<ValTableElem>::difference_type>(m_values.size() - 1);
1080 }
1081
1082 const auto distance = std::distance(m_values.begin(), it);
1083 if (std::cmp_less(distance, MaxValue16Bits))
1084 return static_cast<uint16_t>(distance);
1085 buildAndThrowError(fmt::format("Too many values (exceeds {}), aborting compilation.", MaxValue16Bits), current);
1086 }
1087}
Host the declaration of all the ArkScript builtins.
Tools to report code errors nicely to the user.
ArkScript homemade exceptions.
User defined literals for Ark internals.
const String_t & string() const
Definition Value.hpp:167
uint16_t addValue(const Node &x)
Register a given node in the value table.
IR::Block::vec_t & page(const Page page) noexcept
helper functions to get a temp or finalised code page
void pushFunctionCallArguments(Node &call, Page p, bool is_tail_call)
bool handleFunctionCall(Node &x, Page p, bool is_terminal)
uint16_t addSymbol(const Node &sym)
Register a given node in the symbol table.
IR::Block & block(const Page page) noexcept
static std::optional< Instruction > getListInstruction(const std::string &name) noexcept
Checking if a symbol is a list instruction.
std::vector< ValTableElem > m_values
std::stack< Var > m_opened_vars
stack of vars we are currently declaring
void handleShortcircuit(Node &x, Page p, bool can_use_ref)
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
static bool isRepeatableOperation(Instruction inst) noexcept
Check if an operator can be repeated.
const std::vector< ValTableElem > & values() const noexcept
Return the value table pre-computed.
void process(Node &ast)
Start the compilation.
void offsetPagesBy(std::size_t offset)
Start bytecode pages at a given offset (by default, 0)
void compileExpression(Node &x, Page p, bool is_result_unused, bool is_terminal, bool can_use_ref)
Compile an expression (a node) recursively.
const std::vector< IR::Block > & intermediateRepresentation() const noexcept
Return the IR blocks (one per scope)
static bool isBreakpoint(const Node &node)
static bool isUnaryInst(Instruction inst) noexcept
Check if a given instruction is unary (takes only one argument)
void compileLetMutSet(Keyword n, Node &x, Page p, bool is_result_unused)
Page createNewCodePage(PageCreationData &&args=PageCreationData {}) noexcept
std::vector< IR::Block > m_code_pages
ASTLowerer(unsigned debug)
Construct a new ASTLowerer object.
static void makeError(ErrorKind kind, const Node &node, const std::string &additional_ctx)
Throw a nice error message, using a message builder.
void compileWhile(Node &x, Page p)
void handleOperator(Node &x, Page p, Instruction op)
void compileApplyInstruction(Node &x, Page p, bool is_result_unused)
std::vector< std::string > m_symbols
void handleCalls(Node &x, Page p, bool is_result_unused, bool is_terminal, bool can_use_ref)
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)
LocalsLocator m_locals_locator
void compileSymbol(const Node &x, Page p, bool is_result_unused, bool can_use_ref)
void warning(const std::string &message, const Node &node)
Display a warning message.
bool isFunctionCallingItself(const std::string &name) noexcept
Check if we are in a recursive self call.
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.
void compilePluginImport(const Node &x, Page p)
std::size_t m_start_page_at_offset
Used to offset the page numbers when compiling code in the debugger.
static std::optional< Instruction > getOperator(const std::string &name) noexcept
Checking if a symbol is an operator.
const std::vector< std::string > & symbols() const noexcept
Return the symbol table pre-computed.
void setFunctionMetadata(Page p, std::size_t arg_count, bool mutates_args)
void addToTables(const std::vector< std::string > &symbols, const std::vector< ValTableElem > &constants)
Pre-fill tables (used by the debugger)
void compileIf(Node &x, Page p, bool is_result_unused, bool is_terminal, bool can_use_ref)
static Entity Goto(const Entity &label, Instruction inst=Instruction::JUMP)
Create a new Goto IR Entity.
Definition Entity.cpp:43
static Entity Label(label_t value)
Create a new Label IR Entity.
Definition Entity.cpp:35
static Entity GotoIf(const Entity &label, bool cond)
Create a new Goto IR Entity.
Definition Entity.cpp:62
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...
bool 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 markLastLocalAsUnreachable()
Mark the last variable of a scope as unreachable, blocking lookupLastScopeByName(....
bool colorize() const noexcept
Check if logs can be colorized.
Definition Logger.hpp:157
void warn(const char *fmt, Args &&... args)
Write a warn level log using fmtlib.
Definition Logger.hpp:80
void traceStart(std::string &&trace_name)
Definition Logger.hpp:109
A node of an Abstract Syntax Tree for ArkScript.
Definition Node.hpp:32
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
FileSpan position() const noexcept
Get the span of the node (start and end)
Definition Node.cpp:169
const Namespace & constArkNamespace() const noexcept
Return the namespace held by the value (if the node type allows it)
Definition Node.cpp:58
std::vector< Node > & list() noexcept
Return the list of sub-nodes held by the node.
Definition Node.cpp:68
An interface to describe compiler passes.
Definition Pass.hpp:24
std::string makeContextWithNode(const std::string &message, const internal::Node &node, bool colorize=true)
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::string_view AnonymousBlockName
Definition Entity.hpp:42
constexpr std::array< std::string_view, 9 > listInstructions
Definition Common.hpp:121
constexpr std::string_view Apply
Definition Common.hpp:140
constexpr std::array< std::string_view, 24 > operators
Definition Common.hpp:161
constexpr std::string_view And
Definition Common.hpp:137
constexpr std::array UpdateRef
All the builtins that modify in place a variable.
Definition Common.hpp:114
constexpr std::string_view Or
Definition Common.hpp:138
ARK_ALWAYS_INLINE std::string typeToString(const Node &node) noexcept
Definition Node.hpp:280
constexpr uint8_t FirstOperator
Keyword
The different keywords available.
Definition Common.hpp:79
Instruction
The different bytecodes are stored here.
constexpr uint16_t MaxValue16Bits
Definition Constants.hpp:81
CodeError thrown by the compiler (parser, macro processor, optimizer, and compiler itself)
std::size_t line
0-indexed line number
Definition Position.hpp:22
bool is_simple
Calls only builtin and operators, no user functions/C++ functions.
Definition Entity.hpp:251
struct Ark::internal::IR::Block::Metadata metadata
std::shared_ptr< Node > ast
Definition Namespace.hpp:18
A Compiler Value class helper to handle multiple types.