ArkScript
A small, lisp-inspired, functional scripting language
VM.cpp
Go to the documentation of this file.
1#include <Ark/VM/VM.hpp>
2
3#include <utility>
4#include <numeric>
5#include <fmt/core.h>
6#include <fmt/color.h>
7#include <fmt/ranges.h>
8#include <fmt/ostream.h>
9
10#include <Ark/Utils/Files.hpp>
11#include <Ark/Utils/Utils.hpp>
13#include <Ark/TypeChecker.hpp>
15#include <Ark/VM/Value/Dict.hpp>
16#include <Ark/VM/Helpers.hpp>
17
18namespace Ark
19{
20 using namespace internal;
21
22 VM::VM(State& state) noexcept :
23 m_state(state), m_exit_code(0), m_running(false)
24 {
25 m_execution_contexts.emplace_back(std::make_unique<ExecutionContext>());
26 }
27
28 void VM::init() noexcept
29 {
30 ExecutionContext& context = *m_execution_contexts.back();
31 for (const auto& c : m_execution_contexts)
32 {
33 c->ip = 0;
34 c->pp = 0;
35 c->sp = 0;
36 }
37
38 context.sp = 0;
39 context.fc = 1;
40
42 context.stacked_closure_scopes.clear();
43 context.stacked_closure_scopes.emplace_back(nullptr);
44
45 context.saved_scope.reset();
46 m_exit_code = 0;
47
48 context.locals.clear();
49 context.locals.reserve(64);
50 context.locals.emplace_back(context.scopes_storage.data(), 0);
51
52 // loading bound stuff
53 // put them in the global frame if we can, aka the first one
54 for (const auto& [sym_id, value] : m_state.m_bound)
55 {
56 auto it = std::ranges::find(m_state.m_symbols, sym_id);
57 if (it != m_state.m_symbols.end())
58 context.locals[0].pushBack(static_cast<uint16_t>(std::distance(m_state.m_symbols.begin(), it)), value);
59 }
60 }
61
62 Value VM::getField(Value* closure, const uint16_t id, const ExecutionContext& context, const bool push_with_env)
63 {
64 if (closure->valueType() != ValueType::Closure)
65 {
66 if (context.last_symbol < m_state.m_symbols.size()) [[likely]]
68 ErrorKind::Type,
69 fmt::format(
70 "`{}' is a {}, not a Closure, can not get the field `{}' from it",
72 std::to_string(closure->valueType()),
73 m_state.m_symbols[id]));
74 else
76 ErrorKind::Type,
77 fmt::format(
78 "{} is not a Closure, can not get the field `{}' from it",
79 std::to_string(closure->valueType()),
80 m_state.m_symbols[id]));
81 }
82
83 if (Value* field = closure->refClosure().refScope()[id]; field != nullptr)
84 {
85 if (push_with_env)
86 return Value(Closure(closure->refClosure().scopePtr(), field->pageAddr()));
87 else
88 return *field;
89 }
90 else
91 {
92 if (!closure->refClosure().hasFieldEndingWith(m_state.m_symbols[id], *this))
94 ErrorKind::Scope,
95 fmt::format(
96 "`{0}' isn't in the closure environment: {1}",
98 closure->refClosure().toString(*this)));
100 ErrorKind::Scope,
101 fmt::format(
102 "`{0}' isn't in the closure environment: {1}. A variable in the package might have the same name as '{0}', "
103 "and name resolution tried to fully qualify it. Rename either the variable or the capture to solve this",
104 m_state.m_symbols[id],
105 closure->refClosure().toString(*this)));
106 }
107 }
108
109 Value VM::createList(const std::size_t count, ExecutionContext& context)
110 {
112 if (count != 0)
113 l.list().reserve(count);
114
115 for (std::size_t i = 0; i < count; ++i)
116 l.push_back(*popAndResolveAsPtr(context));
117
118 return l;
119 }
120
121 void VM::listAppendInPlace(Value* list, const std::size_t count, ExecutionContext& context)
122 {
123 if (list->valueType() != ValueType::List)
124 {
125 std::vector<Value> args = { *list };
126 for (std::size_t i = 0; i < count; ++i)
127 args.push_back(*popAndResolveAsPtr(context));
129 "append!",
130 { { types::Contract { { types::Typedef("list", ValueType::List), types::Typedef("value", ValueType::Any, /* is_variadic= */ true) } } } },
131 args);
132 }
133
134 for (std::size_t i = 0; i < count; ++i)
135 list->push_back(*popAndResolveAsPtr(context));
136 }
137
138 Value& VM::operator[](const std::string& name) noexcept
139 {
140 // find id of object
141 const auto it = std::ranges::find(m_state.m_symbols, name);
142 if (it == m_state.m_symbols.end())
143 {
144 m_no_value = Builtins::nil;
145 return m_no_value;
146 }
147
148 const auto dist = std::distance(m_state.m_symbols.begin(), it);
149 if (std::cmp_less(dist, MaxValue16Bits))
150 {
151 ExecutionContext& context = *m_execution_contexts.front();
152
153 const auto id = static_cast<uint16_t>(dist);
154 Value* var = findNearestVariable(id, context);
155 if (var != nullptr)
156 return *var;
157 }
158
159 m_no_value = Builtins::nil;
160 return m_no_value;
161 }
162
163 void VM::loadPlugin(const uint16_t id, ExecutionContext& context)
164 {
165 namespace fs = std::filesystem;
166
167 const std::string file = m_state.m_constants[id].stringRef();
168
169 std::string path = file;
170 // bytecode loaded from file
172 path = (fs::path(m_state.m_filename).parent_path() / fs::path(file)).relative_path().string();
173
174 std::shared_ptr<SharedLibrary> lib;
175 // if it exists alongside the .arkc file
176 if (Utils::fileExists(path))
177 lib = std::make_shared<SharedLibrary>(path);
178 else
179 {
180 for (auto const& v : m_state.m_libenv)
181 {
182 std::string lib_path = (fs::path(v) / fs::path(file)).string();
183
184 // if it's already loaded don't do anything
185 if (std::ranges::find_if(m_shared_lib_objects, [&](const auto& val) {
186 return (val->path() == path || val->path() == lib_path);
187 }) != m_shared_lib_objects.end())
188 return;
189
190 // check in lib_path
191 if (Utils::fileExists(lib_path))
192 {
193 lib = std::make_shared<SharedLibrary>(lib_path);
194 break;
195 }
196 }
197 }
198
199 if (!lib)
200 {
201 auto lib_path = std::accumulate(
202 std::next(m_state.m_libenv.begin()),
203 m_state.m_libenv.end(),
204 m_state.m_libenv[0].string(),
205 [](const std::string& a, const fs::path& b) -> std::string {
206 return a + "\n\t- " + b.string();
207 });
209 ErrorKind::Module,
210 fmt::format("Could not find module '{}'. Searched under\n\t- {}\n\t- {}", file, path, lib_path));
211 }
212
213 m_shared_lib_objects.emplace_back(lib);
214
215 // load the mapping from the dynamic library
216 try
217 {
218 std::vector<ScopeView::pair_t> data;
219 const mapping* map = m_shared_lib_objects.back()->get<mapping* (*)()>("getFunctionsMapping")();
220
221 std::size_t i = 0;
222 while (map[i].name != nullptr)
223 {
224 const auto it = std::ranges::find(m_state.m_symbols, std::string(map[i].name));
225 if (it != m_state.m_symbols.end())
226 data.emplace_back(static_cast<uint16_t>(std::distance(m_state.m_symbols.begin(), it)), Value(map[i].value));
227
228 ++i;
229 }
230
231 context.locals.back().insertFront(data);
232 }
233 catch (const std::system_error& e)
234 {
236 ErrorKind::Module,
237 fmt::format(
238 "An error occurred while loading module '{}': {}\nIt is most likely because the versions of the module and the language don't match.",
239 file, e.what()));
240 }
241 }
242
243 void VM::exit(const int code) noexcept
244 {
245 m_exit_code = code;
246 m_running = false;
247 }
248
250 {
251 const std::lock_guard lock(m_mutex);
252
253 ExecutionContext* ctx = nullptr;
254
255 // Try and find a free execution context.
256 // If there is only one context, this is the primary one, which can't be reused.
257 // Otherwise, we can check if a context is marked as free and reserve it!
258 // It is possible that all contexts are being used, thus we will create one (active by default) in that case.
259
260 if (m_execution_contexts.size() > 1)
261 {
262 const auto it = std::ranges::find_if(
264 [](const std::unique_ptr<ExecutionContext>& context) -> bool {
265 return !context->primary && context->isFree();
266 });
267
268 if (it != m_execution_contexts.end())
269 {
270 ctx = it->get();
271 ctx->setActive(true);
272 // reset the context before using it
273 ctx->sp = 0;
274 ctx->saved_scope.reset();
275 ctx->stacked_closure_scopes.clear();
276 ctx->locals.clear();
277 }
278 }
279
280 if (ctx == nullptr)
281 // cppcheck-suppress mismatchingContainers
282 ctx = m_execution_contexts.emplace_back(std::make_unique<ExecutionContext>()).get();
283
284 assert(!ctx->primary && "The new context shouldn't be marked as primary!");
285 assert(ctx != m_execution_contexts.front().get() && "The new context isn't really new!");
286
287 const ExecutionContext& primary_ctx = *m_execution_contexts.front();
288 ctx->locals.reserve(primary_ctx.locals.size());
289 ctx->scopes_storage = primary_ctx.scopes_storage;
290 ctx->stacked_closure_scopes.emplace_back(nullptr);
291 ctx->fc = 1;
292
293 for (const auto& scope_view : primary_ctx.locals)
294 {
295 auto& new_scope = ctx->locals.emplace_back(ctx->scopes_storage.data(), scope_view.m_start);
296 for (std::size_t i = 0; i < scope_view.size(); ++i)
297 {
298 const auto& [id, val] = scope_view.atPos(i);
299 new_scope.pushBack(id, val);
300 }
301 }
302
303 return ctx;
304 }
305
307 {
308 const std::lock_guard lock(m_mutex);
309
310 // 1 + 4 additional contexts, it's a bit much (~600kB per context) to have in memory
311 if (m_execution_contexts.size() > 5)
312 {
313 const auto it =
314 std::ranges::remove_if(
316 [ec](const std::unique_ptr<ExecutionContext>& ctx) {
317 return ctx.get() == ec;
318 })
319 .begin();
320 m_execution_contexts.erase(it);
321 }
322 else
323 {
324 // mark the used context as ready to be used again
325 ec->setActive(false);
326 }
327 }
328
329 Future* VM::createFuture(std::vector<Value>& args)
330 {
331 const std::lock_guard lock(m_mutex_futures);
332
334 // so that we have access to the presumed symbol id of the function we are calling
335 // assuming that the callee is always the global context
336 ctx->last_symbol = m_execution_contexts.front()->last_symbol;
337
338 m_futures.push_back(std::make_unique<Future>(ctx, this, args));
339 return m_futures.back().get();
340 }
341
343 {
344 const std::lock_guard lock(m_mutex_futures);
345
346 std::erase_if(
347 m_futures,
348 [f](const std::unique_ptr<Future>& future) {
349 return future.get() == f;
350 });
351 }
352
354 {
355 // load the mapping from the dynamic library
356 try
357 {
358 for (const auto& shared_lib : m_shared_lib_objects)
359 {
360 const mapping* map = shared_lib->get<mapping* (*)()>("getFunctionsMapping")();
361 // load the mapping data
362 std::size_t i = 0;
363 while (map[i].name != nullptr)
364 {
365 // put it in the global frame, aka the first one
366 auto it = std::ranges::find(m_state.m_symbols, std::string(map[i].name));
367 if (it != m_state.m_symbols.end())
368 m_execution_contexts[0]->locals[0].pushBack(
369 static_cast<uint16_t>(std::distance(m_state.m_symbols.begin(), it)),
370 Value(map[i].value));
371
372 ++i;
373 }
374 }
375
376 return true;
377 }
378 catch (const std::system_error&)
379 {
380 return false;
381 }
382 }
383
384 void VM::usePromptFileForDebugger(const std::string& path, std::ostream& os)
385 {
386 m_debugger = std::make_unique<Debugger>(m_state.m_libenv, path, os, m_state.m_symbols, m_state.m_constants);
387 }
388
389 void VM::throwVMError(ErrorKind kind, const std::string& message)
390 {
391 throw std::runtime_error(std::string(errorKinds[static_cast<std::size_t>(kind)]) + ": " + message + "\n");
392 }
393
394 int VM::run(const bool fail_with_exception)
395 {
396 init();
397 safeRun(*m_execution_contexts[0], 0, fail_with_exception);
398 return m_exit_code;
399 }
400
401 int VM::safeRun(ExecutionContext& context, const std::size_t untilFrameCount, const bool fail_with_exception [[maybe_unused]])
402 {
403 m_running = true;
404
405#define WITH_TRY
406
407#ifdef WITH_TRY
408 try
409 {
410#endif
412 unsafeRun<true>(context, untilFrameCount);
413 else
414 unsafeRun<false>(context, untilFrameCount);
415#ifdef WITH_TRY
416 }
417 catch (const Error& e)
418 {
419 if (fail_with_exception)
420 {
421 std::stringstream stream;
422 backtrace(context, stream, /* colorize= */ false);
423 // It's important we have an Ark::Error here, as the constructor for NestedError
424 // does more than just aggregate error messages, hence the code duplication.
425 throw NestedError(e, stream.str(), *this);
426 }
427 showBacktraceWithException(Error(e.details(/* colorize= */ true, *this)), context);
428 }
429 catch (const std::exception& e)
430 {
431 if (fail_with_exception)
432 {
433 std::stringstream stream;
434 backtrace(context, stream, /* colorize= */ false);
435 throw NestedError(e, stream.str());
436 }
437 showBacktraceWithException(e, context);
438 }
439 catch (...)
440 {
441 if (fail_with_exception)
442 throw;
443
444# ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
445 throw;
446# endif
447 fmt::println("Unknown error");
448 backtrace(context);
449 m_exit_code = 1;
450 }
451#endif
452#undef WITH_TRY
453
454 return m_exit_code;
455 }
456
457 template <bool WithDebugger>
458 void VM::unsafeRun(ExecutionContext& context, const std::size_t untilFrameCount)
459 {
460#if ARK_USE_COMPUTED_GOTOS
461# define TARGET(op) TARGET_##op:
462# define DISPATCH_GOTO() \
463 _Pragma("GCC diagnostic push") \
464 _Pragma("GCC diagnostic ignored \"-Wpedantic\"") goto* opcode_targets[inst];
465 _Pragma("GCC diagnostic pop")
466# define GOTO_HALT() goto dispatch_end
467#else
468# define TARGET(op) case op:
469# define DISPATCH_GOTO() goto dispatch_opcode
470# define GOTO_HALT() break
471#endif
472
473#define FETCH_NEXT_INSTRUCTION() \
474 do \
475 { \
476 inst = m_state.inst(context.pp, context.ip); \
477 padding = m_state.inst(context.pp, context.ip + 1); \
478 arg = static_cast<uint16_t>((m_state.inst(context.pp, context.ip + 2) << 8) + \
479 m_state.inst(context.pp, context.ip + 3)); \
480 context.ip += 4; \
481 context.inst_exec_counter = (context.inst_exec_counter + 1) % VMOverflowBufferSize; \
482 if constexpr (WithDebugger) \
483 { \
484 if (!m_debugger) initDebugger(context); \
485 m_debugger->registerInstruction(inst, padding, arg, context.ip - 4, context.pp); \
486 } \
487 if (context.inst_exec_counter < 2 && context.sp >= VMStackSize) \
488 stackOverflowError(context); \
489 } while (false)
490#define DISPATCH() \
491 FETCH_NEXT_INSTRUCTION(); \
492 DISPATCH_GOTO();
493#define UNPACK_ARGS() \
494 do \
495 { \
496 secondary_arg = static_cast<uint16_t>((padding << 4) | (arg & 0xf000) >> 12); \
497 primary_arg = arg & 0x0fff; \
498 } while (false)
499
500#if ARK_USE_COMPUTED_GOTOS
501# pragma GCC diagnostic push
502# pragma GCC diagnostic ignored "-Wpedantic"
503 constexpr std::array opcode_targets = {
504 // cppcheck-suppress syntaxError ; cppcheck do not know about labels addresses (GCC extension)
505# define X(name, value) &&TARGET_##name,
506# include <Ark/Compiler/Instructions.x>
507
508
509# undef X
510 };
511
512 static_assert(opcode_targets.size() == static_cast<std::size_t>(Instruction::InstructionsCount) && "Some instructions are not implemented in the VM");
513# pragma GCC diagnostic pop
514#endif
515
516 uint8_t inst = 0;
517 uint8_t padding = 0;
518 uint16_t arg = 0;
519 uint16_t primary_arg = 0;
520 uint16_t secondary_arg = 0;
521
522 DISPATCH();
523 // cppcheck-suppress unreachableCode ; analysis cannot follow the chain of goto... but it works!
524 {
525#if !ARK_USE_COMPUTED_GOTOS
526 dispatch_opcode:
527 switch (inst)
528#endif
529 {
530#pragma region "Instructions"
531 TARGET(NOP)
532 {
533 DISPATCH();
534 }
535
536 TARGET(LOAD_FAST)
537 {
538 push(loadSymbol(arg, context), context);
539 DISPATCH();
540 }
541
542 TARGET(LOAD_FAST_BY_INDEX)
543 {
544 push(loadSymbolFromIndex(arg, context), context);
545 DISPATCH();
546 }
547
548 TARGET(LOAD_SYMBOL)
549 {
550 // force resolving the reference
551 push(*loadSymbol(arg, context), context);
552 DISPATCH();
553 }
554
555 TARGET(LOAD_CONST)
556 {
557 push(loadConstAsPtr(arg), context);
558 DISPATCH();
559 }
560
561 TARGET(POP_JUMP_IF_TRUE)
562 {
563 if (!!*popAndResolveAsPtr(context))
564 jump(arg, context);
565 DISPATCH();
566 }
567
568 TARGET(STORE)
569 {
570 store(arg, popAndResolveAsPtr(context), context);
571 DISPATCH();
572 }
573
574 TARGET(STORE_REF)
575 {
576 // Not resolving a potential ref is on purpose!
577 // This instruction is only used by functions when storing arguments
578 const Value* tmp = pop(context);
579 store(arg, tmp, context);
580 DISPATCH();
581 }
582
583 TARGET(SET_VAL)
584 {
585 setVal(arg, popAndResolveAsPtr(context), context);
586 DISPATCH();
587 }
588
589 TARGET(POP_JUMP_IF_FALSE)
590 {
591 if (!*popAndResolveAsPtr(context))
592 jump(arg, context);
593 DISPATCH();
594 }
595
596 TARGET(JUMP)
597 {
598 jump(arg, context);
599 DISPATCH();
600 }
601
602 TARGET(RET)
603 {
604 {
605 Value ts = *popAndResolveAsPtr(context);
606 Value ts1 = *popAndResolveAsPtr(context);
607
608 if (ts1.valueType() == ValueType::InstPtr)
609 {
610 context.ip = ts1.pageAddr();
611 // we always push PP then IP, thus the next value
612 // MUST be the page pointer
613 context.pp = pop(context)->pageAddr();
614
615 returnFromFuncCall(context);
616 if (ts.valueType() == ValueType::Garbage)
617 push(Builtins::nil, context);
618 else
619 push(std::move(ts), context);
620 }
621 else if (ts1.valueType() == ValueType::Garbage)
622 {
623 const Value* ip = pop(context);
624 assert(ip->valueType() == ValueType::InstPtr && "Expected instruction pointer on the stack (is the stack trashed?)");
625 context.ip = ip->pageAddr();
626 context.pp = pop(context)->pageAddr();
627
628 returnFromFuncCall(context);
629 push(std::move(ts), context);
630 }
631 else if (ts.valueType() == ValueType::InstPtr)
632 {
633 context.ip = ts.pageAddr();
634 context.pp = ts1.pageAddr();
635 returnFromFuncCall(context);
636 push(Builtins::nil, context);
637 }
638 else
639 throw Error(
640 fmt::format(
641 "Unhandled case when returning from function call. TS=({}){}, TS1=({}){}",
643 ts.toString(*this),
645 ts1.toString(*this)));
646
647 if (context.fc <= untilFrameCount)
648 GOTO_HALT();
649 }
650
651 DISPATCH();
652 }
653
654 TARGET(HALT)
655 {
656 m_running = false;
657 GOTO_HALT();
658 }
659
660 TARGET(PUSH_RETURN_ADDRESS)
661 {
662 push(Value(static_cast<PageAddr_t>(context.pp)), context);
663 // arg * 4 to skip over the call instruction, so that the return address points to AFTER the call
664 push(Value(ValueType::InstPtr, static_cast<PageAddr_t>(arg * 4)), context);
665 context.inst_exec_counter++;
666 DISPATCH();
667 }
668
669 TARGET(CALL)
670 {
671 call(context, arg);
672 if (!m_running)
673 GOTO_HALT();
674 DISPATCH();
675 }
676
677 TARGET(TAIL_CALL_SELF)
678 {
679 jump(0, context);
680 context.locals.back().reset();
681 DISPATCH();
682 }
683
684 TARGET(CAPTURE)
685 {
686 if (!context.saved_scope)
687 context.saved_scope = ClosureScope();
688
689 const Value* ptr = findNearestVariable(arg, context);
690 if (!ptr)
691 throwVMError(ErrorKind::Scope, fmt::format("Couldn't capture `{}' as it is currently unbound", m_state.m_symbols[arg]));
692 else
693 {
694 ptr = ptr->valueType() == ValueType::Reference ? ptr->reference() : ptr;
695 uint16_t id = context.capture_rename_id.value_or(arg);
696 context.saved_scope.value().push_back(id, *ptr);
697 context.capture_rename_id.reset();
698 }
699
700 DISPATCH();
701 }
702
703 TARGET(RENAME_NEXT_CAPTURE)
704 {
705 context.capture_rename_id = arg;
706 DISPATCH();
707 }
708
709 TARGET(BUILTIN)
710 {
711 push(Builtins::builtins[arg].second, context);
712 DISPATCH();
713 }
714
715 TARGET(DEL)
716 {
717 if (Value* var = findNearestVariable(arg, context); var != nullptr)
718 {
719 if (var->valueType() == ValueType::User)
720 var->usertypeRef().del();
721 *var = Value();
722 DISPATCH();
723 }
724
725 throwVMError(ErrorKind::Scope, fmt::format("Can not delete unbound variable `{}'", m_state.m_symbols[arg]));
726 }
727
728 TARGET(MAKE_CLOSURE)
729 {
730 push(Value(Closure(context.saved_scope.value(), m_state.m_constants[arg].pageAddr())), context);
731 context.saved_scope.reset();
732 DISPATCH();
733 }
734
735 TARGET(GET_FIELD)
736 {
737 Value* var = popAndResolveAsPtr(context);
738 push(getField(var, arg, context), context);
739 DISPATCH();
740 }
741
742 TARGET(GET_FIELD_AS_CLOSURE)
743 {
744 Value* var = popAndResolveAsPtr(context);
745 push(getField(var, arg, context, /* push_with_env= */ true), context);
746 DISPATCH();
747 }
748
749 TARGET(PLUGIN)
750 {
751 loadPlugin(arg, context);
752 DISPATCH();
753 }
754
755 TARGET(LIST)
756 {
757 push(createList(arg, context), context);
758 DISPATCH();
759 }
760
761 TARGET(APPEND)
762 {
763 {
764 Value* list = popAndResolveAsPtr(context);
765 if (list->valueType() != ValueType::List)
766 {
767 std::vector<Value> args = { *list };
768 for (uint16_t i = 0; i < arg; ++i)
769 args.push_back(*popAndResolveAsPtr(context));
771 "append",
772 { { types::Contract { { types::Typedef("list", ValueType::List), types::Typedef("value", ValueType::Any, /* is_variadic= */ true) } } } },
773 args);
774 }
775
776 const auto size = static_cast<uint16_t>(list->constList().size());
777
778 Value obj { *list };
779 obj.list().reserve(size + arg);
780
781 for (uint16_t i = 0; i < arg; ++i)
782 obj.push_back(*popAndResolveAsPtr(context));
783 push(std::move(obj), context);
784 }
785 DISPATCH();
786 }
787
788 TARGET(CONCAT)
789 {
790 {
791 Value* list = popAndResolveAsPtr(context);
792 Value obj { *list };
793
794 for (uint16_t i = 0; i < arg; ++i)
795 {
796 Value* next = popAndResolveAsPtr(context);
797
798 if (list->valueType() != ValueType::List || next->valueType() != ValueType::List)
800 "concat",
802 { *list, *next });
803
804 std::ranges::copy(next->list(), std::back_inserter(obj.list()));
805 }
806 push(std::move(obj), context);
807 }
808 DISPATCH();
809 }
810
811 TARGET(APPEND_IN_PLACE)
812 {
813 Value* list = popAndResolveAsPtr(context);
814 listAppendInPlace(list, arg, context);
815 DISPATCH();
816 }
817
818 TARGET(CONCAT_IN_PLACE)
819 {
820 Value* list = popAndResolveAsPtr(context);
821
822 for (uint16_t i = 0; i < arg; ++i)
823 {
824 Value* next = popAndResolveAsPtr(context);
825
826 if (list->valueType() != ValueType::List || next->valueType() != ValueType::List)
828 "concat!",
830 { *list, *next });
831
832 std::ranges::copy(next->list(), std::back_inserter(list->list()));
833 }
834 DISPATCH();
835 }
836
837 TARGET(POP_LIST)
838 {
839 {
840 Value list = *popAndResolveAsPtr(context);
841 Value number = *popAndResolveAsPtr(context);
842
843 if (list.valueType() != ValueType::List || number.valueType() != ValueType::Number)
845 "pop",
847 { list, number });
848
849 long idx = static_cast<long>(number.number());
850 idx = idx < 0 ? static_cast<long>(list.list().size()) + idx : idx;
851 if (std::cmp_greater_equal(idx, list.list().size()) || idx < 0)
853 ErrorKind::Index,
854 fmt::format("pop index ({}) out of range (list size: {})", idx, list.list().size()));
855
856 list.list().erase(list.list().begin() + idx);
857 push(list, context);
858 }
859 DISPATCH();
860 }
861
862 TARGET(POP_LIST_IN_PLACE)
863 {
864 {
865 Value* list = popAndResolveAsPtr(context);
866 Value number = *popAndResolveAsPtr(context);
867
868 if (list->valueType() != ValueType::List || number.valueType() != ValueType::Number)
870 "pop!",
872 { *list, number });
873
874 long idx = static_cast<long>(number.number());
875 idx = idx < 0 ? static_cast<long>(list->list().size()) + idx : idx;
876 if (std::cmp_greater_equal(idx, list->list().size()) || idx < 0)
878 ErrorKind::Index,
879 fmt::format("pop! index ({}) out of range (list size: {})", idx, list->list().size()));
880
881 // Save the value we're removing to push it later.
882 // We need to save the value and push later because we're using a pointer to 'list', and pushing before erasing
883 // would overwrite values from the stack.
884 if (arg)
885 number = list->list()[static_cast<std::size_t>(idx)];
886 list->list().erase(list->list().begin() + idx);
887 if (arg)
888 push(number, context);
889 }
890 DISPATCH();
891 }
892
893 TARGET(SET_AT_INDEX)
894 {
895 {
896 Value* container = popAndResolveAsPtr(context);
897 const Value key = *popAndResolveAsPtr(context);
898 const Value new_value = *popAndResolveAsPtr(context);
899 const bool is_dict = container->valueType() == ValueType::Dict;
900
901 if ((!container->isIndexable() && !is_dict) ||
902 (!is_dict && key.valueType() != ValueType::Number) ||
903 (container->valueType() == ValueType::String && new_value.valueType() != ValueType::String))
905 "@=",
906 { { types::Contract {
909 types::Typedef("new_value", ValueType::Any) } } },
913 types::Typedef("char", ValueType::String) } } },
917 types::Typedef("new_value", ValueType::Any) } } } },
918 { *container, key, new_value });
919
920 if (is_dict)
921 {
922 container->dictRef().set(key, new_value);
923 if (arg)
924 push(new_value, context);
925 }
926 else
927 {
928 const std::size_t size = container->valueType() == ValueType::List ? container->list().size() : container->stringRef().size();
929 long idx = static_cast<long>(key.number());
930 idx = idx < 0 ? static_cast<long>(size) + idx : idx;
931 if (std::cmp_greater_equal(idx, size) || idx < 0)
933 ErrorKind::Index,
934 fmt::format("@= index ({}) out of range (indexable size: {})", idx, size));
935
936 if (container->valueType() == ValueType::List)
937 {
938 container->list()[static_cast<std::size_t>(idx)] = new_value;
939 if (arg)
940 push(new_value, context);
941 }
942 else
943 {
944 container->stringRef()[static_cast<std::size_t>(idx)] = new_value.string()[0];
945 if (arg)
946 push(Value(std::string(1, new_value.string()[0])), context);
947 }
948 }
949 }
950 DISPATCH();
951 }
952
953 TARGET(SET_AT_2_INDEX)
954 {
955 {
956 Value* list = popAndResolveAsPtr(context);
957 Value x = *popAndResolveAsPtr(context);
958 Value y = *popAndResolveAsPtr(context);
959 Value new_value = *popAndResolveAsPtr(context);
960
963 "@@=",
964 { { types::Contract {
968 types::Typedef("new_value", ValueType::Any) } } } },
969 { *list, x, y, new_value });
970
971 long idx_y = static_cast<long>(x.number());
972 idx_y = idx_y < 0 ? static_cast<long>(list->list().size()) + idx_y : idx_y;
973 if (std::cmp_greater_equal(idx_y, list->list().size()) || idx_y < 0)
975 ErrorKind::Index,
976 fmt::format("@@= index (y: {}) out of range (list size: {})", idx_y, list->list().size()));
977
978 if (!list->list()[static_cast<std::size_t>(idx_y)].isIndexable() ||
979 (list->list()[static_cast<std::size_t>(idx_y)].valueType() == ValueType::String && new_value.valueType() != ValueType::String))
981 "@@=",
982 { { types::Contract {
986 types::Typedef("new_value", ValueType::Any) } } },
991 types::Typedef("char", ValueType::String) } } } },
992 { *list, x, y, new_value });
993
994 const bool is_list = list->list()[static_cast<std::size_t>(idx_y)].valueType() == ValueType::List;
995 const std::size_t size =
996 is_list
997 ? list->list()[static_cast<std::size_t>(idx_y)].list().size()
998 : list->list()[static_cast<std::size_t>(idx_y)].stringRef().size();
999
1000 long idx_x = static_cast<long>(y.number());
1001 idx_x = idx_x < 0 ? static_cast<long>(size) + idx_x : idx_x;
1002 if (std::cmp_greater_equal(idx_x, size) || idx_x < 0)
1004 ErrorKind::Index,
1005 fmt::format("@@= index (x: {}) out of range (inner indexable size: {})", idx_x, size));
1006
1007 if (is_list)
1008 {
1009 list->list()[static_cast<std::size_t>(idx_y)].list()[static_cast<std::size_t>(idx_x)] = new_value;
1010 if (arg)
1011 push(new_value, context);
1012 }
1013 else
1014 {
1015 list->list()[static_cast<std::size_t>(idx_y)].stringRef()[static_cast<std::size_t>(idx_x)] = new_value.string()[0];
1016 if (arg)
1017 push(Value(std::string(1, new_value.string()[0])), context);
1018 }
1019 }
1020 DISPATCH();
1021 }
1022
1023 TARGET(POP)
1024 {
1025 pop(context);
1026 DISPATCH();
1027 }
1028
1029 TARGET(SHORTCIRCUIT_AND)
1030 {
1031 if (!*peekAndResolveAsPtr(context))
1032 jump(arg, context);
1033 else
1034 pop(context);
1035 DISPATCH();
1036 }
1037
1038 TARGET(SHORTCIRCUIT_OR)
1039 {
1040 if (!!*peekAndResolveAsPtr(context))
1041 jump(arg, context);
1042 else
1043 pop(context);
1044 DISPATCH();
1045 }
1046
1047 TARGET(CREATE_SCOPE)
1048 {
1049 context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd());
1050 DISPATCH();
1051 }
1052
1053 TARGET(RESET_SCOPE_JUMP)
1054 {
1055 context.locals.back().reset();
1056 jump(arg, context);
1057 DISPATCH();
1058 }
1059
1060 TARGET(POP_SCOPE)
1061 {
1062 if (arg == 1)
1063 {
1064 if (Value* ts = peek(context); ts->valueType() == ValueType::Reference)
1065 *ts = *ts->reference();
1066 }
1067 context.locals.pop_back();
1068 DISPATCH();
1069 }
1070
1071 TARGET(APPLY)
1072 {
1073 {
1074 const Value args_list = *popAndResolveAsPtr(context),
1075 func = *popAndResolveAsPtr(context);
1076 if (args_list.valueType() != ValueType::List || !func.isFunction())
1077 {
1079 "apply",
1080 { {
1083 types::Typedef("args", ValueType::List) } },
1086 types::Typedef("args", ValueType::List) } },
1089 types::Typedef("args", ValueType::List) } },
1090 } },
1091 { func, args_list });
1092 }
1093
1094 push(func, context);
1095 for (const Value& a : args_list.constList())
1096 push(a, context);
1097
1098 call(context, static_cast<uint16_t>(args_list.constList().size()));
1099 }
1100 DISPATCH();
1101 }
1102
1103#pragma endregion
1104
1105#pragma region "Operators"
1106
1107 TARGET(BREAKPOINT)
1108 {
1109 {
1110 bool breakpoint_active = true;
1111 if (arg == 1)
1112 breakpoint_active = *popAndResolveAsPtr(context) == Builtins::trueSym;
1113
1114 if (m_state.m_features & FeatureVMDebugger && breakpoint_active)
1115 {
1116 initDebugger(context);
1117 m_debugger->run(*this, context, /* from_breakpoint= */ true);
1118 m_debugger->resetContextToSavedState(context);
1119
1120 if (m_debugger->shouldQuitVM())
1121 GOTO_HALT();
1122 }
1123 }
1124 DISPATCH();
1125 }
1126
1127 TARGET(ADD)
1128 {
1129 Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1130
1131 if (a->valueType() == ValueType::Number && b->valueType() == ValueType::Number)
1132 push(Value(a->number() + b->number()), context);
1133 else if (a->valueType() == ValueType::String && b->valueType() == ValueType::String)
1134 push(Value(a->string() + b->string()), context);
1135 else
1137 "+",
1140 { *a, *b });
1141 DISPATCH();
1142 }
1143
1144 TARGET(SUB)
1145 {
1146 Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1147
1148 if (a->valueType() != ValueType::Number || b->valueType() != ValueType::Number)
1150 "-",
1152 { *a, *b });
1153 push(Value(a->number() - b->number()), context);
1154 DISPATCH();
1155 }
1156
1157 TARGET(MUL)
1158 {
1159 Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1160
1161 if (a->valueType() != ValueType::Number || b->valueType() != ValueType::Number)
1163 "*",
1165 { *a, *b });
1166 push(Value(a->number() * b->number()), context);
1167 DISPATCH();
1168 }
1169
1170 TARGET(DIV)
1171 {
1172 Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1173
1174 if (a->valueType() != ValueType::Number || b->valueType() != ValueType::Number)
1176 "/",
1178 { *a, *b });
1179 auto d = b->number();
1180 if (d == 0)
1181 throwVMError(ErrorKind::DivisionByZero, fmt::format("Can not compute expression (/ {} {})", a->toString(*this), b->toString(*this)));
1182
1183 push(Value(a->number() / d), context);
1184 DISPATCH();
1185 }
1186
1187 TARGET(GT)
1188 {
1189 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1190 push(*b < *a ? Builtins::trueSym : Builtins::falseSym, context);
1191 DISPATCH();
1192 }
1193
1194 TARGET(LT)
1195 {
1196 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1197 push(*a < *b ? Builtins::trueSym : Builtins::falseSym, context);
1198 DISPATCH();
1199 }
1200
1201 TARGET(LE)
1202 {
1203 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1204 push((*a < *b || *a == *b) ? Builtins::trueSym : Builtins::falseSym, context);
1205 DISPATCH();
1206 }
1207
1208 TARGET(GE)
1209 {
1210 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1211 push((*b < *a || *a == *b) ? Builtins::trueSym : Builtins::falseSym, context);
1212 DISPATCH();
1213 }
1214
1215 TARGET(NEQ)
1216 {
1217 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1218 push(*a != *b ? Builtins::trueSym : Builtins::falseSym, context);
1219 DISPATCH();
1220 }
1221
1222 TARGET(EQ)
1223 {
1224 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1225 push(*a == *b ? Builtins::trueSym : Builtins::falseSym, context);
1226 DISPATCH();
1227 }
1228
1229 TARGET(LEN)
1230 {
1231 const Value* a = popAndResolveAsPtr(context);
1232
1233 if (a->valueType() == ValueType::List)
1234 push(Value(static_cast<int>(a->constList().size())), context);
1235 else if (a->valueType() == ValueType::String)
1236 push(Value(static_cast<int>(a->string().size())), context);
1237 else if (a->valueType() == ValueType::Dict)
1238 push(Value(static_cast<int>(a->dict().size())), context);
1239 else
1241 "len",
1242 { { types::Contract { { types::Typedef("value", ValueType::List) } },
1244 types::Contract { { types::Typedef("value", ValueType::Dict) } } } },
1245 { *a });
1246 DISPATCH();
1247 }
1248
1249 TARGET(IS_EMPTY)
1250 {
1251 const Value* a = popAndResolveAsPtr(context);
1252
1253 if (a->valueType() == ValueType::List)
1254 push(a->constList().empty() ? Builtins::trueSym : Builtins::falseSym, context);
1255 else if (a->valueType() == ValueType::String)
1256 push(a->string().empty() ? Builtins::trueSym : Builtins::falseSym, context);
1257 else if (a->valueType() == ValueType::Dict)
1258 push(std::cmp_equal(a->dict().size(), 0) ? Builtins::trueSym : Builtins::falseSym, context);
1259 else if (a->valueType() == ValueType::Nil)
1260 push(Builtins::trueSym, context);
1261 else
1263 "empty?",
1264 { { types::Contract { { types::Typedef("value", ValueType::List) } },
1267 types::Contract { { types::Typedef("value", ValueType::Dict) } } } },
1268 { *a });
1269 DISPATCH();
1270 }
1271
1272 TARGET(TAIL)
1273 {
1274 Value* const a = popAndResolveAsPtr(context);
1275 push(helper::tail(a), context);
1276 DISPATCH();
1277 }
1278
1279 TARGET(HEAD)
1280 {
1281 Value* const a = popAndResolveAsPtr(context);
1282 push(helper::head(a), context);
1283 DISPATCH();
1284 }
1285
1286 TARGET(IS_NIL)
1287 {
1288 const Value* a = popAndResolveAsPtr(context);
1290 DISPATCH();
1291 }
1292
1293 TARGET(TO_NUM)
1294 {
1295 const Value* a = popAndResolveAsPtr(context);
1296
1297 if (a->valueType() != ValueType::String)
1299 "toNumber",
1300 { { types::Contract { { types::Typedef("value", ValueType::String) } } } },
1301 { *a });
1302
1303 double val;
1304 if (Utils::isDouble(a->string(), &val))
1305 push(Value(val), context);
1306 else
1307 push(Builtins::nil, context);
1308 DISPATCH();
1309 }
1310
1311 TARGET(TO_STR)
1312 {
1313 const Value* a = popAndResolveAsPtr(context);
1314 push(Value(a->toString(*this)), context);
1315 DISPATCH();
1316 }
1317
1318 TARGET(AT)
1319 {
1320 Value& b = *popAndResolveAsPtr(context);
1321 Value& a = *popAndResolveAsPtr(context);
1322 push(helper::at(a, b, *this), context);
1323 DISPATCH();
1324 }
1325
1326 TARGET(AT_AT)
1327 {
1328 {
1329 const Value* x = popAndResolveAsPtr(context);
1330 const Value* y = popAndResolveAsPtr(context);
1331 Value& list = *popAndResolveAsPtr(context);
1332
1333 push(helper::atAt(x, y, list), context);
1334 }
1335 DISPATCH();
1336 }
1337
1338 TARGET(MOD)
1339 {
1340 const Value *b = popAndResolveAsPtr(context), *a = popAndResolveAsPtr(context);
1341 if (a->valueType() != ValueType::Number || b->valueType() != ValueType::Number)
1343 "mod",
1345 { *a, *b });
1346 push(Value(std::fmod(a->number(), b->number())), context);
1347 DISPATCH();
1348 }
1349
1350 TARGET(TYPE)
1351 {
1352 const Value* a = popAndResolveAsPtr(context);
1353 push(Value(std::to_string(a->valueType())), context);
1354 DISPATCH();
1355 }
1356
1357 TARGET(HAS_FIELD)
1358 {
1359 {
1360 Value* const field = popAndResolveAsPtr(context);
1361 Value* const closure = popAndResolveAsPtr(context);
1362 if (closure->valueType() != ValueType::Closure || field->valueType() != ValueType::String)
1364 "hasField",
1366 { *closure, *field });
1367
1368 auto it = std::ranges::find(m_state.m_symbols, field->stringRef());
1369 if (it == m_state.m_symbols.end())
1370 {
1371 push(Builtins::falseSym, context);
1372 DISPATCH();
1373 }
1374
1375 auto id = static_cast<std::uint16_t>(std::distance(m_state.m_symbols.begin(), it));
1376 push(closure->refClosure().refScope()[id] != nullptr ? Builtins::trueSym : Builtins::falseSym, context);
1377 }
1378 DISPATCH();
1379 }
1380
1381 TARGET(NOT)
1382 {
1383 const Value* a = popAndResolveAsPtr(context);
1384 push(!(*a) ? Builtins::trueSym : Builtins::falseSym, context);
1385 DISPATCH();
1386 }
1387
1388#pragma endregion
1389
1390#pragma region "Super Instructions"
1391 TARGET(LOAD_CONST_LOAD_CONST)
1392 {
1393 UNPACK_ARGS();
1394 push(loadConstAsPtr(primary_arg), context);
1395 push(loadConstAsPtr(secondary_arg), context);
1396 context.inst_exec_counter++;
1397 DISPATCH();
1398 }
1399
1400 TARGET(LOAD_CONST_STORE)
1401 {
1402 UNPACK_ARGS();
1403 store(secondary_arg, loadConstAsPtr(primary_arg), context);
1404 DISPATCH();
1405 }
1406
1407 TARGET(LOAD_CONST_SET_VAL)
1408 {
1409 UNPACK_ARGS();
1410 setVal(secondary_arg, loadConstAsPtr(primary_arg), context);
1411 DISPATCH();
1412 }
1413
1414 TARGET(STORE_FROM)
1415 {
1416 UNPACK_ARGS();
1417 store(secondary_arg, loadSymbol(primary_arg, context), context);
1418 DISPATCH();
1419 }
1420
1421 TARGET(STORE_FROM_INDEX)
1422 {
1423 UNPACK_ARGS();
1424 store(secondary_arg, loadSymbolFromIndex(primary_arg, context), context);
1425 DISPATCH();
1426 }
1427
1428 TARGET(SET_VAL_FROM)
1429 {
1430 UNPACK_ARGS();
1431 setVal(secondary_arg, loadSymbol(primary_arg, context), context);
1432 DISPATCH();
1433 }
1434
1435 TARGET(SET_VAL_FROM_INDEX)
1436 {
1437 UNPACK_ARGS();
1438 setVal(secondary_arg, loadSymbolFromIndex(primary_arg, context), context);
1439 DISPATCH();
1440 }
1441
1442 TARGET(INCREMENT)
1443 {
1444 UNPACK_ARGS();
1445 {
1446 Value* var = loadSymbol(primary_arg, context);
1447
1448 // use internal reference, shouldn't break anything so far, unless it's already a ref
1449 if (var->valueType() == ValueType::Reference)
1450 var = var->reference();
1451
1452 if (var->valueType() == ValueType::Number)
1453 push(Value(var->number() + secondary_arg), context);
1454 else
1456 "+",
1458 { *var, Value(secondary_arg) });
1459 }
1460 DISPATCH();
1461 }
1462
1463 TARGET(INCREMENT_BY_INDEX)
1464 {
1465 UNPACK_ARGS();
1466 {
1467 Value* var = loadSymbolFromIndex(primary_arg, context);
1468
1469 // use internal reference, shouldn't break anything so far, unless it's already a ref
1470 if (var->valueType() == ValueType::Reference)
1471 var = var->reference();
1472
1473 if (var->valueType() == ValueType::Number)
1474 push(Value(var->number() + secondary_arg), context);
1475 else
1477 "+",
1479 { *var, Value(secondary_arg) });
1480 }
1481 DISPATCH();
1482 }
1483
1484 TARGET(INCREMENT_STORE)
1485 {
1486 UNPACK_ARGS();
1487 {
1488 Value* var = loadSymbol(primary_arg, context);
1489
1490 // use internal reference, shouldn't break anything so far, unless it's already a ref
1491 if (var->valueType() == ValueType::Reference)
1492 var = var->reference();
1493
1494 if (var->valueType() == ValueType::Number)
1495 {
1496 auto val = Value(var->number() + secondary_arg);
1497 setVal(primary_arg, &val, context);
1498 }
1499 else
1501 "+",
1503 { *var, Value(secondary_arg) });
1504 }
1505 DISPATCH();
1506 }
1507
1508 TARGET(DECREMENT)
1509 {
1510 UNPACK_ARGS();
1511 {
1512 Value* var = loadSymbol(primary_arg, context);
1513
1514 // use internal reference, shouldn't break anything so far, unless it's already a ref
1515 if (var->valueType() == ValueType::Reference)
1516 var = var->reference();
1517
1518 if (var->valueType() == ValueType::Number)
1519 push(Value(var->number() - secondary_arg), context);
1520 else
1522 "-",
1524 { *var, Value(secondary_arg) });
1525 }
1526 DISPATCH();
1527 }
1528
1529 TARGET(DECREMENT_BY_INDEX)
1530 {
1531 UNPACK_ARGS();
1532 {
1533 Value* var = loadSymbolFromIndex(primary_arg, context);
1534
1535 // use internal reference, shouldn't break anything so far, unless it's already a ref
1536 if (var->valueType() == ValueType::Reference)
1537 var = var->reference();
1538
1539 if (var->valueType() == ValueType::Number)
1540 push(Value(var->number() - secondary_arg), context);
1541 else
1543 "-",
1545 { *var, Value(secondary_arg) });
1546 }
1547 DISPATCH();
1548 }
1549
1550 TARGET(DECREMENT_STORE)
1551 {
1552 UNPACK_ARGS();
1553 {
1554 Value* var = loadSymbol(primary_arg, context);
1555
1556 // use internal reference, shouldn't break anything so far, unless it's already a ref
1557 if (var->valueType() == ValueType::Reference)
1558 var = var->reference();
1559
1560 if (var->valueType() == ValueType::Number)
1561 {
1562 auto val = Value(var->number() - secondary_arg);
1563 setVal(primary_arg, &val, context);
1564 }
1565 else
1567 "-",
1569 { *var, Value(secondary_arg) });
1570 }
1571 DISPATCH();
1572 }
1573
1574 TARGET(STORE_TAIL)
1575 {
1576 UNPACK_ARGS();
1577 {
1578 Value* list = loadSymbol(primary_arg, context);
1579 Value tail = helper::tail(list);
1580 store(secondary_arg, &tail, context);
1581 }
1582 DISPATCH();
1583 }
1584
1585 TARGET(STORE_TAIL_BY_INDEX)
1586 {
1587 UNPACK_ARGS();
1588 {
1589 Value* list = loadSymbolFromIndex(primary_arg, context);
1590 Value tail = helper::tail(list);
1591 store(secondary_arg, &tail, context);
1592 }
1593 DISPATCH();
1594 }
1595
1596 TARGET(STORE_HEAD)
1597 {
1598 UNPACK_ARGS();
1599 {
1600 Value* list = loadSymbol(primary_arg, context);
1601 Value head = helper::head(list);
1602 store(secondary_arg, &head, context);
1603 }
1604 DISPATCH();
1605 }
1606
1607 TARGET(STORE_HEAD_BY_INDEX)
1608 {
1609 UNPACK_ARGS();
1610 {
1611 Value* list = loadSymbolFromIndex(primary_arg, context);
1612 Value head = helper::head(list);
1613 store(secondary_arg, &head, context);
1614 }
1615 DISPATCH();
1616 }
1617
1618 TARGET(STORE_LIST)
1619 {
1620 UNPACK_ARGS();
1621 {
1622 Value l = createList(primary_arg, context);
1623 store(secondary_arg, &l, context);
1624 }
1625 DISPATCH();
1626 }
1627
1628 TARGET(SET_VAL_TAIL)
1629 {
1630 UNPACK_ARGS();
1631 {
1632 Value* list = loadSymbol(primary_arg, context);
1633 Value tail = helper::tail(list);
1634 setVal(secondary_arg, &tail, context);
1635 }
1636 DISPATCH();
1637 }
1638
1639 TARGET(SET_VAL_TAIL_BY_INDEX)
1640 {
1641 UNPACK_ARGS();
1642 {
1643 Value* list = loadSymbolFromIndex(primary_arg, context);
1644 Value tail = helper::tail(list);
1645 setVal(secondary_arg, &tail, context);
1646 }
1647 DISPATCH();
1648 }
1649
1650 TARGET(SET_VAL_HEAD)
1651 {
1652 UNPACK_ARGS();
1653 {
1654 Value* list = loadSymbol(primary_arg, context);
1655 Value head = helper::head(list);
1656 setVal(secondary_arg, &head, context);
1657 }
1658 DISPATCH();
1659 }
1660
1661 TARGET(SET_VAL_HEAD_BY_INDEX)
1662 {
1663 UNPACK_ARGS();
1664 {
1665 Value* list = loadSymbolFromIndex(primary_arg, context);
1666 Value head = helper::head(list);
1667 setVal(secondary_arg, &head, context);
1668 }
1669 DISPATCH();
1670 }
1671
1672 TARGET(CALL_BUILTIN)
1673 {
1674 UNPACK_ARGS();
1675 // no stack size check because we do not push IP/PP since we are just calling a builtin
1677 context,
1678 Builtins::builtins[primary_arg].second,
1679 secondary_arg,
1680 /* remove_return_address= */ true,
1681 /* remove_builtin= */ false);
1682 if (!m_running)
1683 GOTO_HALT();
1684 DISPATCH();
1685 }
1686
1687 TARGET(CALL_BUILTIN_WITHOUT_RETURN_ADDRESS)
1688 {
1689 UNPACK_ARGS();
1690 // no stack size check because we do not push IP/PP since we are just calling a builtin
1692 context,
1693 Builtins::builtins[primary_arg].second,
1694 secondary_arg,
1695 // we didn't have a PUSH_RETURN_ADDRESS instruction before,
1696 // so do not attempt to remove (pp,ip) from the stack: they're not there!
1697 /* remove_return_address= */ false,
1698 /* remove_builtin= */ false);
1699 if (!m_running)
1700 GOTO_HALT();
1701 DISPATCH();
1702 }
1703
1704 TARGET(LT_CONST_JUMP_IF_FALSE)
1705 {
1706 UNPACK_ARGS();
1707 const Value* sym = popAndResolveAsPtr(context);
1708 if (!(*sym < *loadConstAsPtr(primary_arg)))
1709 jump(secondary_arg, context);
1710 DISPATCH();
1711 }
1712
1713 TARGET(LT_CONST_JUMP_IF_TRUE)
1714 {
1715 UNPACK_ARGS();
1716 const Value* sym = popAndResolveAsPtr(context);
1717 if (*sym < *loadConstAsPtr(primary_arg))
1718 jump(secondary_arg, context);
1719 DISPATCH();
1720 }
1721
1722 TARGET(LT_SYM_JUMP_IF_FALSE)
1723 {
1724 UNPACK_ARGS();
1725 const Value* sym = popAndResolveAsPtr(context);
1726 if (!(*sym < *loadSymbol(primary_arg, context)))
1727 jump(secondary_arg, context);
1728 DISPATCH();
1729 }
1730
1731 TARGET(GT_CONST_JUMP_IF_TRUE)
1732 {
1733 UNPACK_ARGS();
1734 const Value* sym = popAndResolveAsPtr(context);
1735 const Value* cst = loadConstAsPtr(primary_arg);
1736 if (*cst < *sym)
1737 jump(secondary_arg, context);
1738 DISPATCH();
1739 }
1740
1741 TARGET(GT_CONST_JUMP_IF_FALSE)
1742 {
1743 UNPACK_ARGS();
1744 const Value* sym = popAndResolveAsPtr(context);
1745 const Value* cst = loadConstAsPtr(primary_arg);
1746 if (!(*cst < *sym))
1747 jump(secondary_arg, context);
1748 DISPATCH();
1749 }
1750
1751 TARGET(GT_SYM_JUMP_IF_FALSE)
1752 {
1753 UNPACK_ARGS();
1754 const Value* sym = popAndResolveAsPtr(context);
1755 const Value* rhs = loadSymbol(primary_arg, context);
1756 if (!(*rhs < *sym))
1757 jump(secondary_arg, context);
1758 DISPATCH();
1759 }
1760
1761 TARGET(EQ_CONST_JUMP_IF_TRUE)
1762 {
1763 UNPACK_ARGS();
1764 const Value* sym = popAndResolveAsPtr(context);
1765 if (*sym == *loadConstAsPtr(primary_arg))
1766 jump(secondary_arg, context);
1767 DISPATCH();
1768 }
1769
1770 TARGET(EQ_SYM_INDEX_JUMP_IF_TRUE)
1771 {
1772 UNPACK_ARGS();
1773 const Value* sym = popAndResolveAsPtr(context);
1774 if (*sym == *loadSymbolFromIndex(primary_arg, context))
1775 jump(secondary_arg, context);
1776 DISPATCH();
1777 }
1778
1779 TARGET(NEQ_CONST_JUMP_IF_TRUE)
1780 {
1781 UNPACK_ARGS();
1782 const Value* sym = popAndResolveAsPtr(context);
1783 if (*sym != *loadConstAsPtr(primary_arg))
1784 jump(secondary_arg, context);
1785 DISPATCH();
1786 }
1787
1788 TARGET(NEQ_SYM_JUMP_IF_FALSE)
1789 {
1790 UNPACK_ARGS();
1791 const Value* sym = popAndResolveAsPtr(context);
1792 if (*sym == *loadSymbol(primary_arg, context))
1793 jump(secondary_arg, context);
1794 DISPATCH();
1795 }
1796
1797 TARGET(CALL_SYMBOL)
1798 {
1799 UNPACK_ARGS();
1800 call(context, secondary_arg, /* function_ptr= */ loadSymbol(primary_arg, context));
1801 if (!m_running)
1802 GOTO_HALT();
1803 DISPATCH();
1804 }
1805
1806 TARGET(CALL_SYMBOL_BY_INDEX)
1807 {
1808 UNPACK_ARGS();
1809 call(context, secondary_arg, /* function_ptr= */ loadSymbolFromIndex(primary_arg, context));
1810 if (!m_running)
1811 GOTO_HALT();
1812 DISPATCH();
1813 }
1814
1815 TARGET(CALL_CURRENT_PAGE)
1816 {
1817 UNPACK_ARGS();
1818 context.last_symbol = primary_arg;
1819 call(context, secondary_arg, /* function_ptr= */ nullptr, /* or_address= */ static_cast<PageAddr_t>(context.pp));
1820 if (!m_running)
1821 GOTO_HALT();
1822 DISPATCH();
1823 }
1824
1825 TARGET(GET_FIELD_FROM_SYMBOL)
1826 {
1827 UNPACK_ARGS();
1828 push(getField(loadSymbol(primary_arg, context), secondary_arg, context), context);
1829 DISPATCH();
1830 }
1831
1832 TARGET(GET_FIELD_FROM_SYMBOL_INDEX)
1833 {
1834 UNPACK_ARGS();
1835 push(getField(loadSymbolFromIndex(primary_arg, context), secondary_arg, context), context);
1836 DISPATCH();
1837 }
1838
1839 TARGET(AT_SYM_SYM)
1840 {
1841 UNPACK_ARGS();
1842 push(helper::at(*loadSymbol(primary_arg, context), *loadSymbol(secondary_arg, context), *this), context);
1843 DISPATCH();
1844 }
1845
1846 TARGET(AT_SYM_INDEX_SYM_INDEX)
1847 {
1848 UNPACK_ARGS();
1849 push(helper::at(*loadSymbolFromIndex(primary_arg, context), *loadSymbolFromIndex(secondary_arg, context), *this), context);
1850 DISPATCH();
1851 }
1852
1853 TARGET(AT_SYM_INDEX_CONST)
1854 {
1855 UNPACK_ARGS();
1856 push(helper::at(*loadSymbolFromIndex(primary_arg, context), *loadConstAsPtr(secondary_arg), *this), context);
1857 DISPATCH();
1858 }
1859
1860 TARGET(CHECK_TYPE_OF)
1861 {
1862 UNPACK_ARGS();
1863 const Value* sym = loadSymbol(primary_arg, context);
1864 const Value* cst = loadConstAsPtr(secondary_arg);
1865 push(
1866 cst->valueType() == ValueType::String &&
1867 std::to_string(sym->valueType()) == cst->string()
1870 context);
1871 DISPATCH();
1872 }
1873
1874 TARGET(CHECK_TYPE_OF_BY_INDEX)
1875 {
1876 UNPACK_ARGS();
1877 const Value* sym = loadSymbolFromIndex(primary_arg, context);
1878 const Value* cst = loadConstAsPtr(secondary_arg);
1879 push(
1880 cst->valueType() == ValueType::String &&
1881 std::to_string(sym->valueType()) == cst->string()
1884 context);
1885 DISPATCH();
1886 }
1887
1888 TARGET(APPEND_IN_PLACE_SYM)
1889 {
1890 UNPACK_ARGS();
1891 listAppendInPlace(loadSymbol(primary_arg, context), secondary_arg, context);
1892 DISPATCH();
1893 }
1894
1895 TARGET(APPEND_IN_PLACE_SYM_INDEX)
1896 {
1897 UNPACK_ARGS();
1898 listAppendInPlace(loadSymbolFromIndex(primary_arg, context), secondary_arg, context);
1899 DISPATCH();
1900 }
1901
1902 TARGET(STORE_LEN)
1903 {
1904 UNPACK_ARGS();
1905 {
1906 Value* a = loadSymbolFromIndex(primary_arg, context);
1907 Value len;
1908 if (a->valueType() == ValueType::List)
1909 len = Value(static_cast<int>(a->constList().size()));
1910 else if (a->valueType() == ValueType::String)
1911 len = Value(static_cast<int>(a->string().size()));
1912 else
1914 "len",
1915 { { types::Contract { { types::Typedef("value", ValueType::List) } },
1916 types::Contract { { types::Typedef("value", ValueType::String) } } } },
1917 { *a });
1918 store(secondary_arg, &len, context);
1919 }
1920 DISPATCH();
1921 }
1922
1923 TARGET(LT_LEN_SYM_JUMP_IF_FALSE)
1924 {
1925 UNPACK_ARGS();
1926 {
1927 const Value* sym = loadSymbol(primary_arg, context);
1928 Value size;
1929
1930 if (sym->valueType() == ValueType::List)
1931 size = Value(static_cast<int>(sym->constList().size()));
1932 else if (sym->valueType() == ValueType::String)
1933 size = Value(static_cast<int>(sym->string().size()));
1934 else
1936 "len",
1937 { { types::Contract { { types::Typedef("value", ValueType::List) } },
1938 types::Contract { { types::Typedef("value", ValueType::String) } } } },
1939 { *sym });
1940
1941 if (!(*popAndResolveAsPtr(context) < size))
1942 jump(secondary_arg, context);
1943 }
1944 DISPATCH();
1945 }
1946
1947 TARGET(MUL_BY)
1948 {
1949 UNPACK_ARGS();
1950 {
1951 Value* var = loadSymbol(primary_arg, context);
1952 const int other = static_cast<int>(secondary_arg) - 2048;
1953
1954 // use internal reference, shouldn't break anything so far, unless it's already a ref
1955 if (var->valueType() == ValueType::Reference)
1956 var = var->reference();
1957
1958 if (var->valueType() == ValueType::Number)
1959 push(Value(var->number() * other), context);
1960 else
1962 "*",
1964 { *var, Value(other) });
1965 }
1966 DISPATCH();
1967 }
1968
1969 TARGET(MUL_BY_INDEX)
1970 {
1971 UNPACK_ARGS();
1972 {
1973 Value* var = loadSymbolFromIndex(primary_arg, context);
1974 const int other = static_cast<int>(secondary_arg) - 2048;
1975
1976 // use internal reference, shouldn't break anything so far, unless it's already a ref
1977 if (var->valueType() == ValueType::Reference)
1978 var = var->reference();
1979
1980 if (var->valueType() == ValueType::Number)
1981 push(Value(var->number() * other), context);
1982 else
1984 "*",
1986 { *var, Value(other) });
1987 }
1988 DISPATCH();
1989 }
1990
1991 TARGET(MUL_SET_VAL)
1992 {
1993 UNPACK_ARGS();
1994 {
1995 Value* var = loadSymbol(primary_arg, context);
1996 const int other = static_cast<int>(secondary_arg) - 2048;
1997
1998 // use internal reference, shouldn't break anything so far, unless it's already a ref
1999 if (var->valueType() == ValueType::Reference)
2000 var = var->reference();
2001
2002 if (var->valueType() == ValueType::Number)
2003 {
2004 auto val = Value(var->number() * other);
2005 setVal(primary_arg, &val, context);
2006 }
2007 else
2009 "*",
2011 { *var, Value(other) });
2012 }
2013 DISPATCH();
2014 }
2015
2016 TARGET(FUSED_MATH)
2017 {
2018 const auto op1 = static_cast<Instruction>(padding),
2019 op2 = static_cast<Instruction>((arg & 0xff00) >> 8),
2020 op3 = static_cast<Instruction>(arg & 0x00ff);
2021 const std::size_t arg_count = (op1 != NOP) + (op2 != NOP) + (op3 != NOP);
2022
2023 const Value* d = popAndResolveAsPtr(context);
2024 const Value* c = popAndResolveAsPtr(context);
2025 const Value* b = popAndResolveAsPtr(context);
2026
2031 { *c, *d });
2032
2033 double temp = helper::doMath(c->number(), d->number(), op1);
2034 if (b->valueType() != ValueType::Number)
2038 { *b, Value(temp) });
2039 temp = helper::doMath(b->number(), temp, op2);
2040
2041 if (arg_count == 2)
2042 push(Value(temp), context);
2043 else if (arg_count == 3)
2044 {
2045 const Value* a = popAndResolveAsPtr(context);
2046 if (a->valueType() != ValueType::Number)
2050 { *a, Value(temp) });
2051
2052 temp = helper::doMath(a->number(), temp, op3);
2053 push(Value(temp), context);
2054 }
2055 else
2056 throw Error(
2057 fmt::format(
2058 "FUSED_MATH got {} arguments, expected 2 or 3. Arguments: {:x}{:x}{:x}. There is a bug in the codegen!",
2059 arg_count, static_cast<uint8_t>(op1), static_cast<uint8_t>(op2), static_cast<uint8_t>(op3)));
2060 DISPATCH();
2061 }
2062#pragma endregion
2063 }
2064#if ARK_USE_COMPUTED_GOTOS
2065 dispatch_end:
2066 do
2067 {
2068 } while (false);
2069#endif
2070 }
2071 }
2072
2073 uint16_t VM::findNearestVariableIdWithValue(const Value& value, ExecutionContext& context) const noexcept
2074 {
2075 for (auto& local : std::ranges::reverse_view(context.locals))
2076 {
2077 if (const auto id = local.idFromValue(value); id < m_state.m_symbols.size())
2078 return id;
2079 }
2080 return MaxValue16Bits;
2081 }
2082
2083 void VM::throwArityError(std::size_t passed_arg_count, std::size_t expected_arg_count, ExecutionContext& context, const bool skip_function)
2084 {
2085 std::vector<std::string> arg_names;
2086 arg_names.reserve(expected_arg_count + 1);
2087
2088 std::size_t index = 0;
2089 while (m_state.inst(context.pp, index) == STORE ||
2090 m_state.inst(context.pp, index) == STORE_REF)
2091 {
2092 const auto id = static_cast<uint16_t>((m_state.inst(context.pp, index + 2) << 8) + m_state.inst(context.pp, index + 3));
2093 arg_names.insert(arg_names.begin(), m_state.m_symbols[id]);
2094 index += 4;
2095 }
2096 // we have no arg names, probably because of a CALL_BUILTIN_WITHOUT_RETURN_ADDRESS
2097 if (arg_names.empty() && index == 0)
2098 {
2099 for (std::size_t i = 0; i < expected_arg_count; ++i)
2100 arg_names.emplace_back(1, static_cast<char>('a' + i));
2101 }
2102 if (expected_arg_count > 0)
2103 arg_names.insert(arg_names.begin(), ""); // for formatting, so that we have a space between the function and the args
2104
2105 std::vector<std::string> arg_vals;
2106 arg_vals.reserve(passed_arg_count + 1);
2107
2108 for (std::size_t i = 0; i < passed_arg_count && i + 1 <= context.sp; ++i)
2109 // -1 on the stack because we always point to the next available slot
2110 arg_vals.push_back(context.stack[context.sp - passed_arg_count + i].toString(*this));
2111 if (passed_arg_count > 0)
2112 arg_vals.insert(arg_vals.begin(), ""); // for formatting, so that we have a space between the function and the args
2113
2114 // set ip/pp to the callee location so that the error can pinpoint the line
2115 // where the bad call happened
2116 if (context.sp >= 2 + passed_arg_count)
2117 {
2118 // -2/-3 instead of -1/-2 to skip over the function pushed on the stack
2119 context.ip = context.stack[context.sp - 1 - (skip_function ? 1 : 0) - passed_arg_count].pageAddr();
2120 context.pp = context.stack[context.sp - 2 - (skip_function ? 1 : 0) - passed_arg_count].pageAddr();
2121 context.sp -= 2;
2122 returnFromFuncCall(context);
2123 }
2124
2125 std::string function_name = (context.last_symbol < m_state.m_symbols.size())
2126 ? m_state.m_symbols[context.last_symbol]
2127 : Value(static_cast<PageAddr_t>(context.pp)).toString(*this);
2128
2130 ErrorKind::Arity,
2131 fmt::format(
2132 "When calling `({}{})', received {} argument{}, but expected {}: `({}{})'",
2133 function_name,
2134 fmt::join(arg_vals, " "),
2135 passed_arg_count,
2136 passed_arg_count > 1 ? "s" : "",
2137 expected_arg_count,
2138 function_name,
2139 fmt::join(arg_names, " ")));
2140 }
2141
2143 {
2144 if (!m_debugger)
2145 m_debugger = std::make_unique<Debugger>(context, m_state.m_libenv, m_state.m_symbols, m_state.m_constants);
2146 else
2147 m_debugger->saveState(context);
2148 }
2149
2150 void VM::showBacktraceWithException(const std::exception& e, ExecutionContext& context)
2151 {
2152 std::string text = e.what();
2153 if (!text.empty() && text.back() != '\n')
2154 text += '\n';
2155 fmt::println(std::cerr, "{}", text);
2156
2157 // If code being run from the debugger crashed, ignore it and don't trigger a debugger inside the VM inside the debugger inside the VM
2158 const bool error_from_debugger = m_debugger && m_debugger->isRunning();
2159 if (m_state.m_features & FeatureVMDebugger && !error_from_debugger)
2160 initDebugger(context);
2161
2162 const std::size_t saved_ip = context.ip;
2163 const std::size_t saved_pp = context.pp;
2164 const uint16_t saved_sp = context.sp;
2165
2166 backtrace(context);
2167
2168 fmt::println(
2169 std::cerr,
2170 "At IP: {}, PP: {}, SP: {}",
2171 // dividing by 4 because the instructions are actually on 4 bytes
2172 fmt::styled(saved_ip / 4, fmt::fg(fmt::color::cyan)),
2173 fmt::styled(saved_pp, fmt::fg(fmt::color::green)),
2174 fmt::styled(saved_sp, fmt::fg(fmt::color::yellow)));
2175
2176 if (m_debugger && !error_from_debugger)
2177 {
2178 m_debugger->resetContextToSavedState(context);
2179 m_debugger->run(*this, context, /* from_breakpoint= */ false);
2180 }
2181
2182#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
2183 // don't report a "failed" exit code so that the fuzzers can more accurately triage crashes
2184 m_exit_code = 0;
2185#else
2186 m_exit_code = 1;
2187#endif
2188 }
2189
2190 std::optional<InstLoc> VM::findSourceLocation(const std::size_t ip, const std::size_t pp) const
2191 {
2192 std::optional<InstLoc> match = std::nullopt;
2193
2194 for (const auto location : m_state.m_inst_locations)
2195 {
2196 if (location.page_pointer == pp && !match)
2197 match = location;
2198
2199 // select the best match: we want to find the location that's nearest our instruction pointer,
2200 // but not equal to it as the IP will always be pointing to the next instruction,
2201 // not yet executed. Thus, the erroneous instruction is the previous one.
2202 if (location.page_pointer == pp && match && location.inst_pointer < ip / 4)
2203 match = location;
2204
2205 // early exit because we won't find anything better, as inst locations are ordered by ascending (pp, ip)
2206 if (location.page_pointer > pp || (location.page_pointer == pp && location.inst_pointer >= ip / 4))
2207 break;
2208 }
2209
2210 return match;
2211 }
2212
2213 std::string VM::debugShowSource() const
2214 {
2215 const auto& context = m_execution_contexts.front();
2216 auto maybe_source_loc = findSourceLocation(context->ip, context->pp);
2217 if (maybe_source_loc)
2218 {
2219 const auto filename = m_state.m_filenames[maybe_source_loc->filename_id];
2220 return fmt::format("{}:{} -- IP: {}, PP: {}", filename, maybe_source_loc->line + 1, maybe_source_loc->inst_pointer, maybe_source_loc->page_pointer);
2221 }
2222 return "No source location found";
2223 }
2224
2225 void VM::backtrace(ExecutionContext& context, std::ostream& os, const bool colorize)
2226 {
2227 constexpr std::size_t max_consecutive_traces = 7;
2228
2229 const auto maybe_location = findSourceLocation(context.ip, context.pp);
2230 if (maybe_location)
2231 {
2232 const auto filename = m_state.m_filenames[maybe_location->filename_id];
2233
2234 if (Utils::fileExists(filename))
2237 .filename = filename,
2238 .start = FilePos { .line = maybe_location->line, .column = 0 },
2239 .end = std::nullopt,
2240 .maybe_content = std::nullopt },
2241 os,
2242 /* maybe_context= */ std::nullopt,
2243 /* colorize= */ colorize);
2244 fmt::println(os, "");
2245 }
2246
2247 if (context.fc > 1)
2248 {
2249 // display call stack trace
2250 const ScopeView old_scope = context.locals.back();
2251
2252 std::string previous_trace;
2253 std::size_t displayed_traces = 0;
2254 std::size_t consecutive_similar_traces = 0;
2255
2256 while (context.fc != 0 && context.pp != 0 && context.sp > 0)
2257 {
2258 const auto maybe_call_loc = findSourceLocation(context.ip, context.pp);
2259 const auto loc_as_text = maybe_call_loc ? fmt::format(" ({}:{})", m_state.m_filenames[maybe_call_loc->filename_id], maybe_call_loc->line + 1) : "";
2260
2261 const uint16_t id = findNearestVariableIdWithValue(
2262 Value(static_cast<PageAddr_t>(context.pp)),
2263 context);
2264 const std::string& func_name = (id < m_state.m_symbols.size()) ? m_state.m_symbols[id] : "???";
2265
2266 if (func_name + loc_as_text != previous_trace)
2267 {
2268 fmt::println(
2269 os,
2270 "[{:4}] In function `{}'{}",
2271 fmt::styled(context.fc, colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style()),
2272 fmt::styled(func_name, colorize ? fmt::fg(fmt::color::green) : fmt::text_style()),
2273 loc_as_text);
2274 previous_trace = func_name + loc_as_text;
2275 ++displayed_traces;
2276 consecutive_similar_traces = 0;
2277 }
2278 else if (consecutive_similar_traces == 0)
2279 {
2280 fmt::println(os, " ...");
2281 ++consecutive_similar_traces;
2282 }
2283
2284 const Value* ip;
2285 do
2286 {
2287 ip = popAndResolveAsPtr(context);
2288 } while (ip->valueType() != ValueType::InstPtr);
2289
2290 context.ip = ip->pageAddr();
2291 context.pp = pop(context)->pageAddr();
2292 returnFromFuncCall(context);
2293
2294 if (displayed_traces > max_consecutive_traces)
2295 {
2296 fmt::println(os, " ...");
2297 break;
2298 }
2299 }
2300
2301 if (context.pp == 0)
2302 {
2303 const auto maybe_call_loc = findSourceLocation(context.ip, context.pp);
2304 const auto loc_as_text = maybe_call_loc ? fmt::format(" ({}:{})", m_state.m_filenames[maybe_call_loc->filename_id], maybe_call_loc->line + 1) : "";
2305 fmt::println(os, "[{:4}] In global scope{}", fmt::styled(context.fc, colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style()), loc_as_text);
2306 }
2307
2308 // display variables values in the current scope
2309 fmt::println(os, "\nCurrent scope variables values:");
2310 for (std::size_t i = 0, size = old_scope.size(); i < size; ++i)
2311 {
2312 fmt::println(
2313 os,
2314 "{} = {}",
2315 fmt::styled(m_state.m_symbols[old_scope.atPos(i).first], colorize ? fmt::fg(fmt::color::cyan) : fmt::text_style()),
2316 old_scope.atPos(i).second.toString(*this));
2317 }
2318 }
2319 }
2320}
Lots of utilities about string, filesystem and more.
#define ARK_NO_NAME_FILE
Definition Constants.hpp:34
Tools to report code errors nicely to the user.
Define how dictionaries are handled.
Lots of utilities about the filesystem.
Helpers for the VM.
#define GOTO_HALT()
#define TARGET(op)
#define UNPACK_ARGS()
#define DISPATCH()
The ArkScript virtual machine.
virtual std::string details(bool colorize, VM &vm) const
Ark state to handle the dirty job of loading and compiling ArkScript code.
Definition State.hpp:38
std::vector< std::filesystem::path > m_libenv
Definition State.hpp:166
uint16_t m_features
Definition State.hpp:163
std::unordered_map< std::string, Value > m_bound
Values bound to the State, to be used by the VM.
Definition State.hpp:179
ARK_ALWAYS_INLINE constexpr uint8_t inst(const std::size_t pp, const std::size_t ip) const noexcept
Get an instruction in a given page, with a given instruction pointer.
Definition State.hpp:198
std::string m_filename
Definition State.hpp:167
std::vector< Value > m_constants
Definition State.hpp:171
std::vector< internal::InstLoc > m_inst_locations
Definition State.hpp:173
std::vector< std::string > m_filenames
Definition State.hpp:172
std::vector< std::string > m_symbols
Definition State.hpp:170
std::unique_ptr< internal::Debugger > m_debugger
Definition VM.hpp:180
void throwArityError(std::size_t passed_arg_count, std::size_t expected_arg_count, internal::ExecutionContext &context, bool skip_function=true)
Definition VM.cpp:2083
void deleteContext(internal::ExecutionContext *ec)
Free a given execution context.
Definition VM.cpp:306
ARK_ALWAYS_INLINE void returnFromFuncCall(internal::ExecutionContext &context)
Destroy the current frame and get back to the previous one, resuming execution.
void showBacktraceWithException(const std::exception &e, internal::ExecutionContext &context)
Definition VM.cpp:2150
std::vector< std::unique_ptr< internal::Future > > m_futures
Storing the promises while we are resolving them.
Definition VM.hpp:179
int m_exit_code
VM exit code, defaults to 0. Can be changed through sys:exit
Definition VM.hpp:175
Value & operator[](const std::string &name) noexcept
Retrieve a value from the virtual machine, given its symbol name.
Definition VM.cpp:138
std::vector< std::shared_ptr< internal::SharedLibrary > > m_shared_lib_objects
Definition VM.hpp:178
std::vector< std::unique_ptr< internal::ExecutionContext > > m_execution_contexts
Definition VM.hpp:174
ARK_ALWAYS_INLINE Value * peekAndResolveAsPtr(internal::ExecutionContext &context, std::size_t offset=0)
Return a pointer to the top of the stack without consuming it, and resolve it if possible.
std::string debugShowSource() const
Definition VM.cpp:2213
void listAppendInPlace(Value *list, std::size_t count, internal::ExecutionContext &context)
Definition VM.cpp:121
ARK_ALWAYS_INLINE void jump(uint16_t address, internal::ExecutionContext &context)
ARK_ALWAYS_INLINE 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,...
void unsafeRun(internal::ExecutionContext &context, std::size_t untilFrameCount=0)
Definition VM.cpp:458
uint16_t findNearestVariableIdWithValue(const Value &value, internal::ExecutionContext &context) const noexcept
Find the nearest variable id with a given value.
Definition VM.cpp:2073
ARK_ALWAYS_INLINE void setVal(uint16_t id, const Value *val, internal::ExecutionContext &context)
Change the value of a symbol given its identifier.
bool forceReloadPlugins() const
Used by the REPL to force reload all the plugins and their bound methods.
Definition VM.cpp:353
Value getField(Value *closure, uint16_t id, const internal::ExecutionContext &context, bool push_with_env=false)
Definition VM.cpp:62
internal::ExecutionContext * createAndGetContext()
Create an execution context and returns it.
Definition VM.cpp:249
std::mutex m_mutex
Definition VM.hpp:177
void loadPlugin(uint16_t id, internal::ExecutionContext &context)
Load a plugin from a constant id.
Definition VM.cpp:163
void callBuiltin(internal::ExecutionContext &context, const Value &builtin, uint16_t argc, bool remove_return_address=true, bool remove_builtin=true)
Builtin called when the CALL_BUILTIN instruction is met in the bytecode.
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:401
void initDebugger(internal::ExecutionContext &context)
Definition VM.cpp:2142
ARK_ALWAYS_INLINE void push(const Value &value, internal::ExecutionContext &context) noexcept
Push a value on the stack.
ARK_ALWAYS_INLINE Value * peek(internal::ExecutionContext &context, std::size_t offset=0)
Return a pointer to the top of the stack without consuming it.
void deleteFuture(internal::Future *f)
Free a given future.
Definition VM.cpp:342
ARK_ALWAYS_INLINE Value * popAndResolveAsPtr(internal::ExecutionContext &context)
Pop a value from the stack and resolve it if possible, then return it.
bool m_running
Definition VM.hpp:176
std::mutex m_mutex_futures
Definition VM.hpp:177
Value call(const std::string &name, Args &&... args)
Call a function from ArkScript, by giving it arguments.
ARK_ALWAYS_INLINE Value * findNearestVariable(uint16_t id, internal::ExecutionContext &context) noexcept
Find the nearest variable of a given id.
void backtrace(internal::ExecutionContext &context, std::ostream &os=std::cerr, bool colorize=true)
Display a backtrace when the VM encounter an exception.
Definition VM.cpp:2225
ARK_ALWAYS_INLINE Value * loadSymbolFromIndex(uint16_t index, internal::ExecutionContext &context)
Load a symbol by its (reversed) index in the current scope.
friend class internal::Closure
Definition VM.hpp:169
State & m_state
Definition VM.hpp:173
std::optional< internal::InstLoc > findSourceLocation(std::size_t ip, std::size_t pp) const
Find the nearest source location information given instruction and page pointers.
Definition VM.cpp:2190
Value createList(std::size_t count, internal::ExecutionContext &context)
Definition VM.cpp:109
ARK_ALWAYS_INLINE Value * pop(internal::ExecutionContext &context)
Pop a value from the stack.
void init() noexcept
Initialize the VM according to the parameters.
Definition VM.cpp:28
static void throwVMError(internal::ErrorKind kind, const std::string &message)
Throw a VM error message.
Definition VM.cpp:389
friend class Value
Definition VM.hpp:167
VM(State &state) noexcept
Construct a new vm t object.
Definition VM.cpp:22
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:329
int run(bool fail_with_exception=false)
Run the bytecode held in the state.
Definition VM.cpp:394
void usePromptFileForDebugger(const std::string &path, std::ostream &os=std::cout)
Configure the debugger to use a prompt file instead of asking the user for an input.
Definition VM.cpp:384
void exit(int code) noexcept
Ask the VM to exit with a given exit code.
Definition VM.cpp:243
ARK_ALWAYS_INLINE Value * loadConstAsPtr(uint16_t id) const
Load a constant from the constant table by its id.
ARK_ALWAYS_INLINE void store(uint16_t id, const Value *val, internal::ExecutionContext &context)
Create a new symbol with an associated value in the current scope.
const Dict_t & dict() const
Definition Value.hpp:177
const String_t & string() const
Definition Value.hpp:167
const List_t & constList() const
Definition Value.hpp:171
internal::Closure & refClosure()
Definition Value.hpp:217
String_t & stringRef()
Definition Value.hpp:168
List_t & list()
Definition Value.hpp:172
Ref_t reference() const
Definition Value.hpp:180
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:71
ValueType valueType() const noexcept
Definition Value.hpp:154
Number_t number() const
Definition Value.hpp:165
std::string toString(VM &vm, bool show_as_code=false) const noexcept
Definition Value.cpp:81
bool isIndexable() const noexcept
Definition Value.hpp:160
internal::PageAddr_t pageAddr() const
Definition Value.hpp:182
Dict_t & dictRef()
Definition Value.hpp:178
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:44
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:37
const std::shared_ptr< ClosureScope > & scopePtr() const
Definition Closure.hpp:55
void set(const Value &key, const Value &value)
Assign a key to a value inside the dict.
Definition Dict.cpp:9
std::size_t size() const
Compute the number of (key, value) pairs in the dict.
Definition Dict.cpp:40
A class to handle the VM scope more efficiently.
Definition ScopeView.hpp:27
ARK_ALWAYS_INLINE const pair_t & atPos(const std::size_t i) const noexcept
Return the element at index in scope.
ARK_ALWAYS_INLINE std::size_t size() const noexcept
Return the size of the scope.
ARK_API void makeContext(const ErrorLocation &loc, std::ostream &os, const std::optional< CodeErrorContext > &maybe_context, 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:85
ARK_ALWAYS_INLINE Value head(Value *a)
Definition Helpers.hpp:52
ARK_ALWAYS_INLINE Value at(Value &container, Value &index, VM &vm)
Definition Helpers.hpp:74
ARK_ALWAYS_INLINE double doMath(double a, double b, const Instruction op)
Definition Helpers.hpp:158
ARK_ALWAYS_INLINE Value atAt(const Value *x, const Value *y, Value &list)
Definition Helpers.hpp:120
ARK_ALWAYS_INLINE Value tail(Value *a)
Definition Helpers.hpp:23
ARK_ALWAYS_INLINE std::string mathInstToStr(const Instruction op)
Definition Helpers.hpp:176
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
Instruction
The different bytecodes are stored here.
constexpr uint16_t MaxValue16Bits
Definition Constants.hpp:81
@ Garbage
Used to signal a value was used and can/should be collected and removed from the stack.
@ Any
Used only for typechecking.
constexpr uint16_t FeatureVMDebugger
Disabled by default because embedding ArkScript should not launch the debugger on every error when ru...
Definition Constants.hpp:66
std::string to_string(const Ark::ValueType type) noexcept
Definition Value.hpp:235
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::optional< uint16_t > capture_rename_id
std::vector< ScopeView > locals
std::array< Value, VMStackSizeWithOverflowBuffer > stack
void setActive(const bool toggle)
const bool primary
Tells if the current ExecutionContext is the primary one or not.
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
A contract is a list of typed arguments that a function can follow.
A type definition within a contract.