diff --git a/lib/bytecode_parser/CMakeLists.txt b/lib/bytecode_parser/CMakeLists.txt index 2c4e6f8..9ee368e 100644 --- a/lib/bytecode_parser/CMakeLists.txt +++ b/lib/bytecode_parser/CMakeLists.txt @@ -9,7 +9,6 @@ add_library(bytecode_parser STATIC scenarios/VtableParser.cpp scenarios/CommandFactory.cpp scenarios/FunctionFactory.cpp - scenarios/PlaceholderCommandFactory.cpp ) diff --git a/lib/bytecode_parser/ParsingSession.cpp b/lib/bytecode_parser/ParsingSession.cpp index 967f42f..b1b4b65 100644 --- a/lib/bytecode_parser/ParsingSession.cpp +++ b/lib/bytecode_parser/ParsingSession.cpp @@ -1,6 +1,8 @@ #include "ParsingSession.hpp" #include +#include +#include namespace ovum::bytecode::parser { @@ -118,7 +120,8 @@ std::expected ParsingSession::ConsumeStringLit std::to_string(token->GetPosition().GetColumn()))); } - std::string value = Current()->GetLexeme(); + std::string value = + dynamic_cast(dynamic_cast(Current().get())->GetValue())->ToString(); value = value.substr(1, value.length() - 2); diff --git a/lib/bytecode_parser/scenarios/CommandFactory.cpp b/lib/bytecode_parser/scenarios/CommandFactory.cpp index 1566ec7..ecfc1c4 100644 --- a/lib/bytecode_parser/scenarios/CommandFactory.cpp +++ b/lib/bytecode_parser/scenarios/CommandFactory.cpp @@ -6,7 +6,8 @@ namespace ovum::bytecode::parser { -const std::unordered_set CommandFactory::kStringCommands = {"PushString", "PushChar"}; +const std::unordered_set CommandFactory::kStringCommands = {"PushString"}; +const std::unordered_set CommandFactory::kCharCommands = {"PushChar"}; const std::unordered_set CommandFactory::kIntegerCommands = { "PushInt", "PushByte", "Rotate", "LoadLocal", "SetLocal", "LoadStatic", "SetStatic", "GetField", "SetField"}; @@ -16,7 +17,7 @@ const std::unordered_set CommandFactory::kFloatCommands = {"PushFlo const std::unordered_set CommandFactory::kBooleanCommands = {"PushBool"}; const std::unordered_set CommandFactory::kIdentCommands = { - "NewArray", "Call", "CallVirtual", "CallConstructor", "GetVTable", "SetVTable", "SafeCall", "IsType", "SizeOf"}; + "Call", "CallVirtual", "CallConstructor", "GetVTable", "SetVTable", "SafeCall", "IsType", "SizeOf"}; std::expected, BytecodeParserError> CommandFactory::CreateCommand( const std::string& cmd_name, std::shared_ptr ctx) const { @@ -105,6 +106,23 @@ std::expected, BytecodeParserEr return std::move(cmd.value()); } + if (kCharCommands.contains(cmd_name)) { + std::expected value = ctx->ConsumeIntLiteral(); + + if (!value) { + return std::unexpected(value.error()); + } + + std::expected, std::out_of_range> cmd = + vm::execution_tree::CreateIntegerCommandByName(cmd_name, value.value()); + + if (!cmd) { + return std::unexpected(BytecodeParserError("Failed to create char command: " + cmd_name)); + } + + return std::move(cmd.value()); + } + std::expected, std::out_of_range> cmd = vm::execution_tree::CreateSimpleCommandByName(cmd_name); diff --git a/lib/bytecode_parser/scenarios/CommandFactory.hpp b/lib/bytecode_parser/scenarios/CommandFactory.hpp index 50c7d72..b38c69c 100644 --- a/lib/bytecode_parser/scenarios/CommandFactory.hpp +++ b/lib/bytecode_parser/scenarios/CommandFactory.hpp @@ -27,6 +27,7 @@ class CommandFactory : public ICommandFactory { static const std::unordered_set kFloatCommands; static const std::unordered_set kBooleanCommands; static const std::unordered_set kIdentCommands; + static const std::unordered_set kCharCommands; }; } // namespace ovum::bytecode::parser diff --git a/lib/bytecode_parser/scenarios/PlaceholderCommandFactory.cpp b/lib/bytecode_parser/scenarios/PlaceholderCommandFactory.cpp deleted file mode 100644 index c75baae..0000000 --- a/lib/bytecode_parser/scenarios/PlaceholderCommandFactory.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "PlaceholderCommandFactory.hpp" - -#include - -#include "lib/execution_tree/Command.hpp" - -namespace ovum::bytecode::parser { - -namespace { -const std::unordered_set kStringCommands = {"PushString", "PushChar"}; - -const std::unordered_set kIntegerCommands = { - "PushInt", "PushByte", "Rotate", "LoadLocal", "SetLocal", "LoadStatic", "SetStatic", "GetField", "SetField"}; - -const std::unordered_set kFloatCommands = {"PushFloat"}; - -const std::unordered_set kBooleanCommands = {"PushBool"}; - -const std::unordered_set kIdentCommands = { - "NewArray", "Call", "CallVirtual", "CallConstructor", "GetVTable", "SetVTable", "SafeCall", "IsType", "SizeOf"}; - -static const auto kStubCommand = [](const std::string& full_name) -> std::unique_ptr { - auto cmd_func = [full_name](vm::execution_tree::PassedExecutionData&) - -> std::expected { - return std::unexpected(std::runtime_error("Command not implemented: " + full_name)); - }; - return std::make_unique>(std::move(cmd_func)); -}; -} // namespace - -std::expected, BytecodeParserError> -PlaceholderCommandFactory::CreateCommand(const std::string& cmd_name, std::shared_ptr ctx) const { - // Consume arguments based on command type (same logic as CommandFactory) - // This ensures the parser advances correctly through tokens - if (kStringCommands.contains(cmd_name)) { - std::expected value = ctx->ConsumeStringLiteral(); - if (!value) { - return std::unexpected(value.error()); - } - return kStubCommand(cmd_name); - } - - if (kIntegerCommands.contains(cmd_name)) { - std::expected value = ctx->ConsumeIntLiteral(); - if (!value) { - return std::unexpected(value.error()); - } - return kStubCommand(cmd_name); - } - - if (kFloatCommands.contains(cmd_name)) { - std::expected value = ctx->ConsumeFloatLiteral(); - if (!value) { - return std::unexpected(value.error()); - } - return kStubCommand(cmd_name); - } - - if (kBooleanCommands.contains(cmd_name)) { - std::expected value = ctx->ConsumeBoolLiteral(); - if (!value) { - return std::unexpected(value.error()); - } - return kStubCommand(cmd_name); - } - - if (kIdentCommands.contains(cmd_name)) { - std::expected ident = ctx->ConsumeIdentifier(); - if (!ident) { - return std::unexpected(ident.error()); - } - return kStubCommand(cmd_name); - } - - // Simple commands (no arguments) - return kStubCommand(cmd_name); -} - -} // namespace ovum::bytecode::parser diff --git a/lib/bytecode_parser/scenarios/PlaceholderCommandFactory.hpp b/lib/bytecode_parser/scenarios/PlaceholderCommandFactory.hpp deleted file mode 100644 index 802664b..0000000 --- a/lib/bytecode_parser/scenarios/PlaceholderCommandFactory.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef BYTECODE_PARSER_PLACEHOLDERCOMMANDFACTORY_HPP_ -#define BYTECODE_PARSER_PLACEHOLDERCOMMANDFACTORY_HPP_ - -#include -#include -#include - -#include "ICommandFactory.hpp" -#include "lib/execution_tree/IExecutable.hpp" - -namespace ovum::bytecode::parser { - -class PlaceholderCommandFactory : public ICommandFactory { -public: - std::expected, BytecodeParserError> CreateCommand( - const std::string& cmd_name, std::shared_ptr ctx) const override; -}; - -} // namespace ovum::bytecode::parser - -#endif // BYTECODE_PARSER_PLACEHOLDERCOMMANDFACTORY_HPP_ diff --git a/lib/execution_tree/BytecodeCommands.cpp b/lib/execution_tree/BytecodeCommands.cpp index b059ea7..c8a540e 100644 --- a/lib/execution_tree/BytecodeCommands.cpp +++ b/lib/execution_tree/BytecodeCommands.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -21,8 +20,17 @@ #ifdef _WIN32 #include + +#include +#elif __APPLE__ +#include +#include + +#include +#include #else #include +#include #include #endif @@ -109,10 +117,8 @@ std::expected PushString(PassedExecutionDat return std::unexpected(vtable_index_result.error()); } - auto string_obj_result = runtime::AllocateObject(*string_vtable, - static_cast(vtable_index_result.value()), - data.memory.object_repository, - data.allocator); + auto string_obj_result = + data.memory_manager.AllocateObject(*string_vtable, static_cast(vtable_index_result.value()), data); if (!string_obj_result.has_value()) { return std::unexpected(string_obj_result.error()); @@ -140,8 +146,8 @@ std::expected PushNull(PassedExecutionData& return std::unexpected(vtable_index_result.error()); } - auto null_obj_result = runtime::AllocateObject( - *null_vtable, static_cast(vtable_index_result.value()), data.memory.object_repository, data.allocator); + auto null_obj_result = + data.memory_manager.AllocateObject(*null_vtable, static_cast(vtable_index_result.value()), data); if (!null_obj_result.has_value()) { return std::unexpected(null_obj_result.error()); @@ -1211,8 +1217,7 @@ std::expected CallConstructor(PassedExecuti return std::unexpected(vtable.error()); } - auto obj_ptr = - runtime::AllocateObject(*vtable.value(), vtable_idx.value(), data.memory.object_repository, data.allocator); + auto obj_ptr = data.memory_manager.AllocateObject(*vtable.value(), vtable_idx.value(), data); if (!obj_ptr) { return std::unexpected(obj_ptr.error()); @@ -1452,7 +1457,7 @@ std::expected NullCoalesce(PassedExecutionD if (*tested_result_data != nullptr) { data.memory.machine_stack.pop(); - data.memory.machine_stack.emplace(tested_result.value()); + data.memory.machine_stack.emplace(*tested_result_data); } return ExecutionResult::kNormal; @@ -1602,10 +1607,9 @@ std::expected FormatDateTime(PassedExecutio return std::unexpected(vtable_index_result.error()); } - auto string_obj_result = runtime::AllocateObject(*string_vtable, - static_cast(vtable_index_result.value()), - data.memory.object_repository, - data.allocator); + auto string_obj_result = + data.memory_manager.AllocateObject(*string_vtable, static_cast(vtable_index_result.value()), data); + if (!string_obj_result.has_value()) { return std::unexpected(string_obj_result.error()); } @@ -1659,8 +1663,9 @@ std::expected ParseDateTime(PassedExecution return std::unexpected(vtable_index_result.error()); } - auto int_obj_result = runtime::AllocateObject( - *int_vtable, static_cast(vtable_index_result.value()), data.memory.object_repository, data.allocator); + auto int_obj_result = + data.memory_manager.AllocateObject(*int_vtable, static_cast(vtable_index_result.value()), data); + if (!int_obj_result.has_value()) { return std::unexpected(int_obj_result.error()); } @@ -1821,10 +1826,8 @@ std::expected ListDir(PassedExecutionData& return std::unexpected(vtable_index_result.error()); } - auto string_array_obj_result = runtime::AllocateObject(*string_array_vtable, - static_cast(vtable_index_result.value()), - data.memory.object_repository, - data.allocator); + auto string_array_obj_result = data.memory_manager.AllocateObject( + *string_array_vtable, static_cast(vtable_index_result.value()), data); if (!string_array_obj_result.has_value()) { return std::unexpected(string_array_obj_result.error()); } @@ -1847,10 +1850,9 @@ std::expected ListDir(PassedExecutionData& return std::unexpected(string_vtable_index_result.error()); } - auto string_obj_result = runtime::AllocateObject(*string_vtable, - static_cast(string_vtable_index_result.value()), - data.memory.object_repository, - data.allocator); + auto string_obj_result = data.memory_manager.AllocateObject( + *string_vtable, static_cast(string_vtable_index_result.value()), data); + if (!string_obj_result.has_value()) { return std::unexpected(string_obj_result.error()); } @@ -2068,20 +2070,36 @@ std::expected SeedRandom(PassedExecutionDat } std::expected GetMemoryUsage(PassedExecutionData& data) { - // Simple memory usage approximation size_t memory_usage = 0; - // Stack size - memory_usage += data.memory.machine_stack.size() * sizeof(runtime::Variable); - - auto stack_copy = data.memory.stack_frames; - // Local variables in all stack frames - while (!stack_copy.empty()) { - memory_usage += stack_copy.top().local_variables.size() * sizeof(runtime::Variable); - stack_copy.pop(); +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS pmc; + if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) { + memory_usage = static_cast(pmc.WorkingSetSize); + } else { + return std::unexpected(std::runtime_error("GetMemoryUsage: failed to get process memory info")); + } +#elif __APPLE__ + struct task_basic_info info {}; + mach_msg_type_number_t size = sizeof(info); + kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, reinterpret_cast(&info), &size); + if (kerr == KERN_SUCCESS) { + memory_usage = static_cast(info.resident_size); + } else { + return std::unexpected(std::runtime_error("GetMemoryUsage: failed to get task memory info")); + } +#else + // Linux: Use getrusage() system call + struct rusage usage {}; + constexpr size_t kBytesInRusageUnit = 1024; + if (getrusage(RUSAGE_SELF, &usage) != 0) { + return std::unexpected(std::runtime_error("GetMemoryUsage: getrusage() failed")); } - // TODO counter for repositories + // ru_maxrss is the maximum resident set size in kilobytes + // Note: getrusage gives maximum usage, not current usage + memory_usage = static_cast(usage.ru_maxrss) * kBytesInRusageUnit; +#endif data.memory.machine_stack.emplace(static_cast(memory_usage)); return ExecutionResult::kNormal; @@ -2094,8 +2112,10 @@ std::expected GetPeakMemoryUsage(PassedExec std::expected ForceGarbageCollection(PassedExecutionData& data) { // Simple garbage collection: remove unreachable objects - // This is a placeholder implementation - // data.memory.object_repository.CollectGarbage(); + auto result = data.memory_manager.CollectGarbage(data); + if (!result.has_value()) { + return std::unexpected(result.error()); + } return ExecutionResult::kNormal; } diff --git a/lib/execution_tree/CMakeLists.txt b/lib/execution_tree/CMakeLists.txt index 843bad0..7eb5096 100644 --- a/lib/execution_tree/CMakeLists.txt +++ b/lib/execution_tree/CMakeLists.txt @@ -13,3 +13,7 @@ add_library(execution_tree STATIC target_include_directories(execution_tree PUBLIC ${PROJECT_SOURCE_DIR}) target_link_libraries(execution_tree PUBLIC runtime) + +if(WIN32) + target_link_libraries(execution_tree PRIVATE psapi) +endif() diff --git a/lib/execution_tree/Command.hpp b/lib/execution_tree/Command.hpp index e593079..de9df66 100644 --- a/lib/execution_tree/Command.hpp +++ b/lib/execution_tree/Command.hpp @@ -35,6 +35,12 @@ class Command : public IExecutable { return std::unexpected(std::runtime_error(error_message)); } + auto gc_res = execution_data.memory_manager.CollectGarbageIfRequired(execution_data); + + if (!gc_res) { + return std::unexpected(gc_res.error()); + } + return result.value(); } diff --git a/lib/execution_tree/PassedExecutionData.hpp b/lib/execution_tree/PassedExecutionData.hpp index 01b8ee8..6abe089 100644 --- a/lib/execution_tree/PassedExecutionData.hpp +++ b/lib/execution_tree/PassedExecutionData.hpp @@ -5,6 +5,7 @@ #include #include +#include "lib/runtime/MemoryManager.hpp" #include "lib/runtime/RuntimeMemory.hpp" #include "lib/runtime/VirtualTableRepository.hpp" @@ -16,7 +17,7 @@ struct PassedExecutionData { runtime::RuntimeMemory& memory; const runtime::VirtualTableRepository& virtual_table_repository; const FunctionRepository& function_repository; - std::allocator allocator; + runtime::MemoryManager& memory_manager; std::istream& input_stream; std::ostream& output_stream; std::ostream& error_stream; diff --git a/lib/executor/BuiltinFunctions.cpp b/lib/executor/BuiltinFunctions.cpp index a65bb60..0c66632 100644 --- a/lib/executor/BuiltinFunctions.cpp +++ b/lib/executor/BuiltinFunctions.cpp @@ -15,6 +15,23 @@ #include "lib/runtime/Variable.hpp" #include "lib/runtime/VirtualTable.hpp" +namespace ovum::vm::runtime { + +// Helper to hash vector using algorithm from PureFunction.hpp +template +int64_t HashVector(const std::vector& vec) { + static constexpr size_t kHashMultiplier = 0x9e3779b9; + static constexpr size_t kHashShift = 6; + + size_t seed = 0; + for (const T& value : vec) { + seed ^= std::hash{}(value) + kHashMultiplier + (seed << kHashShift) + (seed >> kHashShift); + } + return static_cast(seed); +} + +} // namespace ovum::vm::runtime + namespace ovum::vm::execution_tree { // Helper to check if two objects have the same type (same vtable_index) @@ -172,10 +189,8 @@ std::expected FundamentalTypeToString(Passe return std::unexpected(vtable_index_result.error()); } - auto string_obj_result = runtime::AllocateObject(*string_vtable, - static_cast(vtable_index_result.value()), - data.memory.object_repository, - data.allocator); + auto string_obj_result = + data.memory_manager.AllocateObject(*string_vtable, static_cast(vtable_index_result.value()), data); if (!string_obj_result.has_value()) { return std::unexpected(string_obj_result.error()); @@ -938,10 +953,8 @@ std::expected StringToUtf8Bytes(PassedExecu return std::unexpected(vtable_index_result.error()); } - auto byte_array_obj_result = runtime::AllocateObject(*byte_array_vtable, - static_cast(vtable_index_result.value()), - data.memory.object_repository, - data.allocator); + auto byte_array_obj_result = + data.memory_manager.AllocateObject(*byte_array_vtable, static_cast(vtable_index_result.value()), data); if (!byte_array_obj_result.has_value()) { return std::unexpected(byte_array_obj_result.error()); @@ -1919,10 +1932,8 @@ std::expected FileRead(PassedExecutionData& return std::unexpected(vtable_index_result.error()); } - auto byte_array_obj_result = runtime::AllocateObject(*byte_array_vtable, - static_cast(vtable_index_result.value()), - data.memory.object_repository, - data.allocator); + auto byte_array_obj_result = + data.memory_manager.AllocateObject(*byte_array_vtable, static_cast(vtable_index_result.value()), data); if (!byte_array_obj_result.has_value()) { return std::unexpected(byte_array_obj_result.error()); @@ -2001,10 +2012,8 @@ std::expected FileReadLine(PassedExecutionD return std::unexpected(vtable_index_result.error()); } - auto string_obj_result = runtime::AllocateObject(*string_vtable, - static_cast(vtable_index_result.value()), - data.memory.object_repository, - data.allocator); + auto string_obj_result = + data.memory_manager.AllocateObject(*string_vtable, static_cast(vtable_index_result.value()), data); if (!string_obj_result.has_value()) { return std::unexpected(string_obj_result.error()); diff --git a/lib/executor/BuiltinFunctions.hpp b/lib/executor/BuiltinFunctions.hpp index eabed23..9e77b28 100644 --- a/lib/executor/BuiltinFunctions.hpp +++ b/lib/executor/BuiltinFunctions.hpp @@ -1,19 +1,12 @@ #ifndef EXECUTOR_BUILTINFUNCTIONS_HPP #define EXECUTOR_BUILTINFUNCTIONS_HPP -#include -#include #include -#include -#include #include -#include #include "lib/execution_tree/ExecutionResult.hpp" #include "lib/execution_tree/PassedExecutionData.hpp" #include "lib/runtime/ObjectDescriptor.hpp" -#include "lib/runtime/ObjectRepository.hpp" -#include "lib/runtime/VirtualTable.hpp" namespace ovum::vm::runtime { @@ -28,50 +21,6 @@ const T* GetDataPointer(const void* object_ptr) { return reinterpret_cast(reinterpret_cast(object_ptr) + sizeof(ObjectDescriptor)); } -// Helper to hash vector using algorithm from PureFunction.hpp -template -int64_t HashVector(const std::vector& vec) { - static constexpr size_t kHashMultiplier = 0x9e3779b9; - static constexpr size_t kHashShift = 6; - - size_t seed = 0; - for (const T& value : vec) { - seed ^= std::hash{}(value) + kHashMultiplier + (seed << kHashShift) + (seed >> kHashShift); - } - return static_cast(seed); -} - -// Allocation helper: allocates memory for an object and initializes ObjectDescriptor -template> -std::expected AllocateObject(const VirtualTable& vtable, - uint32_t vtable_index, - ObjectRepository& object_repository, - Allocator&& allocator = Allocator{}) { - const size_t size = vtable.GetSize(); - char* memory = allocator.allocate(size); - if (memory == nullptr) { - return std::unexpected(std::runtime_error("AllocateObject: failed to allocate memory")); - } - - // Initialize ObjectDescriptor at the first 8 bytes - ObjectDescriptor* descriptor = reinterpret_cast(memory); - descriptor->vtable_index = vtable_index; - descriptor->badge = 0; - - // Zero-initialize the rest of the memory - std::memset(memory + sizeof(ObjectDescriptor), 0, size - sizeof(ObjectDescriptor)); - - // Add to ObjectRepository - auto add_result = object_repository.Add(descriptor); - if (!add_result.has_value()) { - allocator.deallocate(memory, size); - return std::unexpected(std::runtime_error(std::string("AllocateObject: failed to add object to repository: ") + - add_result.error().what())); - } - - return memory; -} - } // namespace ovum::vm::runtime namespace ovum::vm::execution_tree { diff --git a/lib/executor/Executor.cpp b/lib/executor/Executor.cpp index 08a2648..fdc5fe9 100644 --- a/lib/executor/Executor.cpp +++ b/lib/executor/Executor.cpp @@ -31,10 +31,8 @@ std::expected CreateStringArrayFromArgs(execution_tre return std::unexpected(string_vtable_index_result.error()); } - auto default_string_obj_result = runtime::AllocateObject(*string_vtable, - static_cast(string_vtable_index_result.value()), - execution_data.memory.object_repository, - execution_data.allocator); + auto default_string_obj_result = execution_data.memory_manager.AllocateObject( + *string_vtable, static_cast(string_vtable_index_result.value()), execution_data); if (!default_string_obj_result.has_value()) { return std::unexpected(default_string_obj_result.error()); @@ -70,10 +68,8 @@ std::expected CreateStringArrayFromArgs(execution_tre } for (size_t i = 0; i < args.size(); ++i) { - auto string_obj_result = runtime::AllocateObject(*string_vtable, - static_cast(string_vtable_index_result.value()), - execution_data.memory.object_repository, - execution_data.allocator); + auto string_obj_result = execution_data.memory_manager.AllocateObject( + *string_vtable, static_cast(string_vtable_index_result.value()), execution_data); if (!string_obj_result.has_value()) { return std::unexpected(string_obj_result.error()); diff --git a/lib/executor/builtin_factory.cpp b/lib/executor/builtin_factory.cpp index 3066a35..70ab73c 100644 --- a/lib/executor/builtin_factory.cpp +++ b/lib/executor/builtin_factory.cpp @@ -20,6 +20,7 @@ #include "lib/runtime/ObjectDescriptor.hpp" #include "lib/runtime/VirtualTable.hpp" #include "lib/runtime/VirtualTableRepository.hpp" +#include "lib/runtime/gc/reference_scanners/ArrayReferenceScanner.hpp" namespace ovum::vm::runtime { @@ -137,7 +138,6 @@ std::expected RegisterBuiltinVirtualTables(VirtualTabl // String: wrapper around std::string { VirtualTable string_vtable("String", sizeof(ObjectDescriptor) + sizeof(std::string)); - string_vtable.AddField("Object", sizeof(ObjectDescriptor)); string_vtable.AddFunction("_destructor_", "_String_destructor_"); string_vtable.AddFunction("_Equals__Object", "_String_Equals__Object"); string_vtable.AddFunction("_IsLess__Object", "_String_IsLess__Object"); @@ -157,7 +157,6 @@ std::expected RegisterBuiltinVirtualTables(VirtualTabl // File: wrapper around std::fstream { VirtualTable file_vtable("File", sizeof(ObjectDescriptor) + sizeof(std::fstream)); - file_vtable.AddField("Object", sizeof(ObjectDescriptor)); file_vtable.AddFunction("_destructor_", "_File_destructor_"); file_vtable.AddFunction("_Open__String_String", "_File_Open__String_String"); file_vtable.AddFunction("_Close_", "_File_Close_"); @@ -179,7 +178,6 @@ std::expected RegisterBuiltinVirtualTables(VirtualTabl // IntArray: wrapper around std::vector { VirtualTable int_array_vtable("IntArray", sizeof(ObjectDescriptor) + sizeof(std::vector)); - int_array_vtable.AddField("Object", sizeof(ObjectDescriptor)); int_array_vtable.AddFunction("_destructor_", "_IntArray_destructor_"); int_array_vtable.AddFunction("_Equals__Object", "_IntArray_Equals__Object"); int_array_vtable.AddFunction("_IsLess__Object", "_IntArray_IsLess__Object"); @@ -195,7 +193,6 @@ std::expected RegisterBuiltinVirtualTables(VirtualTabl // FloatArray: wrapper around std::vector { VirtualTable float_array_vtable("FloatArray", sizeof(ObjectDescriptor) + sizeof(std::vector)); - float_array_vtable.AddField("Object", sizeof(ObjectDescriptor)); float_array_vtable.AddFunction("_destructor_", "_FloatArray_destructor_"); float_array_vtable.AddFunction("_Equals__Object", "_FloatArray_Equals__Object"); float_array_vtable.AddFunction("_IsLess__Object", "_FloatArray_IsLess__Object"); @@ -211,7 +208,6 @@ std::expected RegisterBuiltinVirtualTables(VirtualTabl // CharArray: wrapper around std::vector { VirtualTable char_array_vtable("CharArray", sizeof(ObjectDescriptor) + sizeof(std::vector)); - char_array_vtable.AddField("Object", sizeof(ObjectDescriptor)); char_array_vtable.AddFunction("_destructor_", "_CharArray_destructor_"); char_array_vtable.AddFunction("_Equals__Object", "_CharArray_Equals__Object"); char_array_vtable.AddFunction("_IsLess__Object", "_CharArray_IsLess__Object"); @@ -228,7 +224,6 @@ std::expected RegisterBuiltinVirtualTables(VirtualTabl // Special handling for byte view casting { VirtualTable byte_array_vtable("ByteArray", sizeof(ObjectDescriptor) + sizeof(ovum::vm::runtime::ByteArray)); - byte_array_vtable.AddField("Object", sizeof(ObjectDescriptor)); byte_array_vtable.AddFunction("_destructor_", "_ByteArray_destructor_"); byte_array_vtable.AddFunction("_Equals__Object", "_ByteArray_Equals__Object"); byte_array_vtable.AddFunction("_IsLess__Object", "_ByteArray_IsLess__Object"); @@ -244,7 +239,6 @@ std::expected RegisterBuiltinVirtualTables(VirtualTabl // BoolArray: wrapper around std::vector { VirtualTable bool_array_vtable("BoolArray", sizeof(ObjectDescriptor) + sizeof(std::vector)); - bool_array_vtable.AddField("Object", sizeof(ObjectDescriptor)); bool_array_vtable.AddFunction("_destructor_", "_BoolArray_destructor_"); bool_array_vtable.AddFunction("_Equals__Object", "_BoolArray_Equals__Object"); bool_array_vtable.AddFunction("_IsLess__Object", "_BoolArray_IsLess__Object"); @@ -259,8 +253,9 @@ std::expected RegisterBuiltinVirtualTables(VirtualTabl // ObjectArray: wrapper around std::vector { - VirtualTable object_array_vtable("ObjectArray", sizeof(ObjectDescriptor) + sizeof(std::vector)); - object_array_vtable.AddField("Object", sizeof(ObjectDescriptor)); + VirtualTable object_array_vtable("ObjectArray", + sizeof(ObjectDescriptor) + sizeof(std::vector), + std::make_unique()); object_array_vtable.AddFunction("_destructor_", "_ObjectArray_destructor_"); object_array_vtable.AddFunction("_Equals__Object", "_ObjectArray_Equals__Object"); object_array_vtable.AddFunction("_IsLess__Object", "_ObjectArray_IsLess__Object"); @@ -275,8 +270,9 @@ std::expected RegisterBuiltinVirtualTables(VirtualTabl // StringArray: more typed ObjectArray { - VirtualTable string_array_vtable("StringArray", sizeof(ObjectDescriptor) + sizeof(std::vector)); - string_array_vtable.AddField("Object", sizeof(ObjectDescriptor)); + VirtualTable string_array_vtable("StringArray", + sizeof(ObjectDescriptor) + sizeof(std::vector), + std::make_unique()); string_array_vtable.AddFunction("_destructor_", "_StringArray_destructor_"); string_array_vtable.AddFunction("_Equals__Object", "_StringArray_Equals__Object"); string_array_vtable.AddFunction("_IsLess__Object", "_StringArray_IsLess__Object"); @@ -307,8 +303,9 @@ std::expected RegisterBuiltinVirtualTables(VirtualTabl // PointerArray: unsafe array of pointers { - VirtualTable pointer_array_vtable("PointerArray", sizeof(ObjectDescriptor) + sizeof(std::vector)); - pointer_array_vtable.AddField("Object", sizeof(ObjectDescriptor)); + VirtualTable pointer_array_vtable("PointerArray", + sizeof(ObjectDescriptor) + sizeof(std::vector), + std::make_unique()); pointer_array_vtable.AddFunction("_destructor_", "_PointerArray_destructor_"); pointer_array_vtable.AddFunction("_Equals__Object", "_PointerArray_Equals__Object"); pointer_array_vtable.AddFunction("_IsLess__Object", "_PointerArray_IsLess__Object"); diff --git a/lib/runtime/ByteArray.cpp b/lib/runtime/ByteArray.cpp index 5d41df4..1718086 100644 --- a/lib/runtime/ByteArray.cpp +++ b/lib/runtime/ByteArray.cpp @@ -102,6 +102,7 @@ void ByteArray::Insert(size_t index, const uint8_t* data, size_t count) { if (index > size_) { throw std::runtime_error("ByteArray: Insert index out of bounds"); } + if (data == nullptr && count > 0) { throw std::runtime_error("ByteArray: Insert data is null"); } @@ -125,6 +126,7 @@ void ByteArray::Remove(size_t index, size_t count) { if (index >= size_) { throw std::runtime_error("ByteArray: Remove index out of bounds"); } + if (index + count > size_) { throw std::runtime_error("ByteArray: Remove count exceeds available elements"); } diff --git a/lib/runtime/CMakeLists.txt b/lib/runtime/CMakeLists.txt index 4f18707..7108c04 100644 --- a/lib/runtime/CMakeLists.txt +++ b/lib/runtime/CMakeLists.txt @@ -3,6 +3,10 @@ add_library(runtime STATIC VirtualTableRepository.cpp ObjectRepository.cpp ByteArray.cpp + MemoryManager.cpp + gc/MarkAndSweepGC.cpp + gc/reference_scanners/ArrayReferenceScanner.cpp + gc/reference_scanners/DefaultReferenceScanner.cpp ) target_include_directories(runtime PUBLIC ${PROJECT_SOURCE_DIR}) diff --git a/lib/runtime/MemoryManager.cpp b/lib/runtime/MemoryManager.cpp new file mode 100644 index 0000000..7c97feb --- /dev/null +++ b/lib/runtime/MemoryManager.cpp @@ -0,0 +1,208 @@ +#include "MemoryManager.hpp" + +#include +#include +#include +#include + +#include "lib/execution_tree/FunctionRepository.hpp" +#include "lib/execution_tree/PassedExecutionData.hpp" + +#include "ObjectDescriptor.hpp" + +namespace ovum::vm::runtime { + +MemoryManager::MemoryManager(std::unique_ptr gc, size_t max_objects) : + gc_(std::move(gc)), gc_threshold_(max_objects), gc_in_progress_(false) { +} + +std::expected MemoryManager::AllocateObject(const VirtualTable& vtable, + uint32_t vtable_index, + execution_tree::PassedExecutionData& data) { + const size_t total_size = vtable.GetSize(); + char* raw_memory = nullptr; + + try { + raw_memory = allocator_.allocate(total_size); + } catch (const std::bad_alloc&) { + return std::unexpected(std::runtime_error("MemoryManager: Allocation failed - out of memory")); + } + + auto* descriptor = reinterpret_cast(raw_memory); + descriptor->vtable_index = vtable_index; + descriptor->badge = 0; + + std::memset(raw_memory + sizeof(ObjectDescriptor), 0, total_size - sizeof(ObjectDescriptor)); + + std::expected add_result = repo_.Add(descriptor); + + if (!add_result.has_value()) { + allocator_.deallocate(raw_memory, total_size); + return std::unexpected(add_result.error()); + } + + return reinterpret_cast(descriptor); +} + +std::expected MemoryManager::DeallocateObject(void* obj, + execution_tree::PassedExecutionData& data) { + if (!obj) { + return std::unexpected(std::runtime_error("DeallocateObject: Null object pointer")); + } + + auto* desc = reinterpret_cast(obj); + + std::expected vt_res = + data.virtual_table_repository.GetByIndex(desc->vtable_index); + + if (!vt_res.has_value()) { + return std::unexpected(std::runtime_error("DeallocateObject: Virtual table not found for index " + + std::to_string(desc->vtable_index))); + } + + std::string object_type = vt_res.value()->GetName(); + const VirtualTable* vt = vt_res.value(); + + std::expected dtor_id_res = vt->GetRealFunctionId("_destructor_"); + + if (!dtor_id_res.has_value()) { + return std::unexpected(std::runtime_error("DeallocateObject: Destructor not found for class " + object_type)); + } + + auto func_res = data.function_repository.GetById(dtor_id_res.value()); + + if (!func_res.has_value()) { + return std::unexpected( + std::runtime_error("DeallocateObject: Destructor function not found for class " + object_type)); + } + + runtime::StackFrame frame = {.function_name = "Object deallocation"}; + data.memory.machine_stack.emplace(obj); + data.memory.stack_frames.push(std::move(frame)); + std::expected exec_res = func_res.value()->Execute(data); + data.memory.stack_frames.pop(); + + if (!exec_res.has_value()) { + return std::unexpected(exec_res.error()); + } + + const size_t total_size = vt->GetSize(); + char* raw = reinterpret_cast(obj); + + std::expected remove_res = repo_.Remove(desc); + + if (!remove_res.has_value()) { + return std::unexpected(remove_res.error()); + } + + allocator_.deallocate(raw, total_size); + + return {}; +} + +std::expected MemoryManager::Clear(execution_tree::PassedExecutionData& data) { + gc_in_progress_ = true; + + std::vector objects_to_clear; + objects_to_clear.reserve(repo_.GetCount()); + + repo_.ForAll([&objects_to_clear](void* obj) { objects_to_clear.push_back(obj); }); + + std::optional first_error; + + for (void* obj : objects_to_clear) { + auto* desc = reinterpret_cast(obj); + + std::expected vt_res = + data.virtual_table_repository.GetByIndex(desc->vtable_index); + + if (!vt_res.has_value()) { + if (!first_error) { + first_error = + std::runtime_error("Clear: Virtual table not found for index " + std::to_string(desc->vtable_index)); + } + continue; + } + + const VirtualTable* vt = vt_res.value(); + + std::expected dtor_id_res = vt->GetRealFunctionId("_destructor_"); + + if (dtor_id_res.has_value()) { + auto func_res = data.function_repository.GetById(dtor_id_res.value()); + + if (func_res.has_value()) { + runtime::StackFrame frame = {.function_name = "Object deallocation (Clear)"}; + data.memory.machine_stack.emplace(obj); + data.memory.stack_frames.push(std::move(frame)); + std::expected exec_res = func_res.value()->Execute(data); + data.memory.stack_frames.pop(); + + if (!exec_res.has_value()) { + if (!first_error) { + first_error = exec_res.error(); + } + continue; + } + } + } + + std::expected remove_res = repo_.Remove(desc); + + if (!remove_res.has_value()) { + if (!first_error) { + first_error = remove_res.error(); + } + continue; + } + + const size_t total_size = vt->GetSize(); + char* raw = reinterpret_cast(obj); + + allocator_.deallocate(raw, total_size); + } + + repo_.Clear(); + + if (first_error.has_value()) { + return std::unexpected(*first_error); + } + + gc_in_progress_ = false; + + return {}; +} + +std::expected MemoryManager::CollectGarbage(execution_tree::PassedExecutionData& data) { + if (!gc_) { + return std::unexpected(std::runtime_error("MemoryManager: No GC configured")); + } + + if (!gc_in_progress_) { + gc_in_progress_ = true; + auto gc_res = gc_->Collect(data); + gc_in_progress_ = false; + return gc_res; + } + + return {}; +} + +std::expected MemoryManager::CollectGarbageIfRequired( + execution_tree::PassedExecutionData& data) { + if (repo_.GetCount() > gc_threshold_) { + std::expected collect_res = CollectGarbage(data); + + if (!collect_res.has_value()) { + return std::unexpected(collect_res.error()); + } + } + + return {}; +} + +const ObjectRepository& MemoryManager::GetRepository() const { + return repo_; +} + +} // namespace ovum::vm::runtime diff --git a/lib/runtime/MemoryManager.hpp b/lib/runtime/MemoryManager.hpp new file mode 100644 index 0000000..085699b --- /dev/null +++ b/lib/runtime/MemoryManager.hpp @@ -0,0 +1,43 @@ +#ifndef RUNTIME_MEMORYMANAGER_HPP +#define RUNTIME_MEMORYMANAGER_HPP + +#include +#include +#include + +#include "lib/runtime/gc/IGarbageCollector.hpp" + +#include "ObjectRepository.hpp" +#include "VirtualTable.hpp" + +namespace ovum::vm::execution_tree { +struct PassedExecutionData; +} // namespace ovum::vm::execution_tree + +namespace ovum::vm::runtime { + +class MemoryManager { +public: + MemoryManager(std::unique_ptr gc, size_t max_objects); + + std::expected AllocateObject(const VirtualTable& vtable, + uint32_t vtable_index, + execution_tree::PassedExecutionData& data); + std::expected DeallocateObject(void* obj, execution_tree::PassedExecutionData& data); + std::expected CollectGarbage(execution_tree::PassedExecutionData& data); + std::expected CollectGarbageIfRequired(execution_tree::PassedExecutionData& data); + std::expected Clear(execution_tree::PassedExecutionData& data); + + [[nodiscard]] const ObjectRepository& GetRepository() const; + +private: + ObjectRepository repo_; + std::allocator allocator_; + std::unique_ptr gc_; + size_t gc_threshold_; + bool gc_in_progress_; +}; + +} // namespace ovum::vm::runtime + +#endif // RUNTIME_MEMORYMANAGER_HPP diff --git a/lib/runtime/ObjectRepository.cpp b/lib/runtime/ObjectRepository.cpp index 0db0ba2..c40c856 100644 --- a/lib/runtime/ObjectRepository.cpp +++ b/lib/runtime/ObjectRepository.cpp @@ -8,18 +8,26 @@ void ObjectRepository::Reserve(size_t count) { objects_.reserve(count); } -std::expected ObjectRepository::Add(ObjectDescriptor* descriptor) { - objects_.emplace_back(descriptor); - return objects_.size() - 1U; +std::expected ObjectRepository::Add(ObjectDescriptor* descriptor) { + if (!descriptor) { + return std::unexpected(std::runtime_error("ObjectRepository: Cannot add null descriptor")); + } + + objects_.insert(descriptor); + return {}; } -std::expected ObjectRepository::Remove(size_t index) { - if (index >= objects_.size()) { - return std::unexpected(std::runtime_error("ObjectDescriptor index out of range")); +std::expected ObjectRepository::Remove(ObjectDescriptor* descriptor) { + if (!descriptor) { + return std::unexpected(std::runtime_error("ObjectRepository: Cannot remove null descriptor")); } - objects_.erase(objects_.begin() + static_cast(index)); + auto it = objects_.find(descriptor); + if (it == objects_.end()) { + return std::unexpected(std::runtime_error("ObjectRepository: Descriptor not found")); + } + objects_.erase(it); return {}; } @@ -27,20 +35,10 @@ void ObjectRepository::Clear() { objects_.clear(); } -std::expected ObjectRepository::GetByIndex(size_t index) { - if (index >= objects_.size()) { - return std::unexpected(std::runtime_error("ObjectDescriptor index out of range")); +void ObjectRepository::ForAll(const std::function& func) const { + for (ObjectDescriptor* desc : objects_) { + func(reinterpret_cast(desc)); } - - return objects_[index]; -} - -std::expected ObjectRepository::GetByIndex(size_t index) const { - if (index >= objects_.size()) { - return std::unexpected(std::runtime_error("ObjectDescriptor index out of range")); - } - - return objects_[index]; } size_t ObjectRepository::GetCount() const { diff --git a/lib/runtime/ObjectRepository.hpp b/lib/runtime/ObjectRepository.hpp index e1ed49f..6d66e4e 100644 --- a/lib/runtime/ObjectRepository.hpp +++ b/lib/runtime/ObjectRepository.hpp @@ -3,8 +3,9 @@ #include #include +#include #include -#include +#include #include "ObjectDescriptor.hpp" @@ -16,18 +17,16 @@ class ObjectRepository { void Reserve(size_t count); - [[nodiscard]] std::expected Add(ObjectDescriptor* descriptor); - std::expected Remove(size_t index); + [[nodiscard]] std::expected Add(ObjectDescriptor* descriptor); + std::expected Remove(ObjectDescriptor* descriptor); void Clear(); - [[nodiscard]] std::expected GetByIndex(size_t index); - - [[nodiscard]] std::expected GetByIndex(size_t index) const; + void ForAll(const std::function& func) const; [[nodiscard]] size_t GetCount() const; private: - std::vector objects_; + std::unordered_set objects_; }; } // namespace ovum::vm::runtime diff --git a/lib/runtime/StackFrame.hpp b/lib/runtime/StackFrame.hpp index f1f9dd9..731e289 100644 --- a/lib/runtime/StackFrame.hpp +++ b/lib/runtime/StackFrame.hpp @@ -14,4 +14,5 @@ struct StackFrame { }; } // namespace ovum::vm::runtime + #endif // RUNTIME_STACKFRAME_HPP diff --git a/lib/runtime/VariableAccessor.hpp b/lib/runtime/VariableAccessor.hpp index 6322304..b60c390 100644 --- a/lib/runtime/VariableAccessor.hpp +++ b/lib/runtime/VariableAccessor.hpp @@ -22,6 +22,7 @@ class VariableAccessor : public IVariableAccessor { } *reinterpret_cast(value_ptr) = std::get(variable); + return {}; } }; diff --git a/lib/runtime/VirtualTable.cpp b/lib/runtime/VirtualTable.cpp index aa6f151..0c5f397 100644 --- a/lib/runtime/VirtualTable.cpp +++ b/lib/runtime/VirtualTable.cpp @@ -3,6 +3,7 @@ #include #include "VariableAccessor.hpp" +#include "lib/runtime/gc/reference_scanners/DefaultReferenceScanner.hpp" namespace ovum::vm::runtime { @@ -15,7 +16,11 @@ const std::unordered_map> Virtua {"Object", std::make_shared>()}, }; -VirtualTable::VirtualTable(std::string name, size_t size) : name_(std::move(name)), size_(size) { +VirtualTable::VirtualTable(std::string name, size_t size, std::unique_ptr scanner) : + name_(std::move(name)), size_(size), reference_scanner_(std::move(scanner)) { + if (!reference_scanner_) { + reference_scanner_ = std::make_unique(); + } } std::string VirtualTable::GetName() const { @@ -80,4 +85,12 @@ bool VirtualTable::IsType(const std::string& interface_name) const { return interfaces_.contains(interface_name) || interface_name == name_; } +size_t VirtualTable::GetFieldCount() const { + return fields_.size(); +} + +void VirtualTable::ScanReferences(void* obj, const ReferenceVisitor& visitor) const { + reference_scanner_->Scan(obj, fields_, visitor); +} + } // namespace ovum::vm::runtime diff --git a/lib/runtime/VirtualTable.hpp b/lib/runtime/VirtualTable.hpp index 5626a32..af58e4c 100644 --- a/lib/runtime/VirtualTable.hpp +++ b/lib/runtime/VirtualTable.hpp @@ -2,11 +2,14 @@ #define RUNTIME_VIRTUAL_TABLE_HPP #include +#include #include #include #include #include +#include "lib/runtime/gc/reference_scanners/IReferenceScanner.hpp" + #include "FieldInfo.hpp" #include "FunctionId.hpp" #include "IVariableAccessor.hpp" @@ -16,7 +19,7 @@ namespace ovum::vm::runtime { class VirtualTable { public: - VirtualTable(std::string name, size_t size); + VirtualTable(std::string name, size_t size, std::unique_ptr scanner = nullptr); [[nodiscard]] std::string GetName() const; [[nodiscard]] size_t GetSize() const; @@ -29,10 +32,14 @@ class VirtualTable { const FunctionId& virtual_function_id) const; [[nodiscard]] bool IsType(const std::string& interface_name) const; + [[nodiscard]] size_t GetFieldCount() const; + void AddFunction(const FunctionId& virtual_function_id, const FunctionId& real_function_id); size_t AddField(const std::string& type_name, int64_t offset); void AddInterface(const std::string& interface_name); + void ScanReferences(void* obj, const ReferenceVisitor& visitor) const; + private: static const std::unordered_map> kVariableAccessorsByTypeName; @@ -41,6 +48,8 @@ class VirtualTable { std::vector fields_; std::unordered_map functions_; std::unordered_set interfaces_; + + std::unique_ptr reference_scanner_; }; } // namespace ovum::vm::runtime diff --git a/lib/runtime/VirtualTableRepository.cpp b/lib/runtime/VirtualTableRepository.cpp index cee0b7c..151bb97 100644 --- a/lib/runtime/VirtualTableRepository.cpp +++ b/lib/runtime/VirtualTableRepository.cpp @@ -40,20 +40,22 @@ std::expected VirtualTableRepository::G std::expected VirtualTableRepository::GetByName(const std::string& name) { const auto it = index_by_name_.find(name); - if (index_by_name_.find(name) == index_by_name_.end()) { + if (it == index_by_name_.end()) { return std::unexpected(std::runtime_error("VirtualTable not found by name: " + name)); } - return &vtables_[index_by_name_.at(name)]; + return &vtables_[it->second]; } std::expected VirtualTableRepository::GetByName( const std::string& name) const { - if (index_by_name_.find(name) == index_by_name_.end()) { + const auto it = index_by_name_.find(name); + + if (it == index_by_name_.end()) { return std::unexpected(std::runtime_error("VirtualTable not found by name: " + name)); } - return &vtables_[index_by_name_.at(name)]; + return &vtables_[it->second]; } std::expected VirtualTableRepository::GetIndexByName(const std::string& name) const { diff --git a/lib/runtime/gc/IGarbageCollector.hpp b/lib/runtime/gc/IGarbageCollector.hpp new file mode 100644 index 0000000..def4507 --- /dev/null +++ b/lib/runtime/gc/IGarbageCollector.hpp @@ -0,0 +1,24 @@ +#ifndef RUNTIME_GARBAGECOLLECTOR_HPP +#define RUNTIME_GARBAGECOLLECTOR_HPP + +#include +#include +#include + +namespace ovum::vm::execution_tree { +struct PassedExecutionData; +} // namespace ovum::vm::execution_tree + +namespace ovum::vm::runtime { + +constexpr uint32_t kMarkBit = 1U; + +class IGarbageCollector { // NOLINT(cppcoreguidelines-special-member-functions) +public: + virtual ~IGarbageCollector() = default; + virtual std::expected Collect(execution_tree::PassedExecutionData& data) = 0; +}; + +} // namespace ovum::vm::runtime + +#endif // RUNTIME_GARBAGECOLLECTOR_HPP diff --git a/lib/runtime/gc/MarkAndSweepGC.cpp b/lib/runtime/gc/MarkAndSweepGC.cpp new file mode 100644 index 0000000..6f5d589 --- /dev/null +++ b/lib/runtime/gc/MarkAndSweepGC.cpp @@ -0,0 +1,127 @@ +#include "MarkAndSweepGC.hpp" + +#include +#include +#include +#include + +#include "lib/execution_tree/PassedExecutionData.hpp" +#include "lib/runtime/ObjectDescriptor.hpp" +#include "lib/runtime/VirtualTableRepository.hpp" + +namespace ovum::vm::runtime { + +std::expected MarkAndSweepGC::Collect(execution_tree::PassedExecutionData& data) { + Mark(data); + return Sweep(data); +} + +void MarkAndSweepGC::Mark(execution_tree::PassedExecutionData& data) { + std::queue worklist; + AddRoots(worklist, data); + + while (!worklist.empty()) { + void* obj = worklist.front(); + worklist.pop(); + + if (obj == nullptr) { + continue; + } + + auto* desc = reinterpret_cast(obj); + + if (desc->badge & kMarkBit) { + continue; + } + + desc->badge |= kMarkBit; + + std::expected vt_res = + data.virtual_table_repository.GetByIndex(desc->vtable_index); + + if (!vt_res.has_value()) { + continue; + } + + const VirtualTable* vt = vt_res.value(); + + vt->ScanReferences(obj, [&worklist](void* ref) { + if (ref) { + worklist.push(ref); + } + }); + } +} + +std::expected MarkAndSweepGC::Sweep(execution_tree::PassedExecutionData& data) { + std::vector to_delete; + const ObjectRepository& repo = data.memory_manager.GetRepository(); + + repo.ForAll([&to_delete](void* obj) { + auto* desc = reinterpret_cast(obj); + + if (!(desc->badge & kMarkBit)) { + to_delete.push_back(obj); + } + + desc->badge &= ~kMarkBit; + }); + + std::optional first_error; + + for (void* obj : to_delete) { + std::expected dealloc_res = data.memory_manager.DeallocateObject(obj, data); + + if (!dealloc_res.has_value() && !first_error) { + first_error = dealloc_res.error(); + } + } + + if (first_error) { + return std::unexpected(*first_error); + } + + return {}; +} + +void MarkAndSweepGC::AddRoots(std::queue& worklist, execution_tree::PassedExecutionData& data) { + AddAllVariables(worklist, data.memory.global_variables); + + // Note that there is no way to traverse a std::stack without emptying it, so we need to create a temporary stack. + std::stack temp_stack_frames = data.memory.stack_frames; + + while (!temp_stack_frames.empty()) { + const StackFrame& frame = temp_stack_frames.top(); + AddAllVariables(worklist, frame.local_variables); + temp_stack_frames.pop(); + } + + AddAllVariables(worklist, data.memory.machine_stack); +} + +void MarkAndSweepGC::AddAllVariables(std::queue& worklist, const std::vector& variables) { + for (const Variable& var : variables) { + AddRoot(worklist, var); + } +} + +void MarkAndSweepGC::AddAllVariables(std::queue& worklist, VariableStack variables) { + while (!variables.empty()) { + AddRoot(worklist, variables.top()); + variables.pop(); + } +} + +void MarkAndSweepGC::AddRoot(std::queue& worklist, const Variable& var) { + if (!std::holds_alternative(var)) { + return; + } + + void* ptr = std::get(var); + + if (ptr) { + worklist.push(ptr); + } +} + +} // namespace ovum::vm::runtime diff --git a/lib/runtime/gc/MarkAndSweepGC.hpp b/lib/runtime/gc/MarkAndSweepGC.hpp new file mode 100644 index 0000000..74f74d2 --- /dev/null +++ b/lib/runtime/gc/MarkAndSweepGC.hpp @@ -0,0 +1,28 @@ +#ifndef RUNTIME_MARKANDSWEEPGC_HPP +#define RUNTIME_MARKANDSWEEPGC_HPP + +#include +#include + +#include "lib/runtime/Variable.hpp" +#include "lib/runtime/gc/IGarbageCollector.hpp" + +namespace ovum::vm::runtime { + +class MarkAndSweepGC : public IGarbageCollector { +public: + std::expected Collect(execution_tree::PassedExecutionData& data) override; + +private: + static void Mark(execution_tree::PassedExecutionData& data); + static std::expected Sweep(execution_tree::PassedExecutionData& data); + + static void AddRoots(std::queue& worklist, execution_tree::PassedExecutionData& data); + static void AddAllVariables(std::queue& worklist, const std::vector& variables); + static void AddAllVariables(std::queue& worklist, VariableStack variables); + static void AddRoot(std::queue& worklist, const Variable& var); +}; + +} // namespace ovum::vm::runtime + +#endif // RUNTIME_MARKANDSWEEPGC_HPP diff --git a/lib/runtime/gc/reference_scanners/ArrayReferenceScanner.cpp b/lib/runtime/gc/reference_scanners/ArrayReferenceScanner.cpp new file mode 100644 index 0000000..5ead9ea --- /dev/null +++ b/lib/runtime/gc/reference_scanners/ArrayReferenceScanner.cpp @@ -0,0 +1,18 @@ +#include "ArrayReferenceScanner.hpp" + +#include + +#include "lib/runtime/ObjectDescriptor.hpp" + +namespace ovum::vm::runtime { + +void ArrayReferenceScanner::Scan(void* obj, const std::vector&, const ReferenceVisitor& visitor) const { + const char* base = reinterpret_cast(obj) + sizeof(ObjectDescriptor); + const std::vector& vec = *reinterpret_cast*>(base); + + for (void* p : vec) { + visitor(p); + } +} + +} // namespace ovum::vm::runtime diff --git a/lib/runtime/gc/reference_scanners/ArrayReferenceScanner.hpp b/lib/runtime/gc/reference_scanners/ArrayReferenceScanner.hpp new file mode 100644 index 0000000..0718c19 --- /dev/null +++ b/lib/runtime/gc/reference_scanners/ArrayReferenceScanner.hpp @@ -0,0 +1,15 @@ +#ifndef RUNTIME_ARRAYREFERENCESCANNER_HPP +#define RUNTIME_ARRAYREFERENCESCANNER_HPP + +#include "IReferenceScanner.hpp" + +namespace ovum::vm::runtime { + +class ArrayReferenceScanner : public IReferenceScanner { +public: + void Scan(void* obj, const std::vector& fields, const ReferenceVisitor& visitor) const override; +}; + +} // namespace ovum::vm::runtime + +#endif // RUNTIME_ARRAYREFERENCESCANNER_HPP diff --git a/lib/runtime/gc/reference_scanners/DefaultReferenceScanner.cpp b/lib/runtime/gc/reference_scanners/DefaultReferenceScanner.cpp new file mode 100644 index 0000000..9aefad8 --- /dev/null +++ b/lib/runtime/gc/reference_scanners/DefaultReferenceScanner.cpp @@ -0,0 +1,25 @@ + +#include "DefaultReferenceScanner.hpp" + +#include + +#include "lib/runtime/FieldInfo.hpp" +#include "lib/runtime/Variable.hpp" + +namespace ovum::vm::runtime { + +void DefaultReferenceScanner::Scan(void* obj, + const std::vector& fields, + const ReferenceVisitor& visitor) const { + for (const FieldInfo& field : fields) { + const Variable& var = field.variable_accessor->GetVariable(reinterpret_cast(obj) + field.offset); + + if (!std::holds_alternative(var)) { + continue; + } + + visitor(std::get(var)); + } +} + +} // namespace ovum::vm::runtime diff --git a/lib/runtime/gc/reference_scanners/DefaultReferenceScanner.hpp b/lib/runtime/gc/reference_scanners/DefaultReferenceScanner.hpp new file mode 100644 index 0000000..a3b619f --- /dev/null +++ b/lib/runtime/gc/reference_scanners/DefaultReferenceScanner.hpp @@ -0,0 +1,15 @@ +#ifndef RUNTIME_DEFAULTREFERENCESCANNER_HPP +#define RUNTIME_DEFAULTREFERENCESCANNER_HPP + +#include "IReferenceScanner.hpp" + +namespace ovum::vm::runtime { + +class DefaultReferenceScanner : public IReferenceScanner { +public: + void Scan(void* obj, const std::vector& fields, const ReferenceVisitor& visitor) const override; +}; + +} // namespace ovum::vm::runtime + +#endif // RUNTIME_DEFAULTREFERENCESCANNER_HPP diff --git a/lib/runtime/gc/reference_scanners/IReferenceScanner.hpp b/lib/runtime/gc/reference_scanners/IReferenceScanner.hpp new file mode 100644 index 0000000..21adbbe --- /dev/null +++ b/lib/runtime/gc/reference_scanners/IReferenceScanner.hpp @@ -0,0 +1,21 @@ +#ifndef RUNTIME_IREFERENCESCANNER_HPP +#define RUNTIME_IREFERENCESCANNER_HPP + +#include +#include + +#include "lib/runtime/FieldInfo.hpp" + +namespace ovum::vm::runtime { + +using ReferenceVisitor = std::function; + +class IReferenceScanner { // NOLINT(cppcoreguidelines-special-member-functions) +public: + virtual ~IReferenceScanner() = default; + virtual void Scan(void* obj, const std::vector& fields, const ReferenceVisitor& visitor) const = 0; +}; + +} // namespace ovum::vm::runtime + +#endif // RUNTIME_IREFERENCESCANNER_HPP diff --git a/lib/vm_ui/vm_ui_functions.cpp b/lib/vm_ui/vm_ui_functions.cpp index 1a8584b..47f8614 100644 --- a/lib/vm_ui/vm_ui_functions.cpp +++ b/lib/vm_ui/vm_ui_functions.cpp @@ -16,15 +16,16 @@ #include "lib/executor/Executor.hpp" #include "lib/executor/IJitExecutorFactory.hpp" #include "lib/executor/builtin_factory.hpp" -#include "lib/runtime/ObjectDescriptor.hpp" #include "lib/runtime/RuntimeMemory.hpp" #include "lib/runtime/VirtualTableRepository.hpp" +#include "lib/runtime/gc/MarkAndSweepGC.hpp" #ifdef JIT_PROVIDED #include #endif constexpr size_t kDefaultJitBoundary = 100000; +constexpr size_t kDefaultMaxObjects = 10000; std::string ReadFileContent(const std::string& file_path, std::ostream& err) { std::ifstream file(file_path); @@ -58,6 +59,8 @@ int32_t StartVmConsoleUI(const std::vector& args, std::ostream& out ArgumentParser::ArgParser arg_parser("ovum-vm", PassArgumentTypes()); arg_parser.AddCompositeArgument('f', "file", "Path to the bytecode file").AddIsGood(is_file).AddValidate(is_file); arg_parser.AddUnsignedLongLongArgument('j', "jit-boundary", "JIT compilation boundary").Default(kDefaultJitBoundary); + arg_parser.AddUnsignedLongLongArgument('m', "max-objects", "Maximum number of objects to keep in memory") + .Default(kDefaultMaxObjects); arg_parser.AddHelp('h', "help", "Show this help message"); bool parse_result = arg_parser.Parse(parser_args, {.out_stream = err, .print_messages = true}); @@ -75,6 +78,7 @@ int32_t StartVmConsoleUI(const std::vector& args, std::ostream& out file_path = arg_parser.GetCompositeValue("file"); size_t jit_boundary = arg_parser.GetUnsignedLongLongValue("jit-boundary"); + size_t max_objects = arg_parser.GetUnsignedLongLongValue("max-objects"); std::string sample = ReadFileContent(file_path, err); if (sample.empty()) { @@ -86,10 +90,11 @@ int32_t StartVmConsoleUI(const std::vector& args, std::ostream& out ovum::vm::execution_tree::FunctionRepository func_repo; ovum::vm::runtime::VirtualTableRepository vtable_repo; ovum::vm::runtime::RuntimeMemory memory; + ovum::vm::runtime::MemoryManager memory_manager(std::make_unique(), max_objects); ovum::vm::execution_tree::PassedExecutionData execution_data{.memory = memory, .virtual_table_repository = vtable_repo, .function_repository = func_repo, - .allocator = std::allocator(), + .memory_manager = memory_manager, .input_stream = in, .output_stream = out, .error_stream = err}; @@ -149,55 +154,10 @@ int32_t StartVmConsoleUI(const std::vector& args, std::ostream& out return_code = 4; } - for (size_t i = 0; i < memory.object_repository.GetCount(); ++i) { - auto object_result = memory.object_repository.GetByIndex(i); - - if (!object_result.has_value()) { - err << "Failed to get object: " << object_result.error().what() << "\n"; - return_code = 4; - continue; - } - - ovum::vm::runtime::ObjectDescriptor* object = object_result.value(); - uint32_t vtable_index = object->vtable_index; - auto vtable_result = vtable_repo.GetByIndex(vtable_index); - - if (!vtable_result.has_value()) { - err << "Failed to get vtable for index " << vtable_index << ": " << vtable_result.error().what() << "\n"; - return_code = 4; - continue; - } - - auto destructor_id_result = vtable_result.value()->GetRealFunctionId("_destructor_"); - - if (!destructor_id_result.has_value()) { - err << "Failed to get destructor for index " << vtable_index << ": " << destructor_id_result.error().what() - << "\n"; - return_code = 4; - continue; - } - - auto destructor_function = execution_data.function_repository.GetById(destructor_id_result.value()); - - if (!destructor_function.has_value()) { - err << "Failed to get destructor function for index " << vtable_index << ": " - << destructor_function.error().what() << "\n"; - return_code = 4; - continue; - } - - execution_data.memory.machine_stack.emplace(object); - auto destructor_exec_result = destructor_function.value()->Execute(execution_data); - - if (!destructor_exec_result.has_value()) { - err << "Failed to execute destructor for index " << vtable_index << ": " << destructor_exec_result.error().what() - << "\n"; - return_code = 4; - continue; - } - - size_t object_size = vtable_result.value()->GetSize(); - execution_data.allocator.deallocate(reinterpret_cast(object), object_size); + auto clear_result = memory_manager.Clear(execution_data); + if (!clear_result.has_value()) { + err << "Warning: Failed to clean up objects during shutdown: " << clear_result.error().what() << "\n"; + return_code = 4; } return return_code; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 16d5c66..11bbab1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,6 +12,8 @@ add_executable( bytecode_parser_tests.cpp bytecode_commands_tests.cpp builtin_functions_tests.cpp + gc_tests.cpp + test_suites/GcTestSuite.cpp ) target_link_libraries( diff --git a/tests/builtin_functions_tests.cpp b/tests/builtin_functions_tests.cpp index 7826e44..bd1cdd1 100644 --- a/tests/builtin_functions_tests.cpp +++ b/tests/builtin_functions_tests.cpp @@ -27,8 +27,7 @@ void* AllocateObjectByName(BuiltinTestSuite& suite, const std::string& vtable_na if (!idx.has_value()) { return nullptr; } - auto obj_result = ovum::vm::runtime::AllocateObject( - *vt.value(), static_cast(idx.value()), suite.memory_.object_repository, suite.allocator_); + auto obj_result = suite.memory_manager_.AllocateObject(*vt.value(), static_cast(idx.value()), suite.data_); EXPECT_TRUE(obj_result.has_value()) << "Allocation failed for " << vtable_name; if (!obj_result.has_value()) { return nullptr; diff --git a/tests/bytecode_commands_tests.cpp b/tests/bytecode_commands_tests.cpp index 8e56a38..d7495fa 100644 --- a/tests/bytecode_commands_tests.cpp +++ b/tests/bytecode_commands_tests.cpp @@ -857,10 +857,8 @@ TEST_F(BuiltinTestSuite, CallVirtualConstructorAndFields) { (void) func_idx; (void) destructor_idx; - auto obj_res = ovum::vm::runtime::AllocateObject(*vtable_repo_.GetByIndex(vt_index.value()).value(), - static_cast(vt_index.value()), - memory_.object_repository, - allocator_); + auto obj_res = memory_manager_.AllocateObject( + *vtable_repo_.GetByIndex(vt_index.value()).value(), static_cast(vt_index.value()), data_); ASSERT_TRUE(obj_res.has_value()); void* obj = obj_res.value(); diff --git a/tests/bytecode_parser_tests.cpp b/tests/bytecode_parser_tests.cpp index 60e3636..b9d6e26 100644 --- a/tests/bytecode_parser_tests.cpp +++ b/tests/bytecode_parser_tests.cpp @@ -67,7 +67,7 @@ TEST_F(BytecodeParserTestSuite, InitStatic_WithNestedWhile) { TEST_F(BytecodeParserTestSuite, InitStatic_AllCommandTypes) { auto parser = CreateParserWithJit(); auto tokens = TokenizeString( - R"(init-static { PushString "hello" PushChar "a" PushInt 42 PushFloat 3.14 PushBool true NewArray arrName Call funcName Return })"); + R"(init-static { PushString "hello" PushChar 42 PushInt 42 PushFloat 3.14 PushBool true Call funcName Return })"); ovum::vm::execution_tree::FunctionRepository func_repo; ovum::vm::runtime::VirtualTableRepository vtable_repo; @@ -739,7 +739,7 @@ TEST_F(BytecodeParserTestSuite, Command_String_PushString) { TEST_F(BytecodeParserTestSuite, Command_String_PushChar) { auto parser = CreateParserWithJit(); - auto tokens = TokenizeString(R"(init-static { PushChar "a" })"); + auto tokens = TokenizeString(R"(init-static { PushChar 42 })"); ovum::vm::execution_tree::FunctionRepository func_repo; ovum::vm::runtime::VirtualTableRepository vtable_repo; @@ -809,15 +809,6 @@ TEST_F(BytecodeParserTestSuite, Command_Boolean_PushBool_False) { auto parsing_result = ParseSuccessfully(parser, tokens, func_repo, vtable_repo); } -TEST_F(BytecodeParserTestSuite, Command_Identifier_NewArray) { - auto parser = CreateParserWithJit(); - auto tokens = TokenizeString("init-static { NewArray arrName }"); - ovum::vm::execution_tree::FunctionRepository func_repo; - ovum::vm::runtime::VirtualTableRepository vtable_repo; - - auto parsing_result = ParseSuccessfully(parser, tokens, func_repo, vtable_repo); -} - TEST_F(BytecodeParserTestSuite, Command_Identifier_Call) { auto parser = CreateParserWithJit(); auto tokens = TokenizeString("init-static { Call funcName }"); diff --git a/tests/gc_tests.cpp b/tests/gc_tests.cpp new file mode 100644 index 0000000..8aa47b1 --- /dev/null +++ b/tests/gc_tests.cpp @@ -0,0 +1,186 @@ +#include + +#include "tests/test_suites/GcTestSuite.hpp" + +#include +#include + +#include "lib/execution_tree/PassedExecutionData.hpp" + +TEST_F(GcTestSuite, UnreachableObjectCollected) { + auto data = MakeFreshData(); + + void* obj = AllocateTestObject("Simple", data); + + auto before = SnapshotRepo(mm_.GetRepository()); + ASSERT_EQ(before.size(), 1u); + ASSERT_TRUE(RepoContains(mm_.GetRepository(), obj)); + + CollectGarbage(data); + + auto after = SnapshotRepo(mm_.GetRepository()); + EXPECT_EQ(after.size(), 0u); + EXPECT_FALSE(RepoContains(mm_.GetRepository(), obj)); +} + +TEST_F(GcTestSuite, RootInGlobalVariablesSurvives) { + auto data = MakeFreshData(); + + void* obj = AllocateTestObject("Simple", data); + data.memory.global_variables.emplace_back(obj); + + CollectGarbage(data); + + EXPECT_TRUE(RepoContains(mm_.GetRepository(), obj)); + EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 1u); +} + +TEST_F(GcTestSuite, RootInMachineStackSurvives) { + auto data = MakeFreshData(); + + void* obj = AllocateTestObject("Simple", data); + data.memory.machine_stack.emplace(obj); + + CollectGarbage(data); + + EXPECT_TRUE(RepoContains(mm_.GetRepository(), obj)); +} + +TEST_F(GcTestSuite, TransitiveReachabilityThroughField) { + auto data = MakeFreshData(); + + void* root = AllocateTestObject("WithRef", data); + void* child = AllocateTestObject("Simple", data); + + SetRef(root, child); + + data.memory.global_variables.emplace_back(root); + + CollectGarbage(data); + + EXPECT_TRUE(RepoContains(mm_.GetRepository(), root)); + EXPECT_TRUE(RepoContains(mm_.GetRepository(), child)); + EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 2u); +} + +TEST_F(GcTestSuite, TransitiveReachabilityThroughArray) { + auto data = MakeFreshData(); + + void* arr = AllocateTestObject("Array", data); + InitArray(arr); + + void* child1 = AllocateTestObject("Simple", data); + void* child2 = AllocateTestObject("Simple", data); + + AddToArray(arr, child1); + AddToArray(arr, child2); + + data.memory.global_variables.emplace_back(arr); + + CollectGarbage(data); + + EXPECT_TRUE(RepoContains(mm_.GetRepository(), arr)); + EXPECT_TRUE(RepoContains(mm_.GetRepository(), child1)); + EXPECT_TRUE(RepoContains(mm_.GetRepository(), child2)); + EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 3u); +} + +TEST_F(GcTestSuite, CycleWithoutRootsCollected) { + auto data = MakeFreshData(); + + void* obj_a = AllocateTestObject("WithRef", data); + void* obj_b = AllocateTestObject("WithRef", data); + + SetRef(obj_a, obj_b); + SetRef(obj_b, obj_a); + + CollectGarbage(data); + + EXPECT_FALSE(RepoContains(mm_.GetRepository(), obj_a)); + EXPECT_FALSE(RepoContains(mm_.GetRepository(), obj_b)); + EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 0u); +} + +TEST_F(GcTestSuite, CycleWithRootPreserved) { + auto data = MakeFreshData(); + + void* obj_a = AllocateTestObject("WithRef", data); + void* obj_b = AllocateTestObject("WithRef", data); + + SetRef(obj_a, obj_b); + SetRef(obj_b, obj_a); + + data.memory.global_variables.emplace_back(obj_a); + + CollectGarbage(data); + + EXPECT_TRUE(RepoContains(mm_.GetRepository(), obj_a)); + EXPECT_TRUE(RepoContains(mm_.GetRepository(), obj_b)); + EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 2u); +} + +TEST_F(GcTestSuite, MultipleRootsDifferentPlaces) { + auto data = MakeFreshData(); + + void* global = AllocateTestObject("Simple", data); + void* stack = AllocateTestObject("Simple", data); + void* local = AllocateTestObject("Simple", data); + + data.memory.global_variables.emplace_back(global); + data.memory.machine_stack.emplace(stack); + + ovum::vm::runtime::StackFrame frame; + frame.local_variables.emplace_back(local); + data.memory.stack_frames.push(std::move(frame)); + + CollectGarbage(data); + + EXPECT_TRUE(RepoContains(mm_.GetRepository(), global)); + EXPECT_TRUE(RepoContains(mm_.GetRepository(), stack)); + EXPECT_TRUE(RepoContains(mm_.GetRepository(), local)); + EXPECT_EQ(SnapshotRepo(mm_.GetRepository()).size(), 3u); +} + +TEST_F(GcTestSuite, SmallThresholdFrequentAllocations) { + auto data = MakeFreshData(3); + + std::vector objects; + std::vector should_survive; + + const int k_iterations = 15; + + for (int i = 0; i < k_iterations; ++i) { + void* obj = AllocateTestObject("Simple", data); + objects.push_back(obj); + + if (i % 3 == 0) { + data.memory.global_variables.emplace_back(obj); + should_survive.push_back(obj); + } + + auto gc_res = data.memory_manager.CollectGarbageIfRequired(data); + ASSERT_TRUE(gc_res.has_value()); + } + + CollectGarbage(data); + + auto snapshot = SnapshotRepo(mm_.GetRepository()); + EXPECT_GE(snapshot.size(), should_survive.size()); + + for (void* obj : should_survive) { + EXPECT_TRUE(RepoContains(mm_.GetRepository(), obj)); + } +} + +TEST_F(GcTestSuite, NullReferencesNotCrashing) { + auto data = MakeFreshData(); + + void* root = AllocateTestObject("WithRef", data); + SetRef(root, nullptr); + + data.memory.global_variables.emplace_back(root); + + ASSERT_NO_FATAL_FAILURE(CollectGarbage(data)); + + EXPECT_TRUE(RepoContains(mm_.GetRepository(), root)); +} diff --git a/tests/main_test.cpp b/tests/main_test.cpp index 9ce2568..4b6c049 100644 --- a/tests/main_test.cpp +++ b/tests/main_test.cpp @@ -20,12 +20,14 @@ TEST_F(ProjectIntegrationTestSuite, NegitiveOutputTest1) { std::istringstream in; std::ostringstream err; StartVmConsoleUI(SplitString("test"), out, in, err); - ASSERT_EQ(err.str(), - "Not enough values were passed to argument --file.\n" - "ovum-vm\nShow this help message\n\nOPTIONS:\n" - "-f, --file=: Path to the bytecode file\n" - "-j, --jit-boundary=: JIT compilation boundary [default = 100000]\n\n" - "-h, --help: Display this help and exit\n\n"); + ASSERT_EQ( + err.str(), + "Not enough values were passed to argument --file.\n" + "ovum-vm\nShow this help message\n\nOPTIONS:\n" + "-f, --file=: Path to the bytecode file\n" + "-j, --jit-boundary=: JIT compilation boundary [default = 100000]\n" + "-m, --max-objects=: Maximum number of objects to keep in memory [default = 10000]\n\n" + "-h, --help: Display this help and exit\n\n"); } TEST_F(ProjectIntegrationTestSuite, HelpTest) { @@ -33,11 +35,13 @@ TEST_F(ProjectIntegrationTestSuite, HelpTest) { std::istringstream in; std::ostringstream err; StartVmConsoleUI(SplitString("test --help"), out, in, err); - ASSERT_EQ(err.str(), - "ovum-vm\nShow this help message\n\nOPTIONS:\n" - "-f, --file=: Path to the bytecode file\n" - "-j, --jit-boundary=: JIT compilation boundary [default = 100000]\n\n" - "-h, --help: Display this help and exit\n\n"); + ASSERT_EQ( + err.str(), + "ovum-vm\nShow this help message\n\nOPTIONS:\n" + "-f, --file=: Path to the bytecode file\n" + "-j, --jit-boundary=: JIT compilation boundary [default = 100000]\n" + "-m, --max-objects=: Maximum number of objects to keep in memory [default = 10000]\n\n" + "-h, --help: Display this help and exit\n\n"); } TEST_F(ProjectIntegrationTestSuite, FibTest1) { @@ -245,5 +249,36 @@ TEST_F(ProjectIntegrationTestSuite, InteropTest3) { int64_t time_from_code = code - kAdded; time_from_code *= kDivisor; time_from_code ^= kBinaryFilter; - ASSERT_TRUE(std::abs(time_from_code - current_nanotime) <= kMaxAllowedError); + ASSERT_LE(std::abs(time_from_code - current_nanotime), kMaxAllowedError); +} + +TEST_F(ProjectIntegrationTestSuite, MemcheckTest) { + TestData test_data{ + .test_name = "memcheck.oil", + .arguments = "", + .input = "", + .expected_output = "", + .expected_error = "", + .expected_return_code = 0, + }; + + std::filesystem::path test_file = kTestDataDir; + test_file /= "examples"; + test_file /= "compiled"; + test_file /= test_data.test_name; + std::string cmd = "ovum-vm -f \""; + cmd += test_file.string(); + cmd += "\""; + cmd += " -m 10000"; + + if (!test_data.arguments.empty()) { + cmd += " -- "; + cmd += test_data.arguments; + } + + std::istringstream in(test_data.input); + std::ostringstream out; + std::ostringstream err; + ASSERT_EQ(StartVmConsoleUI(SplitString(cmd), out, in, err), test_data.expected_return_code); + ASSERT_EQ(err.str(), test_data.expected_error); } diff --git a/tests/test_data/examples b/tests/test_data/examples index 7c3c96c..51cb78d 160000 --- a/tests/test_data/examples +++ b/tests/test_data/examples @@ -1 +1 @@ -Subproject commit 7c3c96c065f2e1904f66b3f2d5c926863fb2a201 +Subproject commit 51cb78d7778c97ac9d4561a210a003e94b59fe83 diff --git a/tests/test_suites/BuiltinTestSuite.cpp b/tests/test_suites/BuiltinTestSuite.cpp index d403a6a..277263e 100644 --- a/tests/test_suites/BuiltinTestSuite.cpp +++ b/tests/test_suites/BuiltinTestSuite.cpp @@ -1,14 +1,13 @@ #include "BuiltinTestSuite.hpp" #include -#include #include "lib/execution_tree/ExecutionResult.hpp" -#include "lib/execution_tree/Function.hpp" #include "lib/execution_tree/command_factory.hpp" #include "lib/executor/BuiltinFunctions.hpp" #include "lib/executor/builtin_factory.hpp" #include "lib/runtime/ByteArray.hpp" +#include "lib/runtime/MemoryManager.hpp" #include "lib/runtime/ObjectDescriptor.hpp" using ovum::vm::execution_tree::CreateBooleanCommandByName; @@ -17,17 +16,15 @@ using ovum::vm::execution_tree::CreateIntegerCommandByName; using ovum::vm::execution_tree::CreateSimpleCommandByName; using ovum::vm::execution_tree::CreateStringCommandByName; using ovum::vm::execution_tree::ExecutionResult; -using ovum::vm::execution_tree::Function; using ovum::vm::execution_tree::IExecutable; using ovum::vm::execution_tree::PassedExecutionData; -using ovum::vm::runtime::AllocateObject; using ovum::vm::runtime::GetDataPointer; using ovum::vm::runtime::ObjectDescriptor; using ovum::vm::runtime::StackFrame; using ovum::vm::runtime::Variable; BuiltinTestSuite::BuiltinTestSuite() : - data_(memory_, vtable_repo_, function_repo_, allocator_, input_stream_, output_stream_, error_stream_) { + data_(memory_, vtable_repo_, function_repo_, memory_manager_, input_stream_, output_stream_, error_stream_) { } void BuiltinTestSuite::SetUp() { @@ -43,13 +40,15 @@ void BuiltinTestSuite::SetUp() { } void BuiltinTestSuite::TearDown() { - CleanupObjects(); while (!memory_.machine_stack.empty()) { memory_.machine_stack.pop(); } while (!memory_.stack_frames.empty()) { memory_.stack_frames.pop(); } + + auto clear_result = memory_manager_.Clear(data_); + ASSERT_TRUE(clear_result.has_value()) << clear_result.error().what(); } std::unique_ptr BuiltinTestSuite::MakeSimple(const std::string& name) { @@ -91,6 +90,7 @@ std::unique_ptr BuiltinTestSuite::MakeFloatCmd(const std::string& n std::unique_ptr BuiltinTestSuite::MakeBoolCmd(const std::string& name, bool arg) { auto cmd = CreateBooleanCommandByName(name, arg); EXPECT_TRUE(cmd.has_value()) << "Boolean command not found: " << name; + if (!cmd.has_value()) { return nullptr; } @@ -106,12 +106,12 @@ void* BuiltinTestSuite::MakeString(const std::string& value) { if (!idx.has_value()) { return nullptr; } - auto obj_result = - AllocateObject(*vt.value(), static_cast(idx.value()), memory_.object_repository, data_.allocator); - if (!obj_result.has_value()) { + + auto obj_res = memory_manager_.AllocateObject(*vt.value(), static_cast(idx.value()), data_); + if (!obj_res.has_value()) return nullptr; - } - void* obj = obj_result.value(); + + void* obj = obj_res.value(); auto* str_ptr = GetDataPointer(obj); new (str_ptr) std::string(value); return obj; @@ -126,13 +126,12 @@ void* BuiltinTestSuite::MakeNullable(void* wrapped) { if (!idx.has_value()) { return nullptr; } - auto obj_result = - AllocateObject(*vt.value(), static_cast(idx.value()), memory_.object_repository, data_.allocator); - if (!obj_result.has_value()) { + + auto obj_res = memory_manager_.AllocateObject(*vt.value(), static_cast(idx.value()), data_); + if (!obj_res.has_value()) return nullptr; - } - void* obj = obj_result.value(); + void* obj = obj_res.value(); auto* nullable_ptr = GetDataPointer(obj); *nullable_ptr = wrapped; return obj; @@ -148,12 +147,11 @@ void* BuiltinTestSuite::MakeStringArray(const std::vector& values) return nullptr; } - auto obj_result = - AllocateObject(*vt.value(), static_cast(idx.value()), memory_.object_repository, data_.allocator); - if (!obj_result.has_value()) { + auto obj_res = memory_manager_.AllocateObject(*vt.value(), static_cast(idx.value()), data_); + if (!obj_res.has_value()) return nullptr; - } - void* obj = obj_result.value(); + + void* obj = obj_res.value(); auto* vec_ptr = GetDataPointer>(obj); new (vec_ptr) std::vector(); for (const auto& val : values) { @@ -172,13 +170,12 @@ void* BuiltinTestSuite::MakeByteArray(const std::vector& values) { return nullptr; } - auto obj_result = - AllocateObject(*vt.value(), static_cast(idx.value()), memory_.object_repository, data_.allocator); - if (!obj_result.has_value()) { + auto obj_res = memory_manager_.AllocateObject(*vt.value(), static_cast(idx.value()), data_); + if (!obj_res.has_value()) { return nullptr; } - void* obj = obj_result.value(); + void* obj = obj_res.value(); auto* byte_array = GetDataPointer(obj); new (byte_array) ovum::vm::runtime::ByteArray(values.size()); if (values.size() > 0) { @@ -269,12 +266,7 @@ void BuiltinTestSuite::ExpectTopNullableHasValue(bool has_value) { void BuiltinTestSuite::CleanupObjects() { std::vector objects; - for (size_t i = 0; i < memory_.object_repository.GetCount(); ++i) { - auto obj = memory_.object_repository.GetByIndex(i); - if (obj.has_value()) { - objects.push_back(obj.value()); - } - } + memory_.object_repository.ForAll([&objects](void* obj) { objects.push_back(obj); }); for (void* obj : objects) { DestroyObject(obj); @@ -297,5 +289,6 @@ void BuiltinTestSuite::DestroyObject(void* obj) { data_.memory.machine_stack.emplace(obj); auto destructor_exec_result = destructor_function.value()->Execute(data_); - allocator_.deallocate(reinterpret_cast(obj), vt.value()->GetSize()); + auto dealloc_res = memory_manager_.DeallocateObject(obj, data_); + ASSERT_TRUE(dealloc_res.has_value()) << dealloc_res.error().what(); } diff --git a/tests/test_suites/BuiltinTestSuite.hpp b/tests/test_suites/BuiltinTestSuite.hpp index 6b02927..ebb890c 100644 --- a/tests/test_suites/BuiltinTestSuite.hpp +++ b/tests/test_suites/BuiltinTestSuite.hpp @@ -15,6 +15,7 @@ #include "lib/execution_tree/PassedExecutionData.hpp" #include "lib/runtime/RuntimeMemory.hpp" #include "lib/runtime/VirtualTableRepository.hpp" +#include "lib/runtime/gc/MarkAndSweepGC.hpp" struct BuiltinTestSuite : public testing::Test { BuiltinTestSuite(); @@ -58,6 +59,8 @@ struct BuiltinTestSuite : public testing::Test { void CleanupObjects(); void DestroyObject(void* obj); + constexpr static size_t kDefaultMaxObjects = 10000; + // Repositories for fixtures ovum::vm::runtime::RuntimeMemory memory_{}; ovum::vm::runtime::VirtualTableRepository vtable_repo_{}; @@ -65,7 +68,8 @@ struct BuiltinTestSuite : public testing::Test { std::stringstream input_stream_; std::stringstream output_stream_; std::stringstream error_stream_; - std::allocator allocator_{}; + ovum::vm::runtime::MemoryManager memory_manager_{std::make_unique(), + kDefaultMaxObjects}; ovum::vm::execution_tree::PassedExecutionData data_; }; diff --git a/tests/test_suites/BytecodeParserTestSuite.hpp b/tests/test_suites/BytecodeParserTestSuite.hpp index e97025e..6834a8d 100644 --- a/tests/test_suites/BytecodeParserTestSuite.hpp +++ b/tests/test_suites/BytecodeParserTestSuite.hpp @@ -10,7 +10,7 @@ #include #include "lib/bytecode_parser/BytecodeParser.hpp" -#include "lib/bytecode_parser/scenarios/PlaceholderCommandFactory.hpp" +#include "lib/bytecode_parser/scenarios/CommandFactory.hpp" #include "lib/execution_tree/Block.hpp" #include "lib/execution_tree/FunctionRepository.hpp" #include "lib/execution_tree/IFunctionExecutable.hpp" @@ -58,7 +58,7 @@ struct BytecodeParserTestSuite : public testing::Test { private: std::unique_ptr jit_factory_; - ovum::bytecode::parser::PlaceholderCommandFactory command_factory_; + ovum::bytecode::parser::CommandFactory command_factory_; ovum::vm::runtime::RuntimeMemory memory_; }; diff --git a/tests/test_suites/GcTestSuite.cpp b/tests/test_suites/GcTestSuite.cpp new file mode 100644 index 0000000..5e2c07a --- /dev/null +++ b/tests/test_suites/GcTestSuite.cpp @@ -0,0 +1,195 @@ +#include "GcTestSuite.hpp" +#include "lib/execution_tree/Block.hpp" +#include "lib/execution_tree/Command.hpp" +#include "lib/execution_tree/Function.hpp" +#include "lib/executor/builtin_factory.hpp" + +GcTestSuite::GcTestSuite() : + vtr_(), fr_(), mm_(std::make_unique(), kDefaultGCThreshold), rm_() { +} + +ovum::vm::execution_tree::PassedExecutionData GcTestSuite::MakeFreshData(uint64_t gc_threshold) { + mm_ = ovum::vm::runtime::MemoryManager(std::make_unique(), gc_threshold); + ovum::vm::execution_tree::PassedExecutionData data{.memory = rm_, + .virtual_table_repository = vtr_, + .function_repository = fr_, + .memory_manager = mm_, + .input_stream = std::cin, + .output_stream = std::cout, + .error_stream = std::cerr}; + return data; +} + +ovum::vm::execution_tree::ExecutionResult NoOpDestructor(ovum::vm::execution_tree::PassedExecutionData&) { + return ovum::vm::execution_tree::ExecutionResult::kNormal; +} + +void GcTestSuite::SetUp() { + vtr_ = ovum::vm::runtime::VirtualTableRepository(); + + fr_ = ovum::vm::execution_tree::FunctionRepository(); + + { + auto vt_res = ovum::vm::runtime::RegisterBuiltinVirtualTables(vtr_); + ASSERT_TRUE(vt_res.has_value()) << "Failed builtin vtables: " << vt_res.error().what(); + } + + { + auto fn_res = ovum::vm::execution_tree::RegisterBuiltinFunctions(fr_); + ASSERT_TRUE(fn_res.has_value()) << "Failed builtin functions: " << fn_res.error().what(); + } + + RegisterTestVtables(); + + RegisterNoOpDestructors(); + + auto gc = std::make_unique(); + mm_ = ovum::vm::runtime::MemoryManager(std::move(gc), kDefaultGCThreshold); +} + +void GcTestSuite::TearDown() { + ovum::vm::execution_tree::PassedExecutionData data{.memory = rm_, + .virtual_table_repository = vtr_, + .function_repository = fr_, + .memory_manager = mm_, + .input_stream = std::cin, + .output_stream = std::cout, + .error_stream = std::cerr}; + auto res = data.memory_manager.Clear(data); + ASSERT_TRUE(res.has_value()) << "Clear failed: " << res.error().what(); +} + +void GcTestSuite::RegisterTestVtables() { + { + ovum::vm::runtime::VirtualTable vt("Simple", sizeof(ovum::vm::runtime::ObjectDescriptor)); + vt.AddFunction("_destructor_", "_Simple_destructor_"); + auto res = vtr_.Add(std::move(vt)); + ASSERT_TRUE(res.has_value()) << "Failed Simple: " << res.error().what(); + } + + { + ovum::vm::runtime::VirtualTable vt("WithRef", sizeof(ovum::vm::runtime::ObjectDescriptor) + sizeof(void*)); + vt.AddFunction("_destructor_", "_WithRef_destructor_"); + vt.AddField("Object", sizeof(ovum::vm::runtime::ObjectDescriptor)); + + auto res = vtr_.Add(std::move(vt)); + ASSERT_TRUE(res.has_value()); + } + + { + auto scanner = std::make_unique(); + ovum::vm::runtime::VirtualTable vt( + "Array", sizeof(ovum::vm::runtime::ObjectDescriptor) + sizeof(std::vector), std::move(scanner)); + vt.AddFunction("_destructor_", "_Array_destructor_"); + auto res = vtr_.Add(std::move(vt)); + ASSERT_TRUE(res.has_value()); + } +} + +void GcTestSuite::RegisterNoOpDestructors() { + const std::vector types = {"Simple", "WithRef"}; + + for (const auto& type : types) { + std::string func_name = "_" + type + "_destructor_"; + + auto body = std::make_unique(); + + auto no_op_cmd = [](ovum::vm::execution_tree::PassedExecutionData& d) + -> std::expected { return NoOpDestructor(d); }; + + body->AddStatement(std::make_unique>(no_op_cmd)); + + auto function = std::make_unique(func_name, 1u, std::move(body)); + + auto res = fr_.Add(std::move(function)); + + ASSERT_TRUE(res.has_value()) << "Failed to register destructor for " << type << ": " << res.error().what(); + } + + std::string func_name = "_Array_destructor_"; + + auto body = std::make_unique(); + + auto no_op_cmd = [](ovum::vm::execution_tree::PassedExecutionData& data) + -> std::expected { + if (!std::holds_alternative(data.memory.stack_frames.top().local_variables[0])) { + return std::unexpected(std::runtime_error("ArrayDestructor: invalid argument types")); + } + + using vector_type = std::vector; + void* obj_ptr = std::get(data.memory.stack_frames.top().local_variables[0]); + auto* vec_data = reinterpret_cast*>(reinterpret_cast(obj_ptr) + + sizeof(ovum::vm::runtime::ObjectDescriptor)); + vec_data->~vector_type(); + return NoOpDestructor(data); + }; + + body->AddStatement(std::make_unique>(no_op_cmd)); + + auto function = std::make_unique(func_name, 1u, std::move(body)); + + auto res = fr_.Add(std::move(function)); + + ASSERT_TRUE(res.has_value()) << "Failed to register destructor for " << "Array" << ": " << res.error().what(); +} + +std::unordered_set GcTestSuite::SnapshotRepo(const ovum::vm::runtime::ObjectRepository& repo) const { + std::unordered_set snapshot; + repo.ForAll([&snapshot](void* obj) { snapshot.insert(obj); }); + return snapshot; +} + +std::string GcTestSuite::TypeOf(void* obj, const ovum::vm::execution_tree::PassedExecutionData& data) const { + if (!obj) + return ""; + auto* desc = reinterpret_cast(obj); + auto vt_res = data.virtual_table_repository.GetByIndex(desc->vtable_index); + if (!vt_res.has_value()) + return ""; + return vt_res.value()->GetName(); +} + +bool GcTestSuite::RepoContains(const ovum::vm::runtime::ObjectRepository& repo, void* obj) const { + bool found = false; + repo.ForAll([&](void* o) { + if (o == obj) + found = true; + }); + return found; +} + +void* GcTestSuite::AllocateTestObject(const std::string& type_name, + ovum::vm::execution_tree::PassedExecutionData& data) { + auto vt_res = data.virtual_table_repository.GetByName(type_name); + EXPECT_TRUE(vt_res.has_value()) << "VTable not found: " << type_name; + + auto idx_res = data.virtual_table_repository.GetIndexByName(type_name); + EXPECT_TRUE(idx_res.has_value()); + + auto alloc_res = data.memory_manager.AllocateObject(*vt_res.value(), static_cast(*idx_res), data); + + EXPECT_TRUE(alloc_res.has_value()) << "Allocation failed for " << type_name; + return alloc_res.value(); +} + +void GcTestSuite::SetRef(void* obj, void* target) { + void** ptr = reinterpret_cast(reinterpret_cast(obj) + sizeof(ovum::vm::runtime::ObjectDescriptor)); + *ptr = target; +} + +void GcTestSuite::InitArray(void* array_obj) { + auto* vec = reinterpret_cast*>(reinterpret_cast(array_obj) + + sizeof(ovum::vm::runtime::ObjectDescriptor)); + new (vec) std::vector(); +} + +void GcTestSuite::AddToArray(void* array_obj, void* item) { + auto* vec = reinterpret_cast*>(reinterpret_cast(array_obj) + + sizeof(ovum::vm::runtime::ObjectDescriptor)); + vec->push_back(item); +} + +void GcTestSuite::CollectGarbage(ovum::vm::execution_tree::PassedExecutionData& data) { + auto res = data.memory_manager.CollectGarbage(data); + ASSERT_TRUE(res.has_value()) << "GC failed: " << res.error().what(); +} diff --git a/tests/test_suites/GcTestSuite.hpp b/tests/test_suites/GcTestSuite.hpp new file mode 100644 index 0000000..f6bc4d9 --- /dev/null +++ b/tests/test_suites/GcTestSuite.hpp @@ -0,0 +1,63 @@ +#ifndef GC_TEST_SUITE_HPP +#define GC_TEST_SUITE_HPP + +#include + +#include +#include +#include +#include +#include + +#include "lib/execution_tree/ExecutionResult.hpp" +#include "lib/execution_tree/FunctionRepository.hpp" +#include "lib/execution_tree/PassedExecutionData.hpp" +#include "lib/runtime/MemoryManager.hpp" +#include "lib/runtime/ObjectDescriptor.hpp" +#include "lib/runtime/ObjectRepository.hpp" +#include "lib/runtime/VirtualTable.hpp" +#include "lib/runtime/VirtualTableRepository.hpp" +#include "lib/runtime/gc/MarkAndSweepGC.hpp" +#include "lib/runtime/gc/reference_scanners/ArrayReferenceScanner.hpp" +#include "lib/runtime/gc/reference_scanners/DefaultReferenceScanner.hpp" + +const uint64_t kDefaultGCThreshold = 100; + +ovum::vm::execution_tree::ExecutionResult NoOpDestructor(ovum::vm::execution_tree::PassedExecutionData& data); + +class GcTestSuite : public ::testing::Test { +protected: + GcTestSuite(); + + void SetUp() override; + void TearDown() override; + + ovum::vm::execution_tree::PassedExecutionData MakeFreshData(uint64_t gc_threshold = kDefaultGCThreshold); + + void RegisterTestVtables(); + void RegisterNoOpDestructors(); + + [[nodiscard]] std::unordered_set SnapshotRepo(const ovum::vm::runtime::ObjectRepository& repo) const; + + std::string TypeOf(void* obj, const ovum::vm::execution_tree::PassedExecutionData& data) const; + + bool RepoContains(const ovum::vm::runtime::ObjectRepository& repo, void* obj) const; + + void* AllocateTestObject(const std::string& type_name, ovum::vm::execution_tree::PassedExecutionData& data); + + void SetRef(void* obj, void* target); + + void InitArray(void* array_obj); + + void AddToArray(void* array_obj, void* item); + + void CollectGarbage(ovum::vm::execution_tree::PassedExecutionData& data); + +protected: + ovum::vm::runtime::VirtualTableRepository vtr_; + ovum::vm::execution_tree::FunctionRepository fr_; + ovum::vm::runtime::MemoryManager mm_; + ovum::vm::runtime::RuntimeMemory rm_; +}; + +#endif // GC_TEST_SUITE_HPP