From 6bc89dfc96f92b11771d5a0289c0d5721e6ed3a9 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Mon, 6 Jul 2026 16:47:46 +0200 Subject: [PATCH 1/3] refactor(vm): move include where they are needed --- include/Ark/VM/VM.hpp | 10 +- include/Ark/VM/VM.inl | 620 +++++++++++++++++---------------- lib/std | 2 +- src/arkreactor/VM/Debugger.cpp | 1 + src/arkreactor/VM/VM.cpp | 2 +- 5 files changed, 319 insertions(+), 316 deletions(-) diff --git a/include/Ark/VM/VM.hpp b/include/Ark/VM/VM.hpp index 0e11a882..a4f9995b 100644 --- a/include/Ark/VM/VM.hpp +++ b/include/Ark/VM/VM.hpp @@ -15,15 +15,8 @@ #include #include #include -#include -#include #include -#include -#include -#include -#include -#include #include #include #include @@ -31,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -429,8 +421,8 @@ namespace Ark */ void backtrace(internal::ExecutionContext& context, std::ostream& os = std::cerr, bool colorize = true); }; +} #include "VM.inl" -} #endif diff --git a/include/Ark/VM/VM.inl b/include/Ark/VM/VM.inl index 2e941750..49c0d937 100644 --- a/include/Ark/VM/VM.inl +++ b/include/Ark/VM/VM.inl @@ -1,383 +1,393 @@ #ifndef ARK_VM_VM_INL #define ARK_VM_VM_INL -template -Value VM::call(const std::string& name, Args&&... args) +#include +#include +#include + +#include +#include + +namespace Ark { - internal::ExecutionContext& context = *m_execution_contexts.back(); + template + Value VM::call(const std::string& name, Args&&... args) + { + internal::ExecutionContext& context = *m_execution_contexts.back(); - using namespace internal; + using namespace internal; - // reset ip and pp - context.ip = 0; - context.pp = 0; + // reset ip and pp + context.ip = 0; + context.pp = 0; - // find id of function - const auto it = std::ranges::find(m_state.m_symbols, name); - assert(it != m_state.m_symbols.end() && "Unbound variable"); + // find id of function + const auto it = std::ranges::find(m_state.m_symbols, name); + assert(it != m_state.m_symbols.end() && "Unbound variable"); - // we need to push pp then ip manually, because the PUSH_RETURN_ADDRESS instruction is not present - push(Value(static_cast(0)), context); - push(Value(ValueType::InstPtr, static_cast(0)), context); + // we need to push pp then ip manually, because the PUSH_RETURN_ADDRESS instruction is not present + push(Value(static_cast(0)), context); + push(Value(ValueType::InstPtr, static_cast(0)), context); - // find function object and push it if it's a pageaddr/closure - if (const auto dist = std::distance(m_state.m_symbols.begin(), it); std::cmp_less(dist, MaxValue16Bits)) - { - const uint16_t id = static_cast(dist); - Value* var = findNearestVariable(id, context); - assert(var != nullptr && "Couldn't find variable"); + // find function object and push it if it's a pageaddr/closure + if (const auto dist = std::distance(m_state.m_symbols.begin(), it); std::cmp_less(dist, MaxValue16Bits)) + { + const uint16_t id = static_cast(dist); + Value* var = findNearestVariable(id, context); + assert(var != nullptr && "Couldn't find variable"); - if (!var->isFunction()) - throwVMError(ErrorKind::Type, fmt::format("Can't call '{}': it isn't a Function but a {}", name, std::to_string(var->valueType()))); + if (!var->isFunction()) + throwVMError(ErrorKind::Type, fmt::format("Can't call '{}': it isn't a Function but a {}", name, std::to_string(var->valueType()))); - push(Value(var), context); - context.last_symbol = id; - } - // convert and push arguments - if (sizeof...(args) > 0) - { - std::vector fnargs { { Value(std::forward(args))... } }; - for (auto&& arg : fnargs) - push(arg, context); - } + push(Value(var), context); + context.last_symbol = id; + } + // convert and push arguments + if (sizeof...(args) > 0) + { + std::vector fnargs { { Value(std::forward(args))... } }; + for (auto&& arg : fnargs) + push(arg, context); + } - const std::size_t frames_count = context.fc; - // call it - call(context, static_cast(sizeof...(Args))); - // reset instruction pointer, otherwise the safeRun method will start at ip = -1 - // without doing context.ip++ as intended (done right after the call() in the loop, but here - // we start outside this loop) - context.ip = 0; + const std::size_t frames_count = context.fc; + // call it + call(context, static_cast(sizeof...(Args))); + // reset instruction pointer, otherwise the safeRun method will start at ip = -1 + // without doing context.ip++ as intended (done right after the call() in the loop, but here + // we start outside this loop) + context.ip = 0; - // run until the function returns - safeRun(context, /* untilFrameCount */ frames_count); + // run until the function returns + safeRun(context, /* untilFrameCount */ frames_count); - // get result - return *popAndResolveAsPtr(context); -} + // get result + return *popAndResolveAsPtr(context); + } -inline Value VM::resolve(internal::ExecutionContext* context, const std::vector& n) -{ - if (!n[0].isFunction()) - throw TypeError(fmt::format("VM::resolve couldn't resolve a non-function ({})", std::to_string(n[0].valueType()))); - - const std::size_t ip = context->ip; - const std::size_t pp = context->pp; - - // we need to push pp then ip manually, because the PUSH_RETURN_ADDRESS instruction is not present - push(Value(static_cast(pp)), *context); - push(Value(ValueType::InstPtr, static_cast(ip)), *context); - - // push the function and all its args in the order they were given to us - // the function must go first! - for (auto&& val : n) - push(val, *context); - - const std::size_t frames_count = context->fc; - // call it - call(*context, static_cast(n.size() - 1)); - // reset instruction pointer, otherwise the safeRun method will start at ip = -1 - // without doing context.ip++ as intended (done right after the call() in the loop, but here - // we start outside this loop) - context->ip = 0; - - // run until the function returns - safeRun(*context, /* untilFrameCount */ frames_count); - - // restore VM state - context->ip = ip; - context->pp = pp; - - // get result - return *popAndResolveAsPtr(*context); -} + inline Value VM::resolve(internal::ExecutionContext* context, const std::vector& n) + { + if (!n[0].isFunction()) + throw TypeError(fmt::format("VM::resolve couldn't resolve a non-function ({})", std::to_string(n[0].valueType()))); + + const std::size_t ip = context->ip; + const std::size_t pp = context->pp; + + // we need to push pp then ip manually, because the PUSH_RETURN_ADDRESS instruction is not present + push(Value(static_cast(pp)), *context); + push(Value(ValueType::InstPtr, static_cast(ip)), *context); + + // push the function and all its args in the order they were given to us + // the function must go first! + for (auto&& val : n) + push(val, *context); + + const std::size_t frames_count = context->fc; + // call it + call(*context, static_cast(n.size() - 1)); + // reset instruction pointer, otherwise the safeRun method will start at ip = -1 + // without doing context.ip++ as intended (done right after the call() in the loop, but here + // we start outside this loop) + context->ip = 0; + + // run until the function returns + safeRun(*context, /* untilFrameCount */ frames_count); + + // restore VM state + context->ip = ip; + context->pp = pp; + + // get result + return *popAndResolveAsPtr(*context); + } #pragma region "instruction helpers" -inline ARK_ALWAYS_INLINE Value* VM::loadSymbol(const uint16_t id, internal::ExecutionContext& context) -{ - context.last_symbol = id; - if (Value* var = findNearestVariable(context.last_symbol, context); var != nullptr) [[likely]] + inline ARK_ALWAYS_INLINE Value* VM::loadSymbol(const uint16_t id, internal::ExecutionContext& context) { - // push internal reference, shouldn't break anything so far, unless it's already a ref - if (var->valueType() == ValueType::Reference) - return var->reference(); - return var; + context.last_symbol = id; + if (Value* var = findNearestVariable(context.last_symbol, context); var != nullptr) [[likely]] + { + // push internal reference, shouldn't break anything so far, unless it's already a ref + if (var->valueType() == ValueType::Reference) + return var->reference(); + return var; + } + else [[unlikely]] + throwVMError(internal::ErrorKind::Scope, fmt::format("Unbound variable `{}'", m_state.m_symbols[context.last_symbol])); + return nullptr; } - else [[unlikely]] - throwVMError(internal::ErrorKind::Scope, fmt::format("Unbound variable `{}'", m_state.m_symbols[context.last_symbol])); - return nullptr; -} -inline ARK_ALWAYS_INLINE Value* VM::loadSymbolFromIndex(const uint16_t index, internal::ExecutionContext& context) -{ - // we need to load symbols from the end, because function calls add a reference to the current function - // upon calling it. Which changes the index by 1, making it less clear, because it needs special - // treatment only for function calls. - auto& [id, value] = context.locals.back().atPosReverse(index); - context.last_symbol = id; - if (value.valueType() == ValueType::Reference) - return value.reference(); - return &value; -} + inline ARK_ALWAYS_INLINE Value* VM::loadSymbolFromIndex(const uint16_t index, internal::ExecutionContext& context) + { + // we need to load symbols from the end, because function calls add a reference to the current function + // upon calling it. Which changes the index by 1, making it less clear, because it needs special + // treatment only for function calls. + auto& [id, value] = context.locals.back().atPosReverse(index); + context.last_symbol = id; + if (value.valueType() == ValueType::Reference) + return value.reference(); + return &value; + } -inline ARK_ALWAYS_INLINE Value* VM::loadConstAsPtr(const uint16_t id) const -{ - return &m_state.m_constants[id]; -} + inline ARK_ALWAYS_INLINE Value* VM::loadConstAsPtr(const uint16_t id) const + { + return &m_state.m_constants[id]; + } -inline ARK_ALWAYS_INLINE Value* VM::findNearestVariable(const uint16_t id, internal::ExecutionContext& context) noexcept -{ - for (auto it = context.locals.rbegin(), it_end = context.locals.rend(); it != it_end; ++it) + inline ARK_ALWAYS_INLINE Value* VM::findNearestVariable(const uint16_t id, internal::ExecutionContext& context) noexcept { - if (Value* val = (*it)[id]; val != nullptr) - return val; + for (auto it = context.locals.rbegin(), it_end = context.locals.rend(); it != it_end; ++it) + { + if (Value* val = (*it)[id]; val != nullptr) + return val; + } + return nullptr; } - return nullptr; -} -inline ARK_ALWAYS_INLINE void VM::store(const uint16_t id, const Value* val, internal::ExecutionContext& context) -{ - // avoid adding the pair (id, _) multiple times, with different values - Value* local = context.locals.back()[id]; - if (local == nullptr) [[likely]] - context.locals.back().pushBack(id, *val); - else - *local = *val; -} + inline ARK_ALWAYS_INLINE void VM::store(const uint16_t id, const Value* val, internal::ExecutionContext& context) + { + // avoid adding the pair (id, _) multiple times, with different values + Value* local = context.locals.back()[id]; + if (local == nullptr) [[likely]] + context.locals.back().pushBack(id, *val); + else + *local = *val; + } -inline ARK_ALWAYS_INLINE void VM::setVal(const uint16_t id, const Value* val, internal::ExecutionContext& context) -{ - if (Value* var = findNearestVariable(id, context); var != nullptr) [[likely]] + inline ARK_ALWAYS_INLINE void VM::setVal(const uint16_t id, const Value* val, internal::ExecutionContext& context) { - if (var->valueType() == ValueType::Reference) - *var->reference() = *val; - else [[likely]] - *var = *val; + if (Value* var = findNearestVariable(id, context); var != nullptr) [[likely]] + { + if (var->valueType() == ValueType::Reference) + *var->reference() = *val; + else [[likely]] + *var = *val; + } + else + throwVMError( + internal::ErrorKind::Scope, + fmt::format( + "Unbound variable `{}', can not change its value to {}", + m_state.m_symbols[id], + val->toString(*this))); } - else - throwVMError( - internal::ErrorKind::Scope, - fmt::format( - "Unbound variable `{}', can not change its value to {}", - m_state.m_symbols[id], - val->toString(*this))); -} -inline ARK_ALWAYS_INLINE void VM::jump(const uint16_t address, internal::ExecutionContext& context) -{ - // instructions are on 4 bytes! - context.ip = address * 4; -} + inline ARK_ALWAYS_INLINE void VM::jump(const uint16_t address, internal::ExecutionContext& context) + { + // instructions are on 4 bytes! + context.ip = address * 4; + } #pragma endregion #pragma region "stack management" -inline ARK_ALWAYS_INLINE Value* VM::pop(internal::ExecutionContext& context) -{ - if (context.sp > 0) [[likely]] + inline ARK_ALWAYS_INLINE Value* VM::pop(internal::ExecutionContext& context) { - --context.sp; - return &context.stack[context.sp]; + if (context.sp > 0) [[likely]] + { + --context.sp; + return &context.stack[context.sp]; + } + return &m_undefined_value; } - return &m_undefined_value; -} -inline ARK_ALWAYS_INLINE Value* VM::peek(internal::ExecutionContext& context, const std::size_t offset) -{ - if (context.sp > offset) + inline ARK_ALWAYS_INLINE Value* VM::peek(internal::ExecutionContext& context, const std::size_t offset) { - Value* tmp = &context.stack[context.sp - 1 - offset]; - return tmp; + if (context.sp > offset) + { + Value* tmp = &context.stack[context.sp - 1 - offset]; + return tmp; + } + return &m_undefined_value; } - return &m_undefined_value; -} -inline ARK_ALWAYS_INLINE Value* VM::peekAndResolveAsPtr(internal::ExecutionContext& context, const std::size_t offset) -{ - if (context.sp > offset) + inline ARK_ALWAYS_INLINE Value* VM::peekAndResolveAsPtr(internal::ExecutionContext& context, const std::size_t offset) { - Value* tmp = &context.stack[context.sp - 1 - offset]; - if (tmp->valueType() == ValueType::Reference) - return tmp->reference(); - return tmp; + if (context.sp > offset) + { + Value* tmp = &context.stack[context.sp - 1 - offset]; + if (tmp->valueType() == ValueType::Reference) + return tmp->reference(); + return tmp; + } + return &m_undefined_value; } - return &m_undefined_value; -} -inline ARK_ALWAYS_INLINE void VM::push(const Value& value, internal::ExecutionContext& context) noexcept -{ - context.stack[context.sp] = value; - ++context.sp; -} + inline ARK_ALWAYS_INLINE void VM::push(const Value& value, internal::ExecutionContext& context) noexcept + { + context.stack[context.sp] = value; + ++context.sp; + } -inline ARK_ALWAYS_INLINE void VM::push(Value&& value, internal::ExecutionContext& context) noexcept -{ - context.stack[context.sp] = std::move(value); - ++context.sp; -} + inline ARK_ALWAYS_INLINE void VM::push(Value&& value, internal::ExecutionContext& context) noexcept + { + context.stack[context.sp] = std::move(value); + ++context.sp; + } -inline ARK_ALWAYS_INLINE void VM::push(Value* valptr, internal::ExecutionContext& context) noexcept -{ - context.stack[context.sp].m_type = ValueType::Reference; - context.stack[context.sp].m_value = valptr; - ++context.sp; -} + inline ARK_ALWAYS_INLINE void VM::push(Value* valptr, internal::ExecutionContext& context) noexcept + { + context.stack[context.sp].m_type = ValueType::Reference; + context.stack[context.sp].m_value = valptr; + ++context.sp; + } -inline ARK_ALWAYS_INLINE Value* VM::popAndResolveAsPtr(internal::ExecutionContext& context) -{ - Value* tmp = pop(context); - if (tmp->valueType() == ValueType::Reference) - return tmp->reference(); - return tmp; -} + inline ARK_ALWAYS_INLINE Value* VM::popAndResolveAsPtr(internal::ExecutionContext& context) + { + Value* tmp = pop(context); + if (tmp->valueType() == ValueType::Reference) + return tmp->reference(); + return tmp; + } #pragma endregion -inline ARK_ALWAYS_INLINE void VM::returnFromFuncCall(internal::ExecutionContext& context) -{ - --context.fc; - context.stacked_closure_scopes.pop_back(); - context.locals.pop_back(); -} - -inline void VM::call(internal::ExecutionContext& context, const uint16_t argc, Value* function_ptr, const internal::PageAddr_t or_address) -{ - using namespace internal; - - if (std::cmp_greater_equal(context.sp + 2, VMStackSize)) [[unlikely]] - throwVMError( - ErrorKind::VM, - fmt::format( - "Maximum recursion depth exceeded. You could consider rewriting your function `{}' to make use of tail-call optimization.", - m_state.m_symbols[context.last_symbol])); - - ValueType call_type; - PageAddr_t page_addr = 0; - Value* maybe_value_ptr = nullptr; - - if (function_ptr == nullptr && or_address == 0) - maybe_value_ptr = peekAndResolveAsPtr(context, argc); - else if (or_address != 0) + inline ARK_ALWAYS_INLINE void VM::returnFromFuncCall(internal::ExecutionContext& context) { - call_type = ValueType::PageAddr; - page_addr = or_address; + --context.fc; + context.stacked_closure_scopes.pop_back(); + context.locals.pop_back(); } - else - maybe_value_ptr = function_ptr; - if (maybe_value_ptr != nullptr) + inline void VM::call(internal::ExecutionContext& context, const uint16_t argc, Value* function_ptr, const internal::PageAddr_t or_address) { - call_type = maybe_value_ptr->valueType(); - if (call_type == ValueType::PageAddr) - page_addr = maybe_value_ptr->pageAddr(); - } + using namespace internal; + + if (std::cmp_greater_equal(context.sp + 2, VMStackSize)) [[unlikely]] + throwVMError( + ErrorKind::VM, + fmt::format( + "Maximum recursion depth exceeded. You could consider rewriting your function `{}' to make use of tail-call optimization.", + m_state.m_symbols[context.last_symbol])); - context.stacked_closure_scopes.emplace_back(nullptr); + ValueType call_type; + PageAddr_t page_addr = 0; + Value* maybe_value_ptr = nullptr; - switch (call_type) - { - // is it a builtin function name? - case ValueType::CProc: + if (function_ptr == nullptr && or_address == 0) + maybe_value_ptr = peekAndResolveAsPtr(context, argc); + else if (or_address != 0) { - // We need to remove the builtin from the stack if we came from a standard CALL instruction. - // We know we came from a CALL instruction if function_ptr is null, since only CALL_SYMBOL supplies it. - callBuiltin(context, *maybe_value_ptr, argc, /* remove_return_address= */ true, /* remove_builtin= */ function_ptr == nullptr); - return; + call_type = ValueType::PageAddr; + page_addr = or_address; } + else + maybe_value_ptr = function_ptr; - // is it a user defined function? - case ValueType::PageAddr: + if (maybe_value_ptr != nullptr) { - // create dedicated scope - context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd()); - // set up pointers (frame counter, page, instruction) - context.fc++; - context.pp = page_addr; - context.ip = 0; - break; + call_type = maybe_value_ptr->valueType(); + if (call_type == ValueType::PageAddr) + page_addr = maybe_value_ptr->pageAddr(); } - // is it a user defined closure? - case ValueType::Closure: + context.stacked_closure_scopes.emplace_back(nullptr); + + switch (call_type) { - const Closure& c = maybe_value_ptr->closure(); - const PageAddr_t new_page_pointer = c.pageAddr(); - - // create dedicated scope - context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd()); - // load saved scope - c.refScope().mergeRefInto(context.locals.back()); - context.stacked_closure_scopes.back() = c.scopePtr(); - - context.fc++; - context.pp = new_page_pointer; - context.ip = 0; - break; + // is it a builtin function name? + case ValueType::CProc: + { + // We need to remove the builtin from the stack if we came from a standard CALL instruction. + // We know we came from a CALL instruction if function_ptr is null, since only CALL_SYMBOL supplies it. + callBuiltin(context, *maybe_value_ptr, argc, /* remove_return_address= */ true, /* remove_builtin= */ function_ptr == nullptr); + return; + } + + // is it a user defined function? + case ValueType::PageAddr: + { + // create dedicated scope + context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd()); + // set up pointers (frame counter, page, instruction) + context.fc++; + context.pp = page_addr; + context.ip = 0; + break; + } + + // is it a user defined closure? + case ValueType::Closure: + { + const Closure& c = maybe_value_ptr->closure(); + const PageAddr_t new_page_pointer = c.pageAddr(); + + // create dedicated scope + context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd()); + // load saved scope + c.refScope().mergeRefInto(context.locals.back()); + context.stacked_closure_scopes.back() = c.scopePtr(); + + context.fc++; + context.pp = new_page_pointer; + context.ip = 0; + break; + } + + default: + { + throwVMError( + ErrorKind::Type, + fmt::format( + "{} is not a Function but a {}", + maybe_value_ptr->toString(*this, /* show_as_code= */ true), std::to_string(call_type))); + } } - default: + if (function_ptr == nullptr && or_address == 0) + // so that RET knows it can collect it + peek(context, argc)->m_type = ValueType::Garbage; + + // checking function arity + std::size_t index = 0, + needed_argc = 0; + + // every argument is a MUT declaration in the bytecode + while (m_state.inst(context.pp, index) == STORE || + m_state.inst(context.pp, index) == STORE_REF) { - throwVMError( - ErrorKind::Type, - fmt::format( - "{} is not a Function but a {}", - maybe_value_ptr->toString(*this, /* show_as_code= */ true), std::to_string(call_type))); + needed_argc += 1; + index += 4; // instructions are on 4 bytes } - } - if (function_ptr == nullptr && or_address == 0) - // so that RET knows it can collect it - peek(context, argc)->m_type = ValueType::Garbage; - - // checking function arity - std::size_t index = 0, - needed_argc = 0; + // no store? check for CALL_BUILTIN_WITHOUT_RETURN_ADDRESS + if (index == 0 && m_state.inst(context.pp, 0) == CALL_BUILTIN_WITHOUT_RETURN_ADDRESS) + { + const uint8_t padding = m_state.inst(context.pp, context.ip + 1); + const uint16_t arg = static_cast((m_state.inst(context.pp, context.ip + 2) << 8) + + m_state.inst(context.pp, context.ip + 3)); + needed_argc = static_cast((padding << 4) | (arg & 0xf000) >> 12); + } - // every argument is a MUT declaration in the bytecode - while (m_state.inst(context.pp, index) == STORE || - m_state.inst(context.pp, index) == STORE_REF) - { - needed_argc += 1; - index += 4; // instructions are on 4 bytes + if (std::cmp_not_equal(needed_argc, argc)) [[unlikely]] + throwArityError(argc, needed_argc, context, /* skip_function= */ function_ptr == nullptr); } - // no store? check for CALL_BUILTIN_WITHOUT_RETURN_ADDRESS - if (index == 0 && m_state.inst(context.pp, 0) == CALL_BUILTIN_WITHOUT_RETURN_ADDRESS) + inline void VM::callBuiltin(internal::ExecutionContext& context, const Value& builtin, const uint16_t argc, const bool remove_return_address, const bool remove_builtin) { - const uint8_t padding = m_state.inst(context.pp, context.ip + 1); - const uint16_t arg = static_cast((m_state.inst(context.pp, context.ip + 2) << 8) + - m_state.inst(context.pp, context.ip + 3)); - needed_argc = static_cast((padding << 4) | (arg & 0xf000) >> 12); - } + using namespace Ark::literals; - if (std::cmp_not_equal(needed_argc, argc)) [[unlikely]] - throwArityError(argc, needed_argc, context, /* skip_function= */ function_ptr == nullptr); -} - -inline void VM::callBuiltin(internal::ExecutionContext& context, const Value& builtin, const uint16_t argc, const bool remove_return_address, const bool remove_builtin) -{ - using namespace Ark::literals; - - // drop arguments from the stack - std::vector args; - args.reserve(argc); + // drop arguments from the stack + std::vector args; + args.reserve(argc); - for (uint16_t j = 0; j < argc; ++j) - { - // because we pull `argc` from the CALL instruction generated by the compiler, - // we are guaranteed to have `argc` values pushed on the stack ; thus we can - // skip the `if (context.sp > 0)` check - Value* val = &context.stack[context.sp - argc + j]; - if (val->valueType() == ValueType::Reference) - val = val->reference(); - args.emplace_back(*val); + for (uint16_t j = 0; j < argc; ++j) + { + // because we pull `argc` from the CALL instruction generated by the compiler, + // we are guaranteed to have `argc` values pushed on the stack ; thus we can + // skip the `if (context.sp > 0)` check + Value* val = &context.stack[context.sp - argc + j]; + if (val->valueType() == ValueType::Reference) + val = val->reference(); + args.emplace_back(*val); + } + // +2 to skip PP/IP that were pushed by PUSH_RETURN_ADDRESS + context.sp -= static_cast(argc + (remove_return_address ? 2_u16 : 0_u16) + (remove_builtin ? 1_u16 : 0_u16)); + // call proc + push(builtin.proc()(args, this), context); } - // +2 to skip PP/IP that were pushed by PUSH_RETURN_ADDRESS - context.sp -= static_cast(argc + (remove_return_address ? 2_u16 : 0_u16) + (remove_builtin ? 1_u16 : 0_u16)); - // call proc - push(builtin.proc()(args, this), context); } #endif diff --git a/lib/std b/lib/std index 163bb052..f840fae6 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit 163bb052efc39fc2ab60a319c9659c6bc36990ef +Subproject commit f840fae66437b3dd894f6218bf649ef2c001e558 diff --git a/src/arkreactor/VM/Debugger.cpp b/src/arkreactor/VM/Debugger.cpp index 1afc5b55..17d6f5d9 100644 --- a/src/arkreactor/VM/Debugger.cpp +++ b/src/arkreactor/VM/Debugger.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include diff --git a/src/arkreactor/VM/VM.cpp b/src/arkreactor/VM/VM.cpp index 6b567d3f..8407b61d 100644 --- a/src/arkreactor/VM/VM.cpp +++ b/src/arkreactor/VM/VM.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -11,7 +12,6 @@ #include #include #include -#include #include #include From 37641ceb81cfdb67fb7debcab17b08f58ca20ea0 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Mon, 6 Jul 2026 18:14:09 +0200 Subject: [PATCH 2/3] chore: add missing include for std::get_time on debian --- src/arkreactor/Builtins/Time.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/arkreactor/Builtins/Time.cpp b/src/arkreactor/Builtins/Time.cpp index 5554b4a9..45dc7c2c 100644 --- a/src/arkreactor/Builtins/Time.cpp +++ b/src/arkreactor/Builtins/Time.cpp @@ -6,6 +6,7 @@ #undef abs #include +#include #include From 8fc112058ea8e42c47c3f4a1a3627c7e372e6019 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Mon, 6 Jul 2026 17:02:49 +0200 Subject: [PATCH 3/3] chore(ci): ignore leaks when copying a ClosureScope --- .github/workflows/ci.yml | 8 +++++++- lsan-suppressions.txt | 2 ++ valgrind-suppressions.txt | 12 ++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 lsan-suppressions.txt create mode 100644 valgrind-suppressions.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73ee81af..c4e9d69c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -461,14 +461,16 @@ jobs: - name: Run tests run: | + # we are in /home/runner/work/Ark/Ark/build/ export ASAN_OPTIONS=detect_odr_violation=0 + export LSAN_OPTIONS=suppressions=../lsan-suppressions.txt ./build/unittests - name: Gather coverage run: cmake --build build --target coverage - name: Coveralls - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@main with: path-to-lcov: build/coverage.info github-token: ${{ secrets.GITHUB_TOKEN }} @@ -503,6 +505,7 @@ jobs: shell: bash run: | export ASAN_OPTIONS=detect_odr_violation=0 + export LSAN_OPTIONS=suppressions=lsan-suppressions.txt ./unittests - uses: actions/setup-python@v6 @@ -515,6 +518,7 @@ jobs: shell: bash run: | export ASAN_OPTIONS=detect_odr_violation=0 + export LSAN_OPTIONS=suppressions=lsan-suppressions.txt chmod u+x ./lib/modules/.github/run-tests (./lib/modules/.github/run-tests src) @@ -695,4 +699,6 @@ jobs: --verbose -s \ --demangle=yes \ --error-exitcode=1 \ + --suppressions=valgrind-suppressions.txt \ + --gen-suppressions=all \ build/arkscript -L ./lib tests/unittests/resources/LangSuite/unittests.ark valgrind diff --git a/lsan-suppressions.txt b/lsan-suppressions.txt new file mode 100644 index 00000000..9d4ee5ae --- /dev/null +++ b/lsan-suppressions.txt @@ -0,0 +1,2 @@ +# ArkScript closures that contain references to themselves are seen as leaky +leak:Ark::internal::ClosureScope::ClosureScope(Ark::internal::ClosureScope const&) diff --git a/valgrind-suppressions.txt b/valgrind-suppressions.txt new file mode 100644 index 00000000..f982e832 --- /dev/null +++ b/valgrind-suppressions.txt @@ -0,0 +1,12 @@ +{ + ignore_closure_leak + Memcheck:Leak + ... + fun:_ZN3Ark8internal7ClosureC1ERKNS0_12ClosureScopeEt +} +{ + ignore_closure_leak_2 + Memcheck:Leak + ... + fun:construct +}