ArkScript
A small, fast, functional and scripting language for video games
VM.cpp
Go to the documentation of this file.
1#include <Ark/VM/VM.hpp>
2
3#include <utility>
4#include <numeric>
5#include <limits>
6#include <ranges>
7#include <fmt/core.h>
8#include <fmt/color.h>
9#include <fmt/ostream.h>
10
11#include <Ark/Files.hpp>
12#include <Ark/Utils.hpp>
13#include <Ark/TypeChecker.hpp>
15
16struct mapping
17{
18 char* name;
19 Ark::Value (*value)(std::vector<Ark::Value>&, Ark::VM*);
20};
21
22namespace Ark
23{
24 using namespace internal;
25
26 namespace helper
27 {
28 inline Value tail(Value* a)
29 {
30 if (a->valueType() == ValueType::List)
31 {
32 if (a->constList().size() < 2)
33 return Value(ValueType::List);
34
35 std::vector<Value> tmp(a->constList().size() - 1);
36 for (std::size_t i = 1, end = a->constList().size(); i < end; ++i)
37 tmp[i - 1] = a->constList()[i];
38 return Value(std::move(tmp));
39 }
40 if (a->valueType() == ValueType::String)
41 {
42 if (a->string().size() < 2)
44
45 Value b { *a };
46 b.stringRef().erase(b.stringRef().begin());
47 return b;
48 }
49
51 "tail",
54 { *a });
55 }
56
57 inline Value head(Value* a)
58 {
59 if (a->valueType() == ValueType::List)
60 {
61 if (a->constList().empty())
62 return Builtins::nil;
63 return a->constList()[0];
64 }
65 if (a->valueType() == ValueType::String)
66 {
67 if (a->string().empty())
69 return Value(std::string(1, a->stringRef()[0]));
70 }
71
73 "head",
76 { *a });
77 }
78 }
79
80 VM::VM(State& state) noexcept :
81 m_state(state), m_exit_code(0), m_running(false)
82 {
83 m_execution_contexts.emplace_back(std::make_unique<ExecutionContext>());
84 }
85
86 void VM::init() noexcept
87 {
88 ExecutionContext& context = *m_execution_contexts.back();
89 for (const auto& c : m_execution_contexts)
90 {
91 c->ip = 0;
92 c->pp = 0;
93 c->sp = 0;
94 }
95
96 context.sp = 0;
97 context.fc = 1;
98
100 context.stacked_closure_scopes.clear();
101 context.stacked_closure_scopes.emplace_back(nullptr);
102
103 context.saved_scope.reset();
104 m_exit_code = 0;
105
106 context.locals.clear();
107 context.locals.emplace_back(context.scopes_storage.data(), 0);
108
109 // loading bound stuff
110 // put them in the global frame if we can, aka the first one
111 for (const auto& [sym_id, value] : m_state.m_binded)
112 {
113 auto it = std::ranges::find(m_state.m_symbols, sym_id);
114 if (it != m_state.m_symbols.end())
115 context.locals[0].push_back(static_cast<uint16_t>(std::distance(m_state.m_symbols.begin(), it)), value);
116 }
117 }
118
119 Value& VM::operator[](const std::string& name) noexcept
120 {
121 // find id of object
122 const auto it = std::ranges::find(m_state.m_symbols, name);
123 if (it == m_state.m_symbols.end())
124 {
125 m_no_value = Builtins::nil;
126 return m_no_value;
127 }
128
129 const auto dist = std::distance(m_state.m_symbols.begin(), it);
130 if (std::cmp_less(dist, std::numeric_limits<uint16_t>::max()))
131 {
132 ExecutionContext& context = *m_execution_contexts.front();
133
134 const auto id = static_cast<uint16_t>(dist);
135 Value* var = findNearestVariable(id, context);
136 if (var != nullptr)
137 return *var;
138 }
139
140 m_no_value = Builtins::nil;
141 return m_no_value;
142 }
143
144 void VM::loadPlugin(const uint16_t id, ExecutionContext& context)
145 {
146 namespace fs = std::filesystem;
147
148 const std::string file = m_state.m_constants[id].stringRef();
149
150 std::string path = file;
151 // bytecode loaded from file
153 path = (fs::path(m_state.m_filename).parent_path() / fs::path(file)).relative_path().string();
154
155 std::shared_ptr<SharedLibrary> lib;
156 // if it exists alongside the .arkc file
157 if (Utils::fileExists(path))
158 lib = std::make_shared<SharedLibrary>(path);
159 else
160 {
161 for (auto const& v : m_state.m_libenv)
162 {
163 std::string lib_path = (fs::path(v) / fs::path(file)).string();
164
165 // if it's already loaded don't do anything
166 if (std::ranges::find_if(m_shared_lib_objects, [&](const auto& val) {
167 return (val->path() == path || val->path() == lib_path);
168 }) != m_shared_lib_objects.end())
169 return;
170
171 // check in lib_path
172 if (Utils::fileExists(lib_path))
173 {
174 lib = std::make_shared<SharedLibrary>(lib_path);
175 break;
176 }
177 }
178 }
179
180 if (!lib)
181 {
182 auto lib_path = std::accumulate(
183 std::next(m_state.m_libenv.begin()),
184 m_state.m_libenv.end(),
185 m_state.m_libenv[0].string(),
186 [](const std::string& a, const fs::path& b) -> std::string {
187 return a + "\n\t- " + b.string();
188 });
190 ErrorKind::Module,
191 fmt::format("Could not find module '{}'. Searched under\n\t- {}\n\t- {}", file, path, lib_path));
192 }
193
194 m_shared_lib_objects.emplace_back(lib);
195
196 // load the mapping from the dynamic library
197 try
198 {
199 const mapping* map = m_shared_lib_objects.back()->get<mapping* (*)()>("getFunctionsMapping")();
200 // load the mapping data
201 std::size_t i = 0;
202 while (map[i].name != nullptr)
203 {
204 // put it in the global frame, aka the first one
205 auto it = std::ranges::find(m_state.m_symbols, std::string(map[i].name));
206 if (it != m_state.m_symbols.end())
207 context.locals[0].push_back(static_cast<uint16_t>(std::distance(m_state.m_symbols.begin(), it)), Value(map[i].value));
208
209 ++i;
210 }
211 }
212 catch (const std::system_error& e)
213 {
215 ErrorKind::Module,
216 fmt::format(
217 "An error occurred while loading module '{}': {}\nIt is most likely because the versions of the module and the language don't match.",
218 file, e.what()));
219 }
220 }
221
222 void VM::exit(const int code) noexcept
223 {
224 m_exit_code = code;
225 m_running = false;
226 }
227
229 {
230 const std::lock_guard lock(m_mutex);
231
232 m_execution_contexts.push_back(std::make_unique<ExecutionContext>());
233 ExecutionContext* ctx = m_execution_contexts.back().get();
234 ctx->stacked_closure_scopes.emplace_back(nullptr);
235
236 ctx->locals.reserve(m_execution_contexts.front()->locals.size());
237 ctx->scopes_storage = m_execution_contexts.front()->scopes_storage;
238 for (const auto& local : m_execution_contexts.front()->locals)
239 {
240 auto& scope = ctx->locals.emplace_back(ctx->scopes_storage.data(), local.m_start);
241 scope.m_size = local.m_size;
242 scope.m_min_id = local.m_min_id;
243 scope.m_max_id = local.m_max_id;
244 }
245
246 return ctx;
247 }
248
250 {
251 const std::lock_guard lock(m_mutex);
252
253 const auto it =
254 std::ranges::remove_if(
256 [ec](const std::unique_ptr<ExecutionContext>& ctx) {
257 return ctx.get() == ec;
258 })
259 .begin();
260 m_execution_contexts.erase(it);
261 }
262
263 Future* VM::createFuture(std::vector<Value>& args)
264 {
266 // so that we have access to the presumed symbol id of the function we are calling
267 // assuming that the callee is always the global context
268 ctx->last_symbol = m_execution_contexts.front()->last_symbol;
269
270 // doing this after having created the context
271 // because the context uses the mutex and we don't want a deadlock
272 const std::lock_guard lock(m_mutex);
273 m_futures.push_back(std::make_unique<Future>(ctx, this, args));
274
275 return m_futures.back().get();
276 }
277
279 {
280 const std::lock_guard lock(m_mutex);
281
282 const auto it =
283 std::ranges::remove_if(
284 m_futures,
285 [f](const std::unique_ptr<Future>& future) {
286 return future.get() == f;
287 })
288 .begin();
289 m_futures.erase(it);
290 }
291
293 {
294 // load the mapping from the dynamic library
295 try
296 {
297 for (const auto& shared_lib : m_shared_lib_objects)
298 {
299 const mapping* map = shared_lib->template get<mapping* (*)()>("getFunctionsMapping")();
300 // load the mapping data
301 std::size_t i = 0;
302 while (map[i].name != nullptr)
303 {
304 // put it in the global frame, aka the first one
305 auto it = std::ranges::find(m_state.m_symbols, std::string(map[i].name));
306 if (it != m_state.m_symbols.end())
307 m_execution_contexts[0]->locals[0].push_back(
308 static_cast<uint16_t>(std::distance(m_state.m_symbols.begin(), it)),
309 Value(map[i].value));
310
311 ++i;
312 }
313 }
314
315 return true;
316 }
317 catch (const std::system_error&)
318 {
319 return false;
320 }
321 }
322
323 int VM::run(const bool fail_with_exception)
324 {
325 init();
326 safeRun(*m_execution_contexts[0], 0, fail_with_exception);
327 return m_exit_code;
328 }
329
330 int VM::safeRun(ExecutionContext& context, std::size_t untilFrameCount, bool fail_with_exception)
331 {
332#if ARK_USE_COMPUTED_GOTOS
333# define TARGET(op) TARGET_##op:
334# define DISPATCH_GOTO() \
335 _Pragma("GCC diagnostic push") \
336 _Pragma("GCC diagnostic ignored \"-Wpedantic\"") goto* opcode_targets[inst];
337 _Pragma("GCC diagnostic pop")
338# define GOTO_HALT() goto dispatch_end
339#else
340# define TARGET(op) case op:
341# define DISPATCH_GOTO() goto dispatch_opcode
342# define GOTO_HALT() break
343#endif
344
345#define NEXTOPARG() \
346 do \
347 { \
348 inst = m_state.m_pages[context.pp][context.ip]; \
349 padding = m_state.m_pages[context.pp][context.ip + 1]; \
350 arg = static_cast<uint16_t>((m_state.m_pages[context.pp][context.ip + 2] << 8) + \
351 m_state.m_pages[context.pp][context.ip + 3]); \
352 context.ip += 4; \
353 } while (false)
354#define DISPATCH() \
355 NEXTOPARG(); \
356 DISPATCH_GOTO();
357#define UNPACK_ARGS() \
358 do \
359 { \
360 secondary_arg = static_cast<uint16_t>((padding << 4) | (arg & 0xf000) >> 12); \
361 primary_arg = arg & 0x0fff; \
362 } while (false)
363
364#if ARK_USE_COMPUTED_GOTOS
365# pragma GCC diagnostic push
366# pragma GCC diagnostic ignored "-Wpedantic"
367 constexpr std::array opcode_targets = {
368 // cppcheck-suppress syntaxError ; cppcheck do not know about labels addresses (GCC extension)
369 &&TARGET_NOP,
370 &&TARGET_LOAD_SYMBOL,
371 &&TARGET_LOAD_SYMBOL_BY_INDEX,
372 &&TARGET_LOAD_CONST,
373 &&TARGET_POP_JUMP_IF_TRUE,
374 &&TARGET_STORE,
375 &&TARGET_SET_VAL,
376 &&TARGET_POP_JUMP_IF_FALSE,
377 &&TARGET_JUMP,
378 &&TARGET_RET,
379 &&TARGET_HALT,
380 &&TARGET_CALL,
381 &&TARGET_CAPTURE,
382 &&TARGET_BUILTIN,
383 &&TARGET_DEL,
384 &&TARGET_MAKE_CLOSURE,
385 &&TARGET_GET_FIELD,
386 &&TARGET_PLUGIN,
387 &&TARGET_LIST,
388 &&TARGET_APPEND,
389 &&TARGET_CONCAT,
390 &&TARGET_APPEND_IN_PLACE,
391 &&TARGET_CONCAT_IN_PLACE,
392 &&TARGET_POP_LIST,
393 &&TARGET_POP_LIST_IN_PLACE,
394 &&TARGET_SET_AT_INDEX,
395 &&TARGET_SET_AT_2_INDEX,
396 &&TARGET_POP,
397 &&TARGET_DUP,
398 &&TARGET_CREATE_SCOPE,
399 &&TARGET_RESET_SCOPE,
400 &&TARGET_POP_SCOPE,
401 &&TARGET_ADD,
402 &&TARGET_SUB,
403 &&TARGET_MUL,
404 &&TARGET_DIV,
405 &&TARGET_GT,
406 &&TARGET_LT,
407 &&TARGET_LE,
408 &&TARGET_GE,
409 &&TARGET_NEQ,
410 &&TARGET_EQ,
411 &&TARGET_LEN,
412 &&TARGET_EMPTY,
413 &&TARGET_TAIL,
414 &&TARGET_HEAD,
415 &&TARGET_ISNIL,
416 &&TARGET_ASSERT,
417 &&TARGET_TO_NUM,
418 &&TARGET_TO_STR,
419 &&TARGET_AT,
420 &&TARGET_AT_AT,
421 &&TARGET_MOD,
422 &&TARGET_TYPE,
423 &&TARGET_HASFIELD,
424 &&TARGET_NOT,
425 &&TARGET_LOAD_CONST_LOAD_CONST,
426 &&TARGET_LOAD_CONST_STORE,
427 &&TARGET_LOAD_CONST_SET_VAL,
428 &&TARGET_STORE_FROM,
429 &&TARGET_STORE_FROM_INDEX,
430 &&TARGET_SET_VAL_FROM,
431 &&TARGET_SET_VAL_FROM_INDEX,
432 &&TARGET_INCREMENT,
433 &&TARGET_INCREMENT_BY_INDEX,
434 &&TARGET_DECREMENT,
435 &&TARGET_DECREMENT_BY_INDEX,
436 &&TARGET_STORE_TAIL,
437 &&TARGET_STORE_TAIL_BY_INDEX,
438 &&TARGET_STORE_HEAD,
439 &&TARGET_STORE_HEAD_BY_INDEX,
440 &&TARGET_SET_VAL_TAIL,
441 &&TARGET_SET_VAL_TAIL_BY_INDEX,
442 &&TARGET_SET_VAL_HEAD,
443 &&TARGET_SET_VAL_HEAD_BY_INDEX,
444 &&TARGET_CALL_BUILTIN
445 };
446
447 static_assert(opcode_targets.size() == static_cast<std::size_t>(Instruction::InstructionsCount) && "Some instructions are not implemented in the VM");
448# pragma GCC diagnostic pop
449#endif
450
451 try
452 {
453 uint8_t inst = 0;
454 uint8_t padding = 0;
455 uint16_t arg = 0;
456 uint16_t primary_arg = 0;
457 uint16_t secondary_arg = 0;
458
459 m_running = true;
460
461 DISPATCH();
462 // cppcheck-suppress unreachableCode ; analysis cannot follow the chain of goto... but it works!
463 {
464#if !ARK_USE_COMPUTED_GOTOS
465 dispatch_opcode:
466 switch (inst)
467#endif
468 {
469#pragma region "Instructions"
470 TARGET(NOP)
471 {
472 DISPATCH();
473 }
474
476 {
477 push(loadSymbol(arg, context), context);
478 DISPATCH();
479 }
480
482 {
483 push(loadSymbolFromIndex(arg, context), context);
484 DISPATCH();
485 }
486
488 {
489 push(loadConstAsPtr(arg), context);
490 DISPATCH();
491 }
492
494 {
495 if (Value boolean = *popAndResolveAsPtr(context); !!boolean)
496 context.ip = arg * 4; // instructions are 4 bytes
497 DISPATCH();
498 }
499
501 {
502 store(arg, popAndResolveAsPtr(context), context);
503 DISPATCH();
504 }
505
507 {
508 setVal(arg, popAndResolveAsPtr(context), context);
509 DISPATCH();
510 }
511
513 {
514 if (Value boolean = *popAndResolveAsPtr(context); !boolean)
515 context.ip = arg * 4; // instructions are 4 bytes
516 DISPATCH();
517 }
518
519 TARGET(JUMP)
520 {
521 context.ip = arg * 4; // instructions are 4 bytes
522 DISPATCH();
523 }
524
525 TARGET(RET)
526 {
527 {
528 Value ip_or_val = *popAndResolveAsPtr(context);
529 // no return value on the stack
530 if (ip_or_val.valueType() == ValueType::InstPtr) [[unlikely]]
531 {
532 context.ip = ip_or_val.pageAddr();
533 // we always push PP then IP, thus the next value
534 // MUST be the page pointer
535 context.pp = pop(context)->pageAddr();
536
537 returnFromFuncCall(context);
538 push(Builtins::nil, context);
539 }
540 // value on the stack
541 else [[likely]]
542 {
543 const Value* ip = popAndResolveAsPtr(context);
544 assert(ip->valueType() == ValueType::InstPtr && "Expected instruction pointer on the stack (is the stack trashed?)");
545 context.ip = ip->pageAddr();
546 context.pp = pop(context)->pageAddr();
547
548 returnFromFuncCall(context);
549 push(std::move(ip_or_val), context);
550 }
551
552 if (context.fc <= untilFrameCount)
553 GOTO_HALT();
554 }
555
556 DISPATCH();
557 }
558
559 TARGET(HALT)
560 {
561 m_running = false;
562 GOTO_HALT();
563 }
564
565 TARGET(CALL)
566 {
567 // stack pointer + 2 because we push IP and PP
568 if (context.sp + 2u >= VMStackSize) [[unlikely]]
570 ErrorKind::VM,
571 fmt::format(
572 "Maximum recursion depth exceeded. You could consider rewriting your function `{}' to make use of tail-call optimization.",
573 m_state.m_symbols[context.last_symbol]));
574 call(context, arg);
575 if (!m_running)
576 GOTO_HALT();
577 DISPATCH();
578 }
579
581 {
582 if (!context.saved_scope)
583 context.saved_scope = ClosureScope();
584
585 const Value* ptr = findNearestVariable(arg, context);
586 if (!ptr)
587 throwVMError(ErrorKind::Scope, fmt::format("Couldn't capture `{}' as it is currently unbound", m_state.m_symbols[arg]));
588 else
589 {
590 ptr = ptr->valueType() == ValueType::Reference ? ptr->reference() : ptr;
591 context.saved_scope.value().push_back(arg, *ptr);
592 }
593
594 DISPATCH();
595 }
596
598 {
599 push(Builtins::builtins[arg].second, context);
600 DISPATCH();
601 }
602
603 TARGET(DEL)
604 {
605 if (Value* var = findNearestVariable(arg, context); var != nullptr)
606 {
607 if (var->valueType() == ValueType::User)
608 var->usertypeRef().del();
609 *var = Value();
610 DISPATCH();
611 }
612
613 throwVMError(ErrorKind::Scope, fmt::format("Can not delete unbound variable `{}'", m_state.m_symbols[arg]));
614 }
615
617 {
618 push(Value(Closure(context.saved_scope.value(), m_state.m_constants[arg].pageAddr())), context);
619 context.saved_scope.reset();
620 DISPATCH();
621 }
622
624 {
625 Value* var = popAndResolveAsPtr(context);
626 if (var->valueType() != ValueType::Closure)
627 {
628 if (context.last_symbol < m_state.m_symbols.size()) [[likely]]
630 ErrorKind::Type,
631 fmt::format(
632 "`{}' is a {}, not a Closure, can not get the field `{}' from it",
634 types_to_str[static_cast<std::size_t>(var->valueType())],
635 m_state.m_symbols[arg]));
636 else
637 throwVMError(ErrorKind::Type,
638 fmt::format(
639 "{} is not a Closure, can not get the field `{}' from it",
640 types_to_str[static_cast<std::size_t>(var->valueType())],
641 m_state.m_symbols[arg]));
642 }
643
644 if (Value* field = var->refClosure().refScope()[arg]; field != nullptr)
645 {
646 // check for CALL instruction (the instruction because context.ip is already on the next instruction word)
647 if (m_state.m_pages[context.pp][context.ip] == CALL)
648 push(Value(Closure(var->refClosure().scopePtr(), field->pageAddr())), context);
649 else
650 push(field, context);
651 }
652 else
653 {
654 if (!var->refClosure().hasFieldEndingWith(m_state.m_symbols[arg], *this))
656 ErrorKind::Scope,
657 fmt::format(
658 "`{0}' isn't in the closure environment: {1}",
659 m_state.m_symbols[arg],
660 var->refClosure().toString(*this)));
662 ErrorKind::Scope,
663 fmt::format(
664 "`{0}' isn't in the closure environment: {1}. A variable in the package might have the same name as '{0}', "
665 "and name resolution tried to fully qualify it. Rename either the variable or the capture to solve this",
666 m_state.m_symbols[arg],
667 var->refClosure().toString(*this)));
668 }
669 DISPATCH();
670 }
671
673 {
674 loadPlugin(arg, context);
675 DISPATCH();
676 }
677
678 TARGET(LIST)
679 {
680 {
682 if (arg != 0)
683 l.list().reserve(arg);
684
685 for (uint16_t i = 0; i < arg; ++i)
686 l.push_back(*popAndResolveAsPtr(context));
687 push(std::move(l), context);
688 }
689 DISPATCH();
690 }
691
693 {
694 {
695 Value* list = popAndResolveAsPtr(context);
696 if (list->valueType() != ValueType::List)
697 {
698 std::vector<Value> args = { *list };
699 for (uint16_t i = 0; i < arg; ++i)
700 args.push_back(*popAndResolveAsPtr(context));
702 "append",
703 { { types::Contract { { types::Typedef("list", ValueType::List), types::Typedef("value", ValueType::Any, /* variadic= */ true) } } } },
704 args);
705 }
706
707 const auto size = static_cast<uint16_t>(list->constList().size());
708
709 Value obj { *list };
710 obj.list().reserve(size + arg);
711
712 for (uint16_t i = 0; i < arg; ++i)
713 obj.push_back(*popAndResolveAsPtr(context));
714 push(std::move(obj), context);
715 }
716 DISPATCH();
717 }
718
720 {
721 {
722 Value* list = popAndResolveAsPtr(context);
723 Value obj { *list };
724
725 for (uint16_t i = 0; i < arg; ++i)
726 {
727 Value* next = popAndResolveAsPtr(context);
728
729 if (list->valueType() != ValueType::List || next->valueType() != ValueType::List)
731 "concat",
733 { *list, *next });
734
735 std::ranges::copy(next->list(), std::back_inserter(obj.list()));
736 }
737 push(std::move(obj), context);
738 }
739 DISPATCH();
740 }
741
743 {
744 Value* list = popAndResolveAsPtr(context);
745
746 if (list->valueType() != ValueType::List)
747 {
748 std::vector<Value> args = { *list };
749 for (uint16_t i = 0; i < arg; ++i)
750 args.push_back(*popAndResolveAsPtr(context));
752 "append!",
753 { { types::Contract { { types::Typedef("list", ValueType::List), types::Typedef("value", ValueType::Any, /* variadic= */ true) } } } },
754 args);
755 }
756
757 for (uint16_t i = 0; i < arg; ++i)
758 list->push_back(*popAndResolveAsPtr(context));
759 DISPATCH();
760 }
761
763 {
764 Value* list = popAndResolveAsPtr(context);
765
766 for (uint16_t i = 0; i < arg; ++i)
767 {
768 Value* next = popAndResolveAsPtr(context);
769
770 if (list->valueType() != ValueType::List || next->valueType() != ValueType::List)
772 "concat!",
774 { *list, *next });
775
776 std::ranges::copy(next->list(), std::back_inserter(list->list()));
777 }
778 DISPATCH();
779 }
780
782 {
783 {
784 Value list = *popAndResolveAsPtr(context);
785 Value number = *popAndResolveAsPtr(context);
786
787 if (list.valueType() != ValueType::List || number.valueType() != ValueType::Number)
789 "pop",
791 { list, number });
792
793 long idx = static_cast<long>(number.number());
794 idx = idx < 0 ? static_cast<long>(list.list().size()) + idx : idx;
795 if (std::cmp_greater_equal(idx, list.list().size()))
797 ErrorKind::Index,
798 fmt::format("pop index ({}) out of range (list size: {})", idx, list.list().size()));
799
800 list.list().erase(list.list().begin() + idx);
801 push(list, context);
802 }
803 DISPATCH();
804 }
805
807 {
808 {
809 Value* list = popAndResolveAsPtr(context);
810 Value number = *popAndResolveAsPtr(context);
811
812 if (list->valueType() != ValueType::List || number.valueType() != ValueType::Number)
814 "pop!",
816 { *list, number });
817
818 long idx = static_cast<long>(number.number());
819 idx = idx < 0 ? static_cast<long>(list->list().size()) + idx : idx;
820 if (std::cmp_greater_equal(idx, list->list().size()))
822 ErrorKind::Index,
823 fmt::format("pop! index ({}) out of range (list size: {})", idx, list->list().size()));
824
825 list->list().erase(list->list().begin() + idx);
826 }
827 DISPATCH();
828 }
829
831 {
832 {
833 Value* list = popAndResolveAsPtr(context);
834 Value number = *popAndResolveAsPtr(context);
835 Value new_value = *popAndResolveAsPtr(context);
836
837 if (!list->isIndexable() || number.valueType() != ValueType::Number || (list->valueType() == ValueType::String && new_value.valueType() != ValueType::String))
839 "@=",
840 { { types::Contract {
843 types::Typedef("new_value", ValueType::Any) } } },
847 types::Typedef("char", ValueType::String) } } } },
848 { *list, number, new_value });
849
850 const std::size_t size = list->valueType() == ValueType::List ? list->list().size() : list->stringRef().size();
851 long idx = static_cast<long>(number.number());
852 idx = idx < 0 ? static_cast<long>(size) + idx : idx;
853 if (std::cmp_greater_equal(idx, size))
855 ErrorKind::Index,
856 fmt::format("@= index ({}) out of range (indexable size: {})", idx, size));
857
858 if (list->valueType() == ValueType::List)
859 list->list()[static_cast<std::size_t>(idx)] = new_value;
860 else
861 list->stringRef()[static_cast<std::size_t>(idx)] = new_value.string()[0];
862 }
863 DISPATCH();
864 }
865
867 {
868 {
869 Value* list = popAndResolveAsPtr(context);
870 Value x = *popAndResolveAsPtr(context);
871 Value y = *popAndResolveAsPtr(context);
872 Value new_value = *popAndResolveAsPtr(context);
873
876 "@@=",
877 { { types::Contract {
881 types::Typedef("new_value", ValueType::Any) } } } },
882 { *list, x, y, new_value });
883
884 long idx_y = static_cast<long>(x.number());
885 idx_y = idx_y < 0 ? static_cast<long>(list->list().size()) + idx_y : idx_y;
886 if (std::cmp_greater_equal(idx_y, list->list().size()))
888 ErrorKind::Index,
889 fmt::format("@@= index (y: {}) out of range (list size: {})", idx_y, list->list().size()));
890
891 if (!list->list()[static_cast<std::size_t>(idx_y)].isIndexable() ||
892 (list->list()[static_cast<std::size_t>(idx_y)].valueType() == ValueType::String && new_value.valueType() != ValueType::String))
894 "@@=",
895 { { types::Contract {
899 types::Typedef("new_value", ValueType::Any) } } },
904 types::Typedef("char", ValueType::String) } } } },
905 { *list, x, y, new_value });
906
907 const bool is_list = list->list()[static_cast<std::size_t>(idx_y)].valueType() == ValueType::List;
908 const std::size_t size =
909 is_list
910 ? list->list()[static_cast<std::size_t>(idx_y)].list().size()
911 : list->list()[static_cast<std::size_t>(idx_y)].stringRef().size();
912
913 long idx_x = static_cast<long>(y.number());
914 idx_x = idx_x < 0 ? static_cast<long>(size) + idx_x : idx_x;
915 if (std::cmp_greater_equal(idx_x, size))
917 ErrorKind::Index,
918 fmt::format("@@= index (x: {}) out of range (inner indexable size: {})", idx_x, size));
919
920 if (is_list)
921 list->list()[static_cast<std::size_t>(idx_y)].list()[static_cast<std::size_t>(idx_x)] = new_value;
922 else
923 list->list()[static_cast<std::size_t>(idx_y)].stringRef()[static_cast<std::size_t>(idx_x)] = new_value.string()[0];
924 }
925 DISPATCH();
926 }
927
928 TARGET(POP)
929 {
930 pop(context);
931 DISPATCH();
932 }
933
934 TARGET(DUP)
935 {
936 context.stack[context.sp] = context.stack[context.sp - 1];
937 ++context.sp;
938 DISPATCH();
939 }
940
942 {
943 context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd());
944 DISPATCH();
945 }
946
948 {
949 context.locals.back().reset();
950 DISPATCH();
951 }
952
954 {
955 context.locals.pop_back();
956 DISPATCH();
957 }
958
959#pragma endregion
960
961#pragma region "Operators"
962
963 TARGET(ADD)
964 {
965 Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
966
967 if (a->valueType() == ValueType::Number && b->valueType() == ValueType::Number)
968 push(Value(a->number() + b->number()), context);
969 else if (a->valueType() == ValueType::String && b->valueType() == ValueType::String)
970 push(Value(a->string() + b->string()), context);
971 else
973 "+",
976 { *a, *b });
977 DISPATCH();
978 }
979
980 TARGET(SUB)
981 {
982 Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
983
984 if (a->valueType() != ValueType::Number || b->valueType() != ValueType::Number)
986 "-",
988 { *a, *b });
989 push(Value(a->number() - b->number()), context);
990 DISPATCH();
991 }
992
993 TARGET(MUL)
994 {
995 Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
996
997 if (a->valueType() != ValueType::Number || b->valueType() != ValueType::Number)
999 "*",
1001 { *a, *b });
1002 push(Value(a->number() * b->number()), context);
1003 DISPATCH();
1004 }
1005
1006 TARGET(DIV)
1007 {
1008 Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1009
1010 if (a->valueType() != ValueType::Number || b->valueType() != ValueType::Number)
1012 "/",
1014 { *a, *b });
1015 auto d = b->number();
1016 if (d == 0)
1017 throwVMError(ErrorKind::DivisionByZero, fmt::format("Can not compute expression (/ {} {})", a->toString(*this), b->toString(*this)));
1018
1019 push(Value(a->number() / d), context);
1020 DISPATCH();
1021 }
1022
1023 TARGET(GT)
1024 {
1025 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1026 push((*a != *b && !(*a < *b)) ? Builtins::trueSym : Builtins::falseSym, context);
1027 DISPATCH();
1028 }
1029
1030 TARGET(LT)
1031 {
1032 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1033 push((*a < *b) ? Builtins::trueSym : Builtins::falseSym, context);
1034 DISPATCH();
1035 }
1036
1037 TARGET(LE)
1038 {
1039 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1040 push((((*a < *b) || (*a == *b)) ? Builtins::trueSym : Builtins::falseSym), context);
1041 DISPATCH();
1042 }
1043
1044 TARGET(GE)
1045 {
1046 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1047 push(!(*a < *b) ? Builtins::trueSym : Builtins::falseSym, context);
1048 DISPATCH();
1049 }
1050
1051 TARGET(NEQ)
1052 {
1053 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1054 push((*a != *b) ? Builtins::trueSym : Builtins::falseSym, context);
1055 DISPATCH();
1056 }
1057
1058 TARGET(EQ)
1059 {
1060 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1061 push((*a == *b) ? Builtins::trueSym : Builtins::falseSym, context);
1062 DISPATCH();
1063 }
1064
1065 TARGET(LEN)
1066 {
1067 const Value* a = popAndResolveAsPtr(context);
1068
1069 if (a->valueType() == ValueType::List)
1070 push(Value(static_cast<int>(a->constList().size())), context);
1071 else if (a->valueType() == ValueType::String)
1072 push(Value(static_cast<int>(a->string().size())), context);
1073 else
1075 "len",
1076 { { types::Contract { { types::Typedef("value", ValueType::List) } },
1077 types::Contract { { types::Typedef("value", ValueType::String) } } } },
1078 { *a });
1079 DISPATCH();
1080 }
1081
1082 TARGET(EMPTY)
1083 {
1084 const Value* a = popAndResolveAsPtr(context);
1085
1086 if (a->valueType() == ValueType::List)
1087 push(a->constList().empty() ? Builtins::trueSym : Builtins::falseSym, context);
1088 else if (a->valueType() == ValueType::String)
1089 push(a->string().empty() ? Builtins::trueSym : Builtins::falseSym, context);
1090 else
1092 "empty?",
1093 { { types::Contract { { types::Typedef("value", ValueType::List) } },
1094 types::Contract { { types::Typedef("value", ValueType::String) } } } },
1095 { *a });
1096 DISPATCH();
1097 }
1098
1099 TARGET(TAIL)
1100 {
1101 Value* const a = popAndResolveAsPtr(context);
1102 push(helper::tail(a), context);
1103 DISPATCH();
1104 }
1105
1106 TARGET(HEAD)
1107 {
1108 Value* const a = popAndResolveAsPtr(context);
1109 push(helper::head(a), context);
1110 DISPATCH();
1111 }
1112
1113 TARGET(ISNIL)
1114 {
1115 const Value* a = popAndResolveAsPtr(context);
1117 DISPATCH();
1118 }
1119
1120 TARGET(ASSERT)
1121 {
1122 Value* const b = popAndResolveAsPtr(context);
1123 Value* const a = popAndResolveAsPtr(context);
1124
1125 if (b->valueType() != ValueType::String)
1127 "assert",
1129 { *a, *b });
1130
1131 if (*a == Builtins::falseSym)
1132 throw AssertionFailed(b->stringRef());
1133 DISPATCH();
1134 }
1135
1136 TARGET(TO_NUM)
1137 {
1138 const Value* a = popAndResolveAsPtr(context);
1139
1140 if (a->valueType() != ValueType::String)
1142 "toNumber",
1143 { { types::Contract { { types::Typedef("value", ValueType::String) } } } },
1144 { *a });
1145
1146 double val;
1147 if (Utils::isDouble(a->string(), &val))
1148 push(Value(val), context);
1149 else
1150 push(Builtins::nil, context);
1151 DISPATCH();
1152 }
1153
1154 TARGET(TO_STR)
1155 {
1156 const Value* a = popAndResolveAsPtr(context);
1157 push(Value(a->toString(*this)), context);
1158 DISPATCH();
1159 }
1160
1161 TARGET(AT)
1162 {
1163 {
1164 const Value* b = popAndResolveAsPtr(context);
1165 Value& a = *popAndResolveAsPtr(context);
1166
1167 if (b->valueType() != ValueType::Number)
1169 "@",
1172 { a, *b });
1173
1174 long idx = static_cast<long>(b->number());
1175
1176 if (a.valueType() == ValueType::List)
1177 {
1178 if (std::cmp_less(std::abs(idx), a.list().size()))
1179 push(a.list()[static_cast<std::size_t>(idx < 0 ? static_cast<long>(a.list().size()) + idx : idx)], context);
1180 else
1182 ErrorKind::Index,
1183 fmt::format("{} out of range {} (length {})", idx, a.toString(*this), a.list().size()));
1184 }
1185 else if (a.valueType() == ValueType::String)
1186 {
1187 if (std::cmp_less(std::abs(idx), a.string().size()))
1188 push(Value(std::string(1, a.string()[static_cast<std::size_t>(idx < 0 ? static_cast<long>(a.string().size()) + idx : idx)])), context);
1189 else
1191 ErrorKind::Index,
1192 fmt::format("{} out of range \"{}\" (length {})", idx, a.string(), a.string().size()));
1193 }
1194 else
1196 "@",
1199 { a, *b });
1200 }
1201 DISPATCH();
1202 }
1203
1204 TARGET(AT_AT)
1205 {
1206 {
1207 const Value* x = popAndResolveAsPtr(context);
1208 const Value* y = popAndResolveAsPtr(context);
1209 Value& list = *popAndResolveAsPtr(context);
1210
1211 if (y->valueType() != ValueType::Number || x->valueType() != ValueType::Number ||
1212 list.valueType() != ValueType::List)
1214 "@@",
1215 { { types::Contract {
1218 types::Typedef("x", ValueType::Number) } } } },
1219 { list, *y, *x });
1220
1221 long idx_y = static_cast<long>(y->number());
1222 idx_y = idx_y < 0 ? static_cast<long>(list.list().size()) + idx_y : idx_y;
1223 if (std::cmp_greater_equal(idx_y, list.list().size()))
1225 ErrorKind::Index,
1226 fmt::format("@@ index ({}) out of range (list size: {})", idx_y, list.list().size()));
1227
1228 const bool is_list = list.list()[static_cast<std::size_t>(idx_y)].valueType() == ValueType::List;
1229 const std::size_t size =
1230 is_list
1231 ? list.list()[static_cast<std::size_t>(idx_y)].list().size()
1232 : list.list()[static_cast<std::size_t>(idx_y)].stringRef().size();
1233
1234 long idx_x = static_cast<long>(x->number());
1235 idx_x = idx_x < 0 ? static_cast<long>(size) + idx_x : idx_x;
1236 if (std::cmp_greater_equal(idx_x, size))
1238 ErrorKind::Index,
1239 fmt::format("@@ index (x: {}) out of range (inner indexable size: {})", idx_x, size));
1240
1241 if (is_list)
1242 push(list.list()[static_cast<std::size_t>(idx_y)].list()[static_cast<std::size_t>(idx_x)], context);
1243 else
1244 push(Value(std::string(1, list.list()[static_cast<std::size_t>(idx_y)].stringRef()[static_cast<std::size_t>(idx_x)])), context);
1245 }
1246 DISPATCH();
1247 }
1248
1249 TARGET(MOD)
1250 {
1251 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1252 if (a->valueType() != ValueType::Number || b->valueType() != ValueType::Number)
1254 "mod",
1256 { *a, *b });
1257 push(Value(std::fmod(a->number(), b->number())), context);
1258 DISPATCH();
1259 }
1260
1261 TARGET(TYPE)
1262 {
1263 const Value* a = popAndResolveAsPtr(context);
1264 if (a == &m_undefined_value) [[unlikely]]
1266 "type",
1267 { { types::Contract { { types::Typedef("value", ValueType::Any) } } } },
1268 {});
1269
1270 push(Value(types_to_str[static_cast<unsigned>(a->valueType())]), context);
1271 DISPATCH();
1272 }
1273
1275 {
1276 {
1277 Value* const field = popAndResolveAsPtr(context);
1278 Value* const closure = popAndResolveAsPtr(context);
1279 if (closure->valueType() != ValueType::Closure || field->valueType() != ValueType::String)
1281 "hasField",
1283 { *closure, *field });
1284
1285 auto it = std::ranges::find(m_state.m_symbols, field->stringRef());
1286 if (it == m_state.m_symbols.end())
1287 {
1288 push(Builtins::falseSym, context);
1289 DISPATCH();
1290 }
1291
1292 auto id = static_cast<std::uint16_t>(std::distance(m_state.m_symbols.begin(), it));
1293 push(closure->refClosure().refScope()[id] != nullptr ? Builtins::trueSym : Builtins::falseSym, context);
1294 }
1295 DISPATCH();
1296 }
1297
1298 TARGET(NOT)
1299 {
1300 const Value* a = popAndResolveAsPtr(context);
1301 push(!(*a) ? Builtins::trueSym : Builtins::falseSym, context);
1302 DISPATCH();
1303 }
1304
1305#pragma endregion
1306
1307#pragma region "Super Instructions"
1309 {
1310 UNPACK_ARGS();
1311 push(loadConstAsPtr(primary_arg), context);
1312 push(loadConstAsPtr(secondary_arg), context);
1313 DISPATCH();
1314 }
1315
1317 {
1318 UNPACK_ARGS();
1319 store(secondary_arg, loadConstAsPtr(primary_arg), context);
1320 DISPATCH();
1321 }
1322
1324 {
1325 UNPACK_ARGS();
1326 setVal(secondary_arg, loadConstAsPtr(primary_arg), context);
1327 DISPATCH();
1328 }
1329
1331 {
1332 UNPACK_ARGS();
1333 store(secondary_arg, loadSymbol(primary_arg, context), context);
1334 DISPATCH();
1335 }
1336
1338 {
1339 UNPACK_ARGS();
1340 store(secondary_arg, loadSymbolFromIndex(primary_arg, context), context);
1341 DISPATCH();
1342 }
1343
1345 {
1346 UNPACK_ARGS();
1347 setVal(secondary_arg, loadSymbol(primary_arg, context), context);
1348 DISPATCH();
1349 }
1350
1352 {
1353 UNPACK_ARGS();
1354 setVal(secondary_arg, loadSymbolFromIndex(primary_arg, context), context);
1355 DISPATCH();
1356 }
1357
1359 {
1360 UNPACK_ARGS();
1361 {
1362 Value* var = loadSymbol(primary_arg, context);
1363
1364 // use internal reference, shouldn't break anything so far, unless it's already a ref
1365 if (var->valueType() == ValueType::Reference)
1366 var = var->reference();
1367
1368 if (var->valueType() == ValueType::Number)
1369 push(Value(var->number() + secondary_arg), context);
1370 else
1372 "+",
1374 { *var, Value(secondary_arg) });
1375 }
1376 DISPATCH();
1377 }
1378
1380 {
1381 UNPACK_ARGS();
1382 {
1383 Value* var = loadSymbolFromIndex(primary_arg, context);
1384
1385 // use internal reference, shouldn't break anything so far, unless it's already a ref
1386 if (var->valueType() == ValueType::Reference)
1387 var = var->reference();
1388
1389 if (var->valueType() == ValueType::Number)
1390 push(Value(var->number() + secondary_arg), context);
1391 else
1393 "+",
1395 { *var, Value(secondary_arg) });
1396 }
1397 DISPATCH();
1398 }
1399
1401 {
1402 UNPACK_ARGS();
1403 {
1404 Value* var = loadSymbol(primary_arg, context);
1405
1406 // use internal reference, shouldn't break anything so far, unless it's already a ref
1407 if (var->valueType() == ValueType::Reference)
1408 var = var->reference();
1409
1410 if (var->valueType() == ValueType::Number)
1411 push(Value(var->number() - secondary_arg), context);
1412 else
1414 "-",
1416 { *var, Value(secondary_arg) });
1417 }
1418 DISPATCH();
1419 }
1420
1422 {
1423 UNPACK_ARGS();
1424 {
1425 Value* var = loadSymbolFromIndex(primary_arg, context);
1426
1427 // use internal reference, shouldn't break anything so far, unless it's already a ref
1428 if (var->valueType() == ValueType::Reference)
1429 var = var->reference();
1430
1431 if (var->valueType() == ValueType::Number)
1432 push(Value(var->number() - secondary_arg), context);
1433 else
1435 "-",
1437 { *var, Value(secondary_arg) });
1438 }
1439 DISPATCH();
1440 }
1441
1443 {
1444 UNPACK_ARGS();
1445 {
1446 Value* list = loadSymbol(primary_arg, context);
1447 Value tail = helper::tail(list);
1448 store(secondary_arg, &tail, context);
1449 }
1450 DISPATCH();
1451 }
1452
1454 {
1455 UNPACK_ARGS();
1456 {
1457 Value* list = loadSymbolFromIndex(primary_arg, context);
1458 Value tail = helper::tail(list);
1459 store(secondary_arg, &tail, context);
1460 }
1461 DISPATCH();
1462 }
1463
1465 {
1466 UNPACK_ARGS();
1467 {
1468 Value* list = loadSymbol(primary_arg, context);
1469 Value head = helper::head(list);
1470 store(secondary_arg, &head, context);
1471 }
1472 DISPATCH();
1473 }
1474
1476 {
1477 UNPACK_ARGS();
1478 {
1479 Value* list = loadSymbolFromIndex(primary_arg, context);
1480 Value head = helper::head(list);
1481 store(secondary_arg, &head, context);
1482 }
1483 DISPATCH();
1484 }
1485
1487 {
1488 UNPACK_ARGS();
1489 {
1490 Value* list = loadSymbol(primary_arg, context);
1491 Value tail = helper::tail(list);
1492 setVal(secondary_arg, &tail, context);
1493 }
1494 DISPATCH();
1495 }
1496
1498 {
1499 UNPACK_ARGS();
1500 {
1501 Value* list = loadSymbolFromIndex(primary_arg, context);
1502 Value tail = helper::tail(list);
1503 setVal(secondary_arg, &tail, context);
1504 }
1505 DISPATCH();
1506 }
1507
1509 {
1510 UNPACK_ARGS();
1511 {
1512 Value* list = loadSymbol(primary_arg, context);
1513 Value head = helper::head(list);
1514 setVal(secondary_arg, &head, context);
1515 }
1516 DISPATCH();
1517 }
1518
1520 {
1521 UNPACK_ARGS();
1522 {
1523 Value* list = loadSymbolFromIndex(primary_arg, context);
1524 Value head = helper::head(list);
1525 setVal(secondary_arg, &head, context);
1526 }
1527 DISPATCH();
1528 }
1529
1531 {
1532 UNPACK_ARGS();
1533 // no stack size check because we do not push IP/PP since we are just calling a builtin
1534 callBuiltin(context, Builtins::builtins[primary_arg].second, secondary_arg);
1535 if (!m_running)
1536 GOTO_HALT();
1537 DISPATCH();
1538 }
1539#pragma endregion
1540 }
1541#if ARK_USE_COMPUTED_GOTOS
1542 dispatch_end:
1543 do
1544 {
1545 } while (false);
1546#endif
1547 }
1548 }
1549 catch (const Error& e)
1550 {
1551 if (fail_with_exception)
1552 {
1553 std::stringstream stream;
1554 backtrace(context, stream, /* colorize= */ false);
1555 // It's important we have an Ark::Error here, as the constructor for NestedError
1556 // does more than just aggregate error messages, hence the code duplication.
1557 throw NestedError(e, stream.str());
1558 }
1559 else
1560 showBacktraceWithException(Error(e.details(/* colorize= */ true)), context);
1561 }
1562 catch (const std::exception& e)
1563 {
1564 if (fail_with_exception)
1565 {
1566 std::stringstream stream;
1567 backtrace(context, stream, /* colorize= */ false);
1568 throw NestedError(e, stream.str());
1569 }
1570 else
1571 showBacktraceWithException(e, context);
1572 }
1573 catch (...)
1574 {
1575 if (fail_with_exception)
1576 throw;
1577
1578#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
1579 throw;
1580#endif
1581 fmt::println("Unknown error");
1582 backtrace(context);
1583 m_exit_code = 1;
1584 }
1585
1586 return m_exit_code;
1587 }
1588
1589 uint16_t VM::findNearestVariableIdWithValue(const Value& value, ExecutionContext& context) const noexcept
1590 {
1591 for (auto& local : std::ranges::reverse_view(context.locals))
1592 {
1593 if (const auto id = local.idFromValue(value); id < m_state.m_symbols.size())
1594 return id;
1595 }
1596 return std::numeric_limits<uint16_t>::max();
1597 }
1598
1599 void VM::throwVMError(ErrorKind kind, const std::string& message)
1600 {
1601 throw std::runtime_error(std::string(errorKinds[static_cast<std::size_t>(kind)]) + ": " + message + "\n");
1602 }
1603
1604 void VM::throwArityError(std::size_t passed_arg_count, std::size_t expected_arg_count, internal::ExecutionContext& context)
1605 {
1606 std::vector<std::string> arg_names;
1607 arg_names.reserve(expected_arg_count + 1);
1608 if (expected_arg_count > 0)
1609 arg_names.emplace_back(""); // for formatting, so that we have a space between the function and the args
1610
1611 std::size_t index = 0;
1612 while (m_state.m_pages[context.pp][index] == STORE)
1613 {
1614 const auto id = static_cast<uint16_t>((m_state.m_pages[context.pp][index + 2] << 8) + m_state.m_pages[context.pp][index + 3]);
1615 arg_names.push_back(m_state.m_symbols[id]);
1616 index += 4;
1617 }
1618
1619 std::vector<std::string> arg_vals;
1620 arg_vals.reserve(passed_arg_count + 1);
1621 if (passed_arg_count > 0)
1622 arg_vals.emplace_back(""); // for formatting, so that we have a space between the function and the args
1623
1624 for (std::size_t i = 0; i < passed_arg_count && i + 1 <= context.sp; ++i)
1625 // -1 on the stack because we always point to the next available slot
1626 arg_vals.push_back(context.stack[context.sp - i - 1].toString(*this));
1627
1628 // set ip/pp to the callee location so that the error can pin-point the line
1629 // where the bad call happened
1630 if (context.sp >= 2 + passed_arg_count)
1631 {
1632 context.ip = context.stack[context.sp - 1 - passed_arg_count].pageAddr();
1633 context.pp = context.stack[context.sp - 2 - passed_arg_count].pageAddr();
1634 returnFromFuncCall(context);
1635 }
1636
1637 std::string function_name = (context.last_symbol < m_state.m_symbols.size())
1638 ? m_state.m_symbols[context.last_symbol]
1639 : Value(static_cast<PageAddr_t>(context.pp)).toString(*this);
1640
1642 ErrorKind::Arity,
1643 fmt::format(
1644 "When calling `({}{})', received {} argument{}, but expected {}: `({}{})'",
1645 function_name,
1646 fmt::join(arg_vals, " "),
1647 passed_arg_count,
1648 passed_arg_count > 1 ? "s" : "",
1649 expected_arg_count,
1650 function_name,
1651 fmt::join(arg_names, " ")));
1652 }
1653
1654 void VM::showBacktraceWithException(const std::exception& e, internal::ExecutionContext& context)
1655 {
1656 std::string text = e.what();
1657 if (!text.empty() && text.back() != '\n')
1658 text += '\n';
1659 fmt::println("{}", text);
1660 backtrace(context);
1661#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
1662 // don't report a "failed" exit code so that the fuzzers can more accurately triage crashes
1663 m_exit_code = 0;
1664#else
1665 m_exit_code = 1;
1666#endif
1667 }
1668
1669 std::optional<InstLoc> VM::findSourceLocation(const std::size_t ip, const std::size_t pp)
1670 {
1671 std::optional<InstLoc> match = std::nullopt;
1672
1673 for (const auto location : m_state.m_inst_locations)
1674 {
1675 if (location.page_pointer == pp && !match)
1676 match = location;
1677
1678 // select the best match: we want to find the location that's nearest our instruction pointer,
1679 // but not equal to it as the IP will always be pointing to the next instruction,
1680 // not yet executed. Thus, the erroneous instruction is the previous one.
1681 if (location.page_pointer == pp && match && location.inst_pointer < ip / 4)
1682 match = location;
1683
1684 // early exit because we won't find anything better, as inst locations are ordered by ascending (pp, ip)
1685 if (location.page_pointer > pp || (location.page_pointer == pp && location.inst_pointer >= ip / 4))
1686 break;
1687 }
1688
1689 return match;
1690 }
1691
1692 void VM::backtrace(ExecutionContext& context, std::ostream& os, const bool colorize)
1693 {
1694 const std::size_t saved_ip = context.ip;
1695 const std::size_t saved_pp = context.pp;
1696 const uint16_t saved_sp = context.sp;
1697
1698 const auto maybe_location = findSourceLocation(context.ip, context.pp);
1699 if (maybe_location)
1700 {
1701 const auto filename = m_state.m_filenames[maybe_location->filename_id];
1702
1703 if (Utils::fileExists(filename))
1705 os,
1706 filename,
1707 /* expr= */ std::nullopt,
1708 /* sym_size= */ 0,
1709 maybe_location->line,
1710 /* col_start= */ 0,
1711 /* maybe_context= */ std::nullopt,
1712 /* whole_line= */ true,
1713 /* colorize= */ colorize);
1714 fmt::println(os, "");
1715 }
1716
1717 if (context.fc > 1)
1718 {
1719 // display call stack trace
1720 const ScopeView old_scope = context.locals.back();
1721
1722 std::string previous_trace;
1723 std::size_t displayed_traces = 0;
1724 std::size_t consecutive_similar_traces = 0;
1725
1726 while (context.fc != 0)
1727 {
1728 const auto maybe_call_loc = findSourceLocation(context.ip, context.pp);
1729 const auto loc_as_text = maybe_call_loc ? fmt::format(" ({}:{})", m_state.m_filenames[maybe_call_loc->filename_id], maybe_call_loc->line + 1) : "";
1730
1731 if (context.pp != 0)
1732 {
1733 const uint16_t id = findNearestVariableIdWithValue(
1734 Value(static_cast<PageAddr_t>(context.pp)),
1735 context);
1736 const auto func_name = (id < m_state.m_symbols.size()) ? m_state.m_symbols[id] : "???";
1737
1738 if (func_name + loc_as_text != previous_trace)
1739 {
1740 fmt::println(
1741 os,
1742 "[{:4}] In function `{}'{}",
1743 fmt::styled(context.fc, colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style()),
1744 fmt::styled(func_name, colorize ? fmt::fg(fmt::color::green) : fmt::text_style()),
1745 loc_as_text);
1746 previous_trace = func_name + loc_as_text;
1747 ++displayed_traces;
1748 consecutive_similar_traces = 0;
1749 }
1750 else if (consecutive_similar_traces == 0)
1751 {
1752 fmt::println(os, " ...");
1753 ++consecutive_similar_traces;
1754 }
1755
1756 const Value* ip;
1757 do
1758 {
1759 ip = popAndResolveAsPtr(context);
1760 } while (ip->valueType() != ValueType::InstPtr);
1761
1762 context.ip = ip->pageAddr();
1763 context.pp = pop(context)->pageAddr();
1764 returnFromFuncCall(context);
1765 }
1766 else
1767 {
1768 fmt::println(os, "[{:4}] In global scope{}", fmt::styled(context.fc, colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style()), loc_as_text);
1769 break;
1770 }
1771
1772 if (displayed_traces > 7)
1773 {
1774 fmt::println(os, "...");
1775 break;
1776 }
1777 }
1778
1779 // display variables values in the current scope
1780 fmt::println(os, "\nCurrent scope variables values:");
1781 for (std::size_t i = 0, size = old_scope.size(); i < size; ++i)
1782 {
1783 fmt::println(
1784 os,
1785 "{} = {}",
1786 fmt::styled(m_state.m_symbols[old_scope.atPos(i).first], colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style()),
1787 old_scope.atPos(i).second.toString(*this));
1788 }
1789 }
1790
1791 fmt::println(
1792 os,
1793 "At IP: {}, PP: {}, SP: {}",
1794 // dividing by 4 because the instructions are actually on 4 bytes
1795 fmt::styled(saved_ip / 4, colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style()),
1796 fmt::styled(saved_pp, colorize ? fmt::fg(fmt::color::green) : fmt::text_style()),
1797 fmt::styled(saved_sp, colorize ? fmt::fg(fmt::color::yellow) : fmt::text_style()));
1798 }
1799}
Lots of utilities about string, filesystem and more.
#define ARK_NO_NAME_FILE
Definition Constants.hpp:26
Lots of utilities about the filesystem.
The different instructions used by the compiler and virtual machine.
#define GOTO_HALT()
#define TARGET(op)
#define UNPACK_ARGS()
#define DISPATCH()
The ArkScript virtual machine.
An assertion error, only triggered from ArkScript code through (assert expr error-message)
virtual std::string details(bool colorize) const
Ark state to handle the dirty job of loading and compiling ArkScript code.
Definition State.hpp:32
std::vector< std::filesystem::path > m_libenv
Definition State.hpp:145
std::string m_filename
Definition State.hpp:146
std::vector< Value > m_constants
Definition State.hpp:150
std::vector< internal::InstLoc > m_inst_locations
Definition State.hpp:152
std::vector< std::string > m_filenames
Definition State.hpp:151
std::unordered_map< std::string, Value > m_binded
Definition State.hpp:156
std::vector< std::string > m_symbols
Definition State.hpp:149
std::vector< bytecode_t > m_pages
Definition State.hpp:153
The ArkScript virtual machine, executing ArkScript bytecode.
Definition VM.hpp:45
void deleteContext(internal::ExecutionContext *ec)
Free a given execution context.
Definition VM.cpp:249
void showBacktraceWithException(const std::exception &e, internal::ExecutionContext &context)
Definition VM.cpp:1654
std::vector< std::unique_ptr< internal::Future > > m_futures
Storing the promises while we are resolving them.
Definition VM.hpp:162
int m_exit_code
VM exit code, defaults to 0. Can be changed through sys:exit
Definition VM.hpp:158
Value & operator[](const std::string &name) noexcept
Retrieve a value from the virtual machine, given its symbol name.
Definition VM.cpp:119
Value * popAndResolveAsPtr(internal::ExecutionContext &context)
Pop a value from the stack and resolve it if possible, then return it.
Definition VM.hpp:181
std::optional< internal::InstLoc > findSourceLocation(std::size_t ip, std::size_t pp)
Find the nearest source location information given instruction and page pointers.
Definition VM.cpp:1669
std::vector< std::shared_ptr< internal::SharedLibrary > > m_shared_lib_objects
Definition VM.hpp:161
std::vector< std::unique_ptr< internal::ExecutionContext > > m_execution_contexts
Definition VM.hpp:157
Value * loadSymbol(uint16_t id, internal::ExecutionContext &context)
Load a symbol by its id in the current context. Performs a lookup in the scope stack,...
Definition VM.hpp:88
uint16_t findNearestVariableIdWithValue(const Value &value, internal::ExecutionContext &context) const noexcept
Find the nearest variable id with a given value.
Definition VM.cpp:1589
Value * loadSymbolFromIndex(uint16_t index, internal::ExecutionContext &context)
Load a symbol by its (reversed) index in the current scope.
Definition VM.hpp:103
bool forceReloadPlugins() const
Used by the REPL to force reload all the plugins and their bound methods.
Definition VM.cpp:292
void backtrace(internal::ExecutionContext &context, std::ostream &os=std::cout, bool colorize=true)
Display a backtrace when the VM encounter an exception.
Definition VM.cpp:1692
internal::ExecutionContext * createAndGetContext()
Create an execution context and returns it.
Definition VM.cpp:228
std::mutex m_mutex
Definition VM.hpp:160
void loadPlugin(uint16_t id, internal::ExecutionContext &context)
Load a plugin from a constant id.
Definition VM.cpp:144
int safeRun(internal::ExecutionContext &context, std::size_t untilFrameCount=0, bool fail_with_exception=false)
Run ArkScript bytecode inside a try catch to retrieve all the exceptions and display a stack trace if...
Definition VM.cpp:330
void deleteFuture(internal::Future *f)
Free a given future.
Definition VM.cpp:278
bool m_running
Definition VM.hpp:159
Value call(const std::string &name, Args &&... args)
Call a function from ArkScript, by giving it arguments.
Definition VM.hpp:6
Value * pop(internal::ExecutionContext &context)
Pop a value from the stack.
Definition VM.hpp:150
void returnFromFuncCall(internal::ExecutionContext &context)
Destroy the current frame and get back to the previous one, resuming execution.
Definition VM.hpp:256
Value m_undefined_value
Definition VM.hpp:166
void store(uint16_t id, const Value *val, internal::ExecutionContext &context)
Create a new symbol with an associated value in the current scope.
Definition VM.hpp:118
void setVal(uint16_t id, const Value *val, internal::ExecutionContext &context)
Change the value of a symbol given its identifier.
Definition VM.hpp:128
void push(const Value &value, internal::ExecutionContext &context)
Push a value on the stack.
Definition VM.hpp:160
friend class internal::Closure
Definition VM.hpp:152
State & m_state
Definition VM.hpp:156
Value * loadConstAsPtr(uint16_t id) const
Load a constant from the constant table by its id.
Definition VM.hpp:113
void callBuiltin(internal::ExecutionContext &context, const Value &builtin, uint16_t argc)
Builtin called when the CALL_BUILTIN instruction is met in the bytecode.
Definition VM.hpp:348
void init() noexcept
Initialize the VM according to the parameters.
Definition VM.cpp:86
Value * findNearestVariable(uint16_t id, internal::ExecutionContext &context) noexcept
Find the nearest variable of a given id.
Definition VM.hpp:246
void throwArityError(std::size_t passed_arg_count, std::size_t expected_arg_count, internal::ExecutionContext &context)
Definition VM.cpp:1604
static void throwVMError(internal::ErrorKind kind, const std::string &message)
Throw a VM error message.
Definition VM.cpp:1599
friend class Value
Definition VM.hpp:151
VM(State &state) noexcept
Construct a new vm t object.
Definition VM.cpp:80
internal::Future * createFuture(std::vector< Value > &args)
Create a Future object from a function and its arguments and return a managed pointer to it.
Definition VM.cpp:263
int run(bool fail_with_exception=false)
Run the bytecode held in the state.
Definition VM.cpp:323
void exit(int code) noexcept
Ask the VM to exit with a given exit code.
Definition VM.cpp:222
const std::vector< Value > & constList() const
Definition Value.hpp:126
std::vector< Value > & list()
Definition Value.hpp:128
internal::Closure & refClosure()
Definition Value.hpp:165
Value * reference() const
Definition Value.hpp:131
double number() const
Definition Value.hpp:124
void push_back(const Value &value)
Add an element to the list held by the value (if the value type is set to list)
Definition Value.cpp:57
ValueType valueType() const noexcept
Definition Value.hpp:113
const std::string & string() const
Definition Value.hpp:125
bool isIndexable() const noexcept
Definition Value.hpp:119
std::string toString(VM &vm) const noexcept
Definition Value.cpp:67
internal::PageAddr_t pageAddr() const
Definition Value.hpp:162
std::string & stringRef()
Definition Value.hpp:129
A class to store fields captured by a closure.
std::string toString(VM &vm) const noexcept
Print the closure to a string.
Definition Closure.cpp:27
ClosureScope & refScope() const noexcept
Definition Closure.hpp:54
bool hasFieldEndingWith(const std::string &end, const VM &vm) const
Used when generating error messages in the VM, to see if a symbol might have been wrongly fully quali...
Definition Closure.cpp:20
const std::shared_ptr< ClosureScope > & scopePtr() const
Definition Closure.hpp:55
A class to handle the VM scope more efficiently.
Definition ScopeView.hpp:27
std::size_t size() const noexcept
Return the size of the scope.
const pair_t & atPos(const std::size_t i) const noexcept
Return the element at index in scope.
Definition ScopeView.hpp:98
ARK_API void makeContext(std::ostream &os, const std::string &filename, const std::optional< std::string > &expr, std::size_t sym_size, std::size_t target_line, std::size_t col_start, const std::optional< CodeErrorContext > &maybe_context, bool whole_line, bool colorize)
Helper to create a colorized context to report errors to the user.
bool fileExists(const std::string &name) noexcept
Checks if a file exists.
Definition Files.hpp:28
bool isDouble(const std::string &s, double *output=nullptr)
Checks if a string is a valid double.
Definition Utils.hpp:55
Value head(Value *a)
Definition VM.cpp:57
Value tail(Value *a)
Definition VM.cpp:28
ARK_API const std::vector< std::pair< std::string, Value > > builtins
constexpr std::array< std::string_view, 7 > errorKinds
Definition ErrorKind.hpp:20
uint16_t PageAddr_t
Definition Closure.hpp:27
constexpr std::array types_to_str
Definition Value.hpp:51
constexpr std::size_t VMStackSize
Definition Constants.hpp:68
@ Any
Used only for typechecking.
std::array< ScopeView::pair_t, ScopeStackSize > scopes_storage
All the ScopeView use this array to store id->value.
std::vector< std::shared_ptr< ClosureScope > > stacked_closure_scopes
Stack the closure scopes to keep the closure alive as long as we are calling them.
std::array< Value, VMStackSize > stack
std::vector< ScopeView > locals
std::size_t ip
Instruction pointer.
std::optional< ClosureScope > saved_scope
Scope created by CAPTURE <x> instructions, used by the MAKE_CLOSURE instruction.
const char * name
Definition Module.hpp:11
A contract is a list of typed arguments that a function can follow.
A type definition within a contract.
Definition VM.cpp:17
Ark::Value(* value)(std::vector< Ark::Value > &, Ark::VM *)
Definition VM.cpp:19
char * name
Definition VM.cpp:18