From a1bf4f16f370c83f53a72a55a958073c97ad8e87 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Mon, 30 Mar 2026 18:34:38 +0300 Subject: [PATCH 1/4] Implement bindings --- CMakeLists.txt | 8 +- include/korka/compiler/binding.hpp | 145 ++++++++ include/korka/compiler/binding_wrapper.hpp | 37 ++ include/korka/compiler/compiler.hpp | 409 +++++---------------- include/korka/compiler/info.hpp | 61 +++ include/korka/compiler/result.hpp | 223 +++++++++++ include/korka/utils/function_traits.hpp | 8 + include/korka/vm/bytecode_builder.hpp | 5 + include/korka/vm/context_base.hpp | 101 +++++ include/korka/vm/op_codes.hpp | 17 +- include/korka/vm/value.hpp | 25 ++ include/korka/vm/vm_runtime.hpp | 121 ++---- main.cpp | 42 ++- 13 files changed, 777 insertions(+), 425 deletions(-) create mode 100644 include/korka/compiler/binding.hpp create mode 100644 include/korka/compiler/binding_wrapper.hpp create mode 100644 include/korka/compiler/info.hpp create mode 100644 include/korka/compiler/result.hpp create mode 100644 include/korka/vm/context_base.hpp create mode 100644 include/korka/vm/value.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b4b6d04..f67a53f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ project(pxkorka ) -set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -48,6 +48,12 @@ add_library(korka_lib include/korka/utils/frozen_hash_string_view.hpp include/korka/utils/byte_reader.hpp include/korka/utils/function_traits.hpp + include/korka/compiler/binding.hpp + include/korka/compiler/info.hpp + include/korka/vm/value.hpp + include/korka/compiler/result.hpp + include/korka/compiler/binding_wrapper.hpp + include/korka/vm/context_base.hpp ) target_include_directories(korka_lib diff --git a/include/korka/compiler/binding.hpp b/include/korka/compiler/binding.hpp new file mode 100644 index 0000000..972feb2 --- /dev/null +++ b/include/korka/compiler/binding.hpp @@ -0,0 +1,145 @@ +#pragma once + +#include "frozen/unordered_map.h" +#include "info.hpp" +#include "korka/utils/function_traits.hpp" +#include "korka/vm/op_codes.hpp" +#include "korka/vm/value.hpp" +#include "korka/compiler/binding_wrapper.hpp" +#include +#include + +namespace korka { + template + struct function_binding { + vm_external_function_id id{}; + std::string_view name{}; + + std::size_t param_count{}; + std::array param_types{}; + + vm_external_function_type &wrapper; + + vm::type_info return_type; + + + template + static consteval auto from_function(vm_external_function_id idx, Wrapped wrapped) -> function_binding { + using function_type = std::remove_cvref_t; + using traits = function_traits; + using rtype = typename traits::return_type; + using args = typename traits::args_tuple; + + constexpr std::size_t arg_count = traits::args_count; + + static_assert(arg_count <= NMaxArgs, "Param count mismatch"); + + auto ptypes = [](std::index_sequence) { + return std::array{ + vm::cpp_t_to_type_info>()... + }; + }(std::make_index_sequence()); + + return { + .id = idx, + .name = wrapped.name, + .param_count = arg_count, + .param_types = ptypes, + .wrapper = wrapped.external_func, + .return_type = vm::cpp_t_to_type_info() + }; + } + }; + + template + class bindings { + using binding_type = function_binding; + using array_type = std::array, NBindings>; + using map_type = frozen::unordered_map; + + public: + constexpr explicit bindings(const array_type &array) + : m_map(frozen::make_unordered_map(array)) {} + + constexpr auto get(std::string_view name) -> std::optional { + if (not m_map.contains(name)) { + return std::nullopt; + } + return m_map.at(name); + } + + // runtime + auto get_callable_by_id(vm_external_function_id id) const -> vm_external_function_type * { + for (auto &&[k, v]: m_map) { + if (v.id == id) { + return std::addressof(v.wrapper); + } + } + return nullptr; + } + + private: + map_type m_map; + }; + + template + class bindings<0, NMaxArgs> { + using binding_type = function_binding; + using array_type = std::array, 0>; + + public: + constexpr explicit bindings() = default; + + constexpr explicit bindings(const array_type &) {}; + + constexpr auto get(std::string_view) -> std::optional { + return std::nullopt; + } + + auto get_callable_by_id(vm_external_function_id) const -> vm_external_function_type * { + return nullptr; + } + }; + + class const_bindings { + + }; + + consteval auto make_bindings(auto ...wrapped_functions) { + constexpr auto func_max_args = [] { + std::size_t max{}; + ((max = std::max(max, function_traits< + typename std::decay_t::signature_t + >::args_count)), ...); + return max; + }(); + + using binding_type = function_binding; + + vm_external_function_id i{}; + return bindings{ + std::array, sizeof...(wrapped_functions)>{ + std::pair{ + wrapped_functions.name, + binding_type::from_function(i++, wrapped_functions) + }... + }}; + } + + template + struct wrapped_function { + using signature_t = Signature; + + vm_external_function_type &external_func; + std::string_view name; + }; + + template + consteval auto wrap(std::string_view name) { + return wrapped_function>{ + binding_wrapper, + name + }; + } + +} \ No newline at end of file diff --git a/include/korka/compiler/binding_wrapper.hpp b/include/korka/compiler/binding_wrapper.hpp new file mode 100644 index 0000000..9fa5e44 --- /dev/null +++ b/include/korka/compiler/binding_wrapper.hpp @@ -0,0 +1,37 @@ +#pragma once +#include "korka/vm/context_base.hpp" + +namespace korka { + using vm_external_function_id = std::uint16_t; + using vm_external_function_type = void(vm::context_base &context); + + template + auto binding_wrapper(vm::context_base &ctx) -> void { + using function_type = std::remove_cvref_t; + using traits = function_traits; + using rtype = typename traits::return_type; + using args_tuple = typename traits::args_tuple; + constexpr auto arg_count = traits::args_count; + + // Load args from stack + std::array retrieved_args; + for (int i = arg_count - 1; i >= 0; --i) { + retrieved_args[i] = ctx.pop_value(); + } + + // Unbox them into types + auto args = [&](std::index_sequence) { + return args_tuple{ + vm::unbox>(retrieved_args[I])... + }; + }(std::make_index_sequence()); + + if constexpr (std::is_void_v) { + std::apply(Func, std::move(args)); + } else { + rtype v = std::apply(Func, std::move(args)); + auto boxed = vm::box(v); + ctx.push_value(boxed); + } + } +} \ No newline at end of file diff --git a/include/korka/compiler/compiler.hpp b/include/korka/compiler/compiler.hpp index acb2312..4ec9e03 100644 --- a/include/korka/compiler/compiler.hpp +++ b/include/korka/compiler/compiler.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include "frozen/unordered_map.h" +#include "korka/compiler/binding.hpp" #include "korka/shared/error.hpp" #include "korka/shared/flat_map.hpp" #include "korka/utils/overloaded.hpp" @@ -12,294 +14,19 @@ #include #include #include +#include "info.hpp" +#include "binding.hpp" +#include "result.hpp" namespace korka { - struct void_t { - }; - - using vm::type_info; - - constexpr auto string_to_type(std::string_view name) -> type { - if (name == "int") return type::i64; - else if (name == "void") return type::void_; - // TODO: other types - return type::i64; - } - - constexpr auto type_to_string(type t) -> std::string_view { - switch (t) { - case type::void_: - return "void"; - case type::i64: - return "int"; - } - } - - struct variable_info { - std::string_view name; - type_info type; - - std::size_t locals_index; - - static constexpr auto from_node(const nodes::decl_var &node) -> variable_info { - return { - .name = node.var_name, - .type{}, - .locals_index{} - }; - } - }; - - struct function_info { - std::string_view name; - std::vector params; - type_info return_type; - - std::size_t start_pos; - }; - - template - struct const_function_info { - std::string_view name; - std::size_t param_count; - std::array params; - type_info return_type; - - std::size_t start_pos; - }; - - template - struct _extract_function_signature { - template - struct param_helper; - - template - struct param_helper> { - // Map each parameter info to its corresponding C++ type - using type = vm::type_info_to_cpp_t<[] { return info_getter().return_type; }>( - vm::type_info_to_cpp_t<[] { return info_getter().params[Is].type; }>... - ); - }; - - using type = typename param_helper< - std::make_index_sequence - >::type; - }; - - template - using const_function_info_to_signature_t = typename _extract_function_signature::type; - - template - constexpr auto function_info_to_const(const function_info &f) { - const_function_info info{ - .name = f.name, - .param_count = f.params.size(), - .params{}, - .return_type = f.return_type, - .start_pos = f.start_pos - }; - - std::ranges::copy(f.params, std::begin(info.params)); - return info; - } - - struct symbol_table { - struct scope { - flat_map variables; - - std::size_t current_locals_size{}; - }; - std::vector scopes; - flat_map functions; - - constexpr auto push_scope() -> void { scopes.emplace_back(); } - - constexpr auto pop_scope() -> void { scopes.pop_back(); } - - constexpr auto declare_var(std::string_view name, const type_info &type) -> std::expected { - if (scopes.empty()) { - return std::unexpected{error::other_compiler_error{ - .message = "No scope" - }}; - } - - auto ¤t = scopes.back(); - if (current.variables.contains(name)) { - return std::unexpected{error::redeclaration{ - .identifier = name - }}; - } - - variable_info info{ - .name = name, - .type = type, - .locals_index = current.current_locals_size++ - }; - - current.variables[name] = info; - return info; - } - - constexpr auto declare_function(std::string_view name, auto &&...args) -> std::expected { - functions.emplace(std::piecewise_construct, - std::forward_as_tuple(name), - std::forward_as_tuple(name, std::forward(args)...)); - return {}; - } - - constexpr auto lookup_variable(std::string_view name) -> std::optional { - for (auto &scp: std::ranges::reverse_view(scopes)) { - if (auto var_it = scp.variables.find(name); var_it != std::end(scp.variables)) { - return var_it->second; - } - } - return std::nullopt; - } - - constexpr auto lookup_function(std::string_view name) -> std::optional { - if (auto func_it = functions.find(name); func_it != std::end(functions)) { - return func_it->second; - } - return std::nullopt; - } - - constexpr auto clear() -> void { - scopes.clear(); - functions.clear(); - } - }; - - - struct compilation_result { - std::vector bytes; - flat_map functions; - }; - - - struct function_runtime_info { - std::size_t start_pos; - }; - - template - struct function_runtime_info_with_signature : public function_runtime_info { - using signature_t = Signature; - - using function_runtime_info::start_pos; - }; - - template - struct const_compilation_result { - std::array bytes; - frozen::unordered_map, NFunctions> functions; - - template - using get_signature_t = typename SignatureMapper::template get_signature_t; - - template - constexpr auto function() const { - return function_runtime_info_with_signature>{ - functions.at(name).start_pos - }; - } - -// SignatureMapper mapper{}; - }; - - template - struct unique_type { - }; - - template - struct signature_mapper; - - template - struct signature_mapper> { - consteval static auto hash(auto &&v) -> std::size_t { - return frozen::elsa{}(v, 0); - } - - // That's how we basically map types to strings - // Function name -> hash -> unique_type - // And then we create a functor with overloading for different types - // Basically it looks like this: - // overloaded{ - // [](unique_type) -> TYPE1* { return nullptr; }, - // [](unique_type) -> TYPE2* { return nullptr; }, - // ... - // } - - // I think I could return here some meta info for internal types later, idk - - constexpr static auto _overloaded = overloaded{ - ([](unique_type) - -> const_function_info_to_signature_t<[] { return function_info_getter(Is); }> * { - return nullptr; - })... - }; - - // Retrieve the type - template - #ifdef __clang__ // GCC crashes on this check for some reason - requires ([] -> bool { - constexpr bool found = requires { - { _overloaded(unique_type{}) }; - }; - if constexpr (not found) { - report_error<[] { - return format("Symbol '~' not found", name); - }>(); - } - return true; - }()) - #endif - using get_signature_t = std::remove_pointer_t{}))>; - -// std::tuple *...> debug1; -// std::tuple{}(function_info_getter(Is).name, 0)>...> debug2; - }; - - - template - constexpr auto compilation_result_to_const() { - // --- BYTES --- - constexpr static auto bytes = to_array<[] { return r().bytes; }>(); - - // --- FUNCTIONS --- - constexpr static auto function_count = []() constexpr { - return r().functions.size(); - }(); - constexpr static auto max_params_n = []() constexpr { - std::size_t n{}; - for (auto &&f: r().functions) { - n = std::max(n, f.second.params.size()); - } - return n; - }(); - constexpr static auto functions = []() constexpr { - std::array>, function_count> functions_data{}; - std::size_t i{}; - for (auto &&[key, value]: r().functions) { - functions_data[i++] = (std::make_pair(key, function_info_to_const(value))); - } - return frozen::make_unordered_map(functions_data); - }; - - // Function mapper - using sign_mapper = signature_mapper<[](std::size_t i) { - return (functions().begin() + i)->second; - }, std::make_index_sequence>; - - return const_compilation_result{ - bytes, - functions() - }; - } - + template class compiler { + using bindings_container = bindings; + public: - constexpr compiler(std::span nodes, nodes::index_t root_node) - : m_nodes(nodes), m_root_node(root_node) {} + constexpr compiler(std::span nodes, nodes::index_t root_node, const bindings_container &bindings) + : m_nodes(nodes), m_root_node(root_node), m_bindings(bindings) { + } constexpr auto compile() -> std::expected { m_symbols.push_scope(); @@ -318,6 +45,9 @@ namespace korka { symbol_table m_symbols; vm::bytecode_builder builder; + // External bindings + bindings_container m_bindings; + // Info for ast walker std::optional m_current_func_ret; @@ -418,20 +148,27 @@ namespace korka { }, [&](const nodes::stmt_return &stmt) -> result_t { - auto actual_type = process_node(stmt.expr, emit); - if (!actual_type) return actual_type; - - // Semantic Check: Does return type match function signature? - if (m_current_func_ret && *actual_type != *m_current_func_ret) { - // TODO: proper error - return std::unexpected{error::other_compiler_error{ - .message = "Function return type mismatch" - }}; - } - if (emit) { - builder.emit_op(vm::op_code::ret); + if (stmt.expr != empty_node) { + auto actual_type = process_node(stmt.expr, emit); + if (!actual_type) return actual_type; + + // Semantic Check: Does return type match function signature? + if (m_current_func_ret && *actual_type != *m_current_func_ret) { + // TODO: proper error + return std::unexpected{error::other_compiler_error{ + .message = "Function return type mismatch" + }}; + } + if (emit) { + builder.emit_op(vm::op_code::ret); + } + return *actual_type; + } else { + if (emit) { + builder.emit_op(vm::op_code::ret); + } + return korka::type_info{korka::type::void_}; } - return *actual_type; }, [&](const nodes::decl_var &var) -> result_t { auto ok = m_symbols.declare_var(var.var_name, string_to_type(var.type_name)); @@ -556,25 +293,49 @@ namespace korka { }, [&](nodes::expr_call const &call) -> result_t { auto func = m_symbols.lookup_function(call.name); - if (!func) { + auto bindings_func = m_bindings.get(call.name); + + + if (not func and not bindings_func) { return std::unexpected{error::undefined_symbol{.identifier = call.name}}; } - auto &expected_params = func->params; std::vector arg_indices; - std::size_t pidx{}; - for (auto param_node_idx: nodes::get_list_view(m_nodes, call.args_head)) { - auto param_type = process_node(param_node_idx, /*emit=*/ false); - if (!param_type) return param_type; + // Internal function + if (func) { + auto &expected_params = func->params; - if (pidx >= expected_params.size() || expected_params[pidx].type != *param_type) { - return std::unexpected{error::function_call_param_mismatch{ - .function_name = func->name, .param_idx = pidx - }}; + std::size_t pidx{}; + for (auto param_node_idx: nodes::get_list_view(m_nodes, call.args_head)) { + auto param_type = process_node(param_node_idx, /*emit=*/ false); + if (!param_type) return param_type; + + if (pidx >= expected_params.size() || expected_params[pidx].type != *param_type) { + return std::unexpected{error::function_call_param_mismatch{ + .function_name = call.name, .param_idx = pidx + }}; + } + arg_indices.emplace_back(param_node_idx); + pidx++; + } + } else { // External function + auto &expected_params = bindings_func->param_types; + + std::size_t pidx{}; + for (auto param_node_idx: nodes::get_list_view(m_nodes, call.args_head)) { + auto param_type = process_node(param_node_idx, false); + if (not param_type) return param_type; + + if (pidx >= expected_params.size() or expected_params[pidx] != *param_type) { + return std::unexpected{error::function_call_param_mismatch{ + .function_name = call.name, .param_idx = pidx + }}; + } + + arg_indices.emplace_back(param_node_idx); + pidx++; } - arg_indices.emplace_back(param_node_idx); - pidx++; } if (emit) { @@ -582,15 +343,26 @@ namespace korka { if (auto ok = process_node(idx, true); !ok) return ok; } - builder.emit_const(static_cast(arg_indices.size())); - - builder.emit_call(func->start_pos); + if (func) { + builder.emit_const(static_cast(arg_indices.size())); + builder.emit_call(func->start_pos); + } else { + builder.emit_trap(bindings_func->id); + } } - return func->return_type; + if (func) { + return func->return_type; + } else { + return bindings_func->return_type; + } + }, + [&](const stmt_expr &expr) -> result_t { + return process_node(expr.expr, emit); }, [&](const auto &value) -> result_t { + throw 0; std::ignore = value; return std::unexpected{error::other_compiler_error{ "Not implemented" @@ -600,11 +372,10 @@ namespace korka { } }; - template + template consteval static auto compile_nodes() { - constexpr static auto expected = [] constexpr { - return compiler{nodes, root}.compile(); - }; + + constexpr static auto expected = [] { return compiler{nodes, root, *bindings}.compile(); }; if constexpr (not expected()) { report_error<[] { return expected().error(); }>(); @@ -614,10 +385,10 @@ namespace korka { } } - template + template consteval static auto compile() { constexpr static auto nodes_root = parse(); - return compile_nodes(); + return compile_nodes(); } } // namespace korka \ No newline at end of file diff --git a/include/korka/compiler/info.hpp b/include/korka/compiler/info.hpp new file mode 100644 index 0000000..807b2d7 --- /dev/null +++ b/include/korka/compiler/info.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include "korka/shared/error.hpp" +#include +#include "korka/vm/op_codes.hpp" +#include "korka/compiler/parser.hpp" + +namespace korka { + + using vm::type_info; + + constexpr auto string_to_type(std::string_view name) -> type { + if (name == "int") return type::i64; + else if (name == "void") return type::void_; + // TODO: other types + return type::i64; + } + + constexpr auto type_to_string(type t) -> std::string_view { + switch (t) { + case type::void_: + return "void"; + case type::i64: + return "int"; + } + } + + struct variable_info { + std::string_view name{}; + type_info type; + + std::size_t locals_index{}; + + static constexpr auto from_node(const nodes::decl_var &node) -> variable_info { + return { + .name = node.var_name, + .type{}, + .locals_index{} + }; + } + }; + + struct function_info { + std::string_view name; + std::vector params; + type_info return_type; + + std::size_t start_pos; + }; + + template + struct const_function_info { + std::string_view name{}; + std::size_t param_count{}; + std::array params{}; + type_info return_type{}; + + std::size_t start_pos{}; + }; +} \ No newline at end of file diff --git a/include/korka/compiler/result.hpp b/include/korka/compiler/result.hpp new file mode 100644 index 0000000..2fd25eb --- /dev/null +++ b/include/korka/compiler/result.hpp @@ -0,0 +1,223 @@ +// +// Created by pyxiion on 20/03/2026. +// +#pragma once + +#include +#include "korka/vm/op_codes.hpp" +#include "korka/compiler/info.hpp" +#include "korka/shared/flat_map.hpp" +#include "korka/utils/frozen_hash_string_view.hpp" + +namespace korka { + template + struct _extract_function_signature { + template + struct param_helper; + + template + struct param_helper> { + // Map each parameter info to its corresponding C++ type + using type = vm::type_info_to_cpp_t<[] { return info_getter().return_type; }>( + vm::type_info_to_cpp_t<[] { return info_getter().params[Is].type; }>... + ); + }; + + using type = typename param_helper< + std::make_index_sequence + >::type; + }; + + template + using const_function_info_to_signature_t = typename _extract_function_signature::type; + + template + constexpr auto function_info_to_const(const function_info &f) { + const_function_info info{ + .name = f.name, + .param_count = f.params.size(), + .params{}, + .return_type = f.return_type, + .start_pos = f.start_pos + }; + + std::ranges::copy(f.params, std::begin(info.params)); + return info; + } + + struct symbol_table { + struct scope { + flat_map variables; + + std::size_t current_locals_size{}; + }; + std::vector scopes; + flat_map functions; + + constexpr auto push_scope() -> void { scopes.emplace_back(); } + + constexpr auto pop_scope() -> void { scopes.pop_back(); } + + constexpr auto declare_var(std::string_view name, const type_info &type) -> std::expected { + if (scopes.empty()) { + return std::unexpected{error::other_compiler_error{ + .message = "No scope" + }}; + } + + auto ¤t = scopes.back(); + if (current.variables.contains(name)) { + return std::unexpected{error::redeclaration{ + .identifier = name + }}; + } + + variable_info info{ + .name = name, + .type = type, + .locals_index = current.current_locals_size++ + }; + + current.variables[name] = info; + return info; + } + + constexpr auto declare_function(std::string_view name, auto &&...args) -> std::expected { + functions.emplace(std::piecewise_construct, + std::forward_as_tuple(name), + std::forward_as_tuple(name, std::forward(args)...)); + return {}; + } + + constexpr auto lookup_variable(std::string_view name) -> std::optional { + for (auto &scp: std::ranges::reverse_view(scopes)) { + if (auto var_it = scp.variables.find(name); var_it != std::end(scp.variables)) { + return var_it->second; + } + } + return std::nullopt; + } + + constexpr auto lookup_function(std::string_view name) -> std::optional { + if (auto func_it = functions.find(name); func_it != std::end(functions)) { + return func_it->second; + } + return std::nullopt; + } + + constexpr auto clear() -> void { + scopes.clear(); + functions.clear(); + } + }; + + + struct compilation_result { + std::vector bytes; + flat_map functions; + }; + + + struct function_runtime_info { + std::size_t start_pos; + }; + + template + struct function_runtime_info_with_signature : public function_runtime_info { + using signature_t = Signature; + + using function_runtime_info::start_pos; + }; + + template + struct const_compilation_result { + std::array bytes; + frozen::unordered_map, NFunctions> functions; + + template + using get_signature_t = typename SignatureMapper::template get_signature_t; + + template + constexpr auto function() const { + return function_runtime_info_with_signature>{ + functions.at(name).start_pos + }; + } + }; + + template + struct unique_type { + }; + + template + struct signature_mapper; + + template + struct signature_mapper> { + consteval static auto hash(auto &&v) -> std::size_t { + return frozen::elsa{}(v, 0); + } + + constexpr static auto _overloaded = overloaded{ + ([](unique_type) + -> const_function_info_to_signature_t<[] { return function_info_getter(Is); }> * { + return nullptr; + })... + }; + + // Retrieve the type + template + #ifdef __clang__ // GCC crashes on this check for some reason + requires ([] -> bool { + constexpr bool found = requires { + { _overloaded(unique_type{}) }; + }; + if constexpr (not found) { + report_error<[] { + return format("Symbol '~' not found", name); + }>(); + } + return true; + }()) + #endif + using get_signature_t = std::remove_pointer_t{}))>; + }; + + + template + constexpr auto compilation_result_to_const() { + // --- BYTES --- + constexpr static auto bytes = to_array<[] { return r().bytes; }>(); + + // --- FUNCTIONS --- + constexpr static auto function_count = []() constexpr { + return r().functions.size(); + }(); + constexpr static auto max_params_n = []() constexpr { + std::size_t n{}; + for (auto &&f: r().functions) { + n = std::max(n, f.second.params.size()); + } + return n; + }(); + constexpr static auto functions = []() constexpr { + std::array>, function_count> functions_data{}; + std::size_t i{}; + for (auto &&[key, value]: r().functions) { + functions_data[i++] = (std::make_pair(key, function_info_to_const(value))); + } + return frozen::make_unordered_map(functions_data); + }; + + // Function mapper + using sign_mapper = signature_mapper<[](std::size_t i) { + return (functions().begin() + i)->second; + }, std::make_index_sequence>; + + return const_compilation_result{ + bytes, + functions() + }; + } +} \ No newline at end of file diff --git a/include/korka/utils/function_traits.hpp b/include/korka/utils/function_traits.hpp index 9e421e4..b667c22 100644 --- a/include/korka/utils/function_traits.hpp +++ b/include/korka/utils/function_traits.hpp @@ -16,9 +16,17 @@ namespace korka { using args_tuple = std::tuple; constexpr static std::size_t args_count = sizeof...(Args); + + /** + * callback(Args*... = nullptr) + */ + static constexpr auto process_args(auto &&callback) { + callback(std::add_pointer_t{}...); + } }; template struct function_traits : function_traits { + }; } \ No newline at end of file diff --git a/include/korka/vm/bytecode_builder.hpp b/include/korka/vm/bytecode_builder.hpp index 22882a0..702768c 100644 --- a/include/korka/vm/bytecode_builder.hpp +++ b/include/korka/vm/bytecode_builder.hpp @@ -70,6 +70,11 @@ namespace korka::vm { emit_op(op_code::call); m_data.write_many(address); } + + constexpr auto emit_trap(vm_external_function_id function_id) { + emit_op(op_code::trap); + m_data.write_many(function_id); + } // // constexpr auto emit_jmp_if(const label &target, reg_id_t cond) { // record_jump(op_code::jmp_if, target, cond); diff --git a/include/korka/vm/context_base.hpp b/include/korka/vm/context_base.hpp new file mode 100644 index 0000000..5f7c875 --- /dev/null +++ b/include/korka/vm/context_base.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include "korka/utils/function_traits.hpp" +#include "korka/vm/op_codes.hpp" +#include "korka/utils/byte_reader.hpp" +#include "korka/compiler/result.hpp" +#include "korka/vm/value.hpp" +#include "korka/compiler/binding.hpp" +#include +#include +#include +#include + +namespace korka::vm { + template + concept bindings_concepts = requires(const T &t) { + { t.get_callable_by_id(0) }; + }; + + struct function_scope { + // Set on `call`, used on `ret` + std::size_t suspension_point{}; + + std::vector locals{8}; + + auto set_local_value(local_index_t i, value const &v) -> void { + locals.at(i) = v; + } + + template + auto set_local(local_index_t i, T const &value) -> void { + set_local_value(i, box(value)); + } + + auto get_local_value(local_index_t i) -> value { + return locals.at(i); + } + + template + auto get_local(local_index_t i) -> T { + return unbox(get_local_value(i)); + } + + + auto clear() -> void { + locals.clear(); + } + }; + + class context_base { + public: + explicit context_base(std::span bytes) + : m_reader(bytes) { + m_stack.reserve(64); + } + + auto push_value(value const &v) -> void { + m_stack.emplace_back(v); + } + + template + auto push(T const &v) -> void { + push_value(box(v)); + } + + auto pop_value() -> value { + value v = m_stack.back(); + m_stack.pop_back(); + return v; + } + + template + auto pop() -> T { + return unbox(pop_value()); + } + + template + auto pop() -> type_to_cpp_t { + return unbox>(pop_value()); + } + + protected: + + auto current_scope() -> function_scope * { + return m_scopes.empty() ? nullptr : std::addressof(m_scopes.back()); + } + + auto push_scope() -> void { + m_scopes.emplace_back(); + } + + auto pop_scope() -> void { + m_scopes.pop_back(); + } + + std::vector m_stack; + std::vector m_scopes; + + byte_reader m_reader; + }; +} \ No newline at end of file diff --git a/include/korka/vm/op_codes.hpp b/include/korka/vm/op_codes.hpp index cc0d14e..868183c 100644 --- a/include/korka/vm/op_codes.hpp +++ b/include/korka/vm/op_codes.hpp @@ -60,7 +60,10 @@ namespace korka::vm { // C // Then VM on call puts them into locals call, // - ret + ret, + + // call external function + trap, // }; template @@ -130,4 +133,16 @@ namespace korka::vm { template using type_info_to_cpp_t = decltype(_type_info_to_cpp()); + + + template + constexpr auto cpp_t_to_type_info() -> type_info { + if constexpr (std::is_same_v) { + return {type::void_}; + } else if constexpr (std::is_same_v) { + return {type::i64}; + } else { + static_assert(false, "Unsupported type"); + } + } } \ No newline at end of file diff --git a/include/korka/vm/value.hpp b/include/korka/vm/value.hpp new file mode 100644 index 0000000..0d6ca0d --- /dev/null +++ b/include/korka/vm/value.hpp @@ -0,0 +1,25 @@ +// +// Created by pyxiion on 20/03/2026. +// + +#pragma once +#include +#include + +namespace korka::vm { + using value = std::array; + + template + constexpr auto box(T const &v) -> value { + static_assert(std::is_same_v or std::is_same_v, "T must be int or double"); + + return std::bit_cast(v); + } + + template + constexpr auto unbox(value const &v) -> T { + static_assert(std::is_same_v or std::is_same_v, "T must be int or double"); + + return std::bit_cast(v); + } +} \ No newline at end of file diff --git a/include/korka/vm/vm_runtime.hpp b/include/korka/vm/vm_runtime.hpp index 52ff753..6b13713 100644 --- a/include/korka/vm/vm_runtime.hpp +++ b/include/korka/vm/vm_runtime.hpp @@ -7,68 +7,22 @@ #include "korka/utils/function_traits.hpp" #include "korka/vm/op_codes.hpp" #include "korka/utils/byte_reader.hpp" -#include "korka/compiler/compiler.hpp" +#include "korka/compiler/result.hpp" +#include "korka/vm/value.hpp" +#include "korka/compiler/binding.hpp" +#include "korka/vm/context_base.hpp" #include #include #include #include namespace korka::vm { - using value = std::array; - - template - constexpr auto box(T const &v) -> value { - static_assert(std::is_same_v or std::is_same_v, "T must be int or double"); - - return std::bit_cast(v); - } - - template - constexpr auto unbox(value const &v) -> T { - static_assert(std::is_same_v or std::is_same_v, "T must be int or double"); - - return std::bit_cast(v); - } - - constexpr auto box_int(std::int64_t i) -> value { - return std::bit_cast(i); - } - - struct function_scope { - // Set on `call`, used on `ret` - std::size_t suspension_point{}; - - std::vector locals{8}; - - auto set_local_value(local_index_t i, value const &v) -> void { - locals.at(i) = v; - } - - template - auto set_local(local_index_t i, T const &value) -> void { - set_local_value(i, box(value)); - } - - auto get_local_value(local_index_t i) -> value { - return locals.at(i); - } - - template - auto get_local(local_index_t i) -> T { - return unbox(get_local_value(i)); - } - - - auto clear() -> void { - locals.clear(); - } - }; - - class context { + template> + class context : public context_base { public: - explicit context(std::span bytes) - : m_reader(bytes) { - m_stack.reserve(64); + explicit context(std::span bytes, bindings_t binds = {}) + : context_base(bytes), m_bindings(binds) { + } template> @@ -100,35 +54,17 @@ namespace korka::vm { } } - return pop(); - } - - auto push_value(value const &v) -> void { - m_stack.emplace_back(v); - } - - template - auto push(T const &v) -> void { - push_value(box(v)); - } - - auto pop_value() -> value { - value v = m_stack.back(); - m_stack.pop_back(); - return v; - } - - template - auto pop() -> T { - return unbox(pop_value()); + using return_type = typename Traits::return_type; + if constexpr (std::is_void_v) { + return; + } else { + return pop(); + } } - template - auto pop() -> type_to_cpp_t { - return unbox>(pop_value()); - } + protected: + bindings_t m_bindings; - private: auto execute_op() -> op_code { auto initial_pc = m_reader.cursor(); @@ -218,27 +154,16 @@ namespace korka::vm { m_reader.set_cursor(current_scope()->suspension_point); } break; + case op_code::trap: { + auto id = m_reader.read(); + auto func = m_bindings.get_callable_by_id(id); + (*func)(*this); + } + break; } return code; } - - auto current_scope() -> function_scope * { - return m_scopes.empty() ? nullptr : std::addressof(m_scopes.back()); - } - - auto push_scope() -> void { - m_scopes.emplace_back(); - } - - auto pop_scope() -> void { - m_scopes.pop_back(); - } - - std::vector m_stack; - std::vector m_scopes; - - byte_reader m_reader; }; class runtime { diff --git a/main.cpp b/main.cpp index ebcac29..ed16b81 100644 --- a/main.cpp +++ b/main.cpp @@ -1,27 +1,57 @@ +#include "korka/compiler/binding.hpp" #include "korka/compiler/parser.hpp" #include "korka/compiler/compiler.hpp" #include "korka/compiler/ast_walker.hpp" #include "korka/vm/vm_runtime.hpp" #include +#include + +auto fib(std::int64_t n) -> std::int64_t { + if (n == 0) return 0; + if (n == 1) return 1; + + return fib(n - 1) + fib(n - 2); +} + +auto print_n(std::int64_t n) -> void { + std::cout << n << '\n'; +} constexpr char code[] = R"( int fib(int n) { if (n == 0) return 0; if (n == 1) return 1; - return fib(n-1) + fib(n-2); + return fib(n-1) + cpp_fib(n-2); +} + +void print_fib(int n) { + int result = fib(n); + + print_n(result); + return; } )"; -constexpr auto compile_result = korka::compile(); +constexpr auto bindings = korka::make_bindings( + korka::wrap("cpp_fib"), + korka::wrap("print_n") +); +constexpr auto compile_result = korka::compile(); + +constexpr auto script_fib = compile_result.function<"fib">(); +constexpr auto script_print_fib = compile_result.function<"print_fib">(); int main() { - korka::vm::context ctx{compile_result.bytes}; -// auto main_func = compile_result.function<"main">(); - auto fib_func = compile_result.function<"fib">(); + korka::vm::context ctx{compile_result.bytes, bindings}; + + auto result = ctx.call(script_fib, 12L); + std::cout << result << '\n'; + + ctx.call(script_print_fib, 16L); - std::println("{}", ctx.call(fib_func, 12L)); +// std::println("{}", ctx.call(fib_func, 12L)); // std::ignore = tokens; // std::println("{:n:02X}", compile_result.bytes | std::views::transform([](auto b) { return static_cast(b); })); From 36543543f26f3e78d3c3d82fd4d7232cdcef42c8 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Mon, 6 Apr 2026 12:08:02 +0300 Subject: [PATCH 2/4] Add benchmark --- CMakeLists.txt | 38 +++++++++-- bench.cpp | 175 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 bench.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f67a53f..2812474 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,13 @@ cmake_minimum_required(VERSION 3.25) project(pxkorka VERSION 0.1.0 - LANGUAGES CXX + LANGUAGES C CXX ) set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_EXTENSIONS ON) # --- OPTIONS --- option(ENABLE_TESTS "Build tests" ON) @@ -16,7 +16,7 @@ option(ENABLE_TESTS "Build tests" ON) if (MSVC) add_compile_options(/W4 /WX /permissive-) else () - add_compile_options(-Wall -Wextra -Wpedantic -Werror -Wshadow -Wunused) +# add_compile_options(-Wall -Wextra -Wpedantic -Werror -Wshadow -Wunused) endif () # Dependencies @@ -88,4 +88,34 @@ target_compile_options(korka_lib PUBLIC -Wno-error=unused-but-set-variable -Wno- # ) # # catch_discover_tests(pxkorka_tests) -#endif () \ No newline at end of file +#endif () + + +# --- BENCH --- +CPMAddPackage( + NAME lua + GIT_REPOSITORY https://github.com/lua/lua.git + VERSION 5.3.5 + DOWNLOAD_ONLY YES +) + +if (lua_ADDED) + # lua has no CMake support, so we create our own target + + FILE(GLOB lua_sources ${lua_SOURCE_DIR}/*.c) + list(REMOVE_ITEM lua_sources "${lua_SOURCE_DIR}/lua.c" "${lua_SOURCE_DIR}/luac.c") + add_library(lua STATIC ${lua_sources}) + + target_include_directories(lua + PUBLIC + $ + ) +endif() + + +find_package(Python3 COMPONENTS Development REQUIRED) + +add_executable(bench bench.cpp) +target_link_libraries(bench PUBLIC lua korka_lib) +target_link_libraries(bench PRIVATE Python3::Python) +target_include_directories(bench PRIVATE ${Python3_INCLUDE_DIRS}) \ No newline at end of file diff --git a/bench.cpp b/bench.cpp new file mode 100644 index 0000000..ba71a4c --- /dev/null +++ b/bench.cpp @@ -0,0 +1,175 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "korka/compiler/compiler.hpp" +#include "korka/compiler/binding.hpp" +#include "korka/vm/vm_runtime.hpp" + +[[gnu::noinline]] +auto native_fib(std::int64_t n) -> std::int64_t { + if (n == 0) return 0; + if (n == 1) return 1; + + return native_fib(n - 1) + native_fib(n - 2); +} + +const char *py_code = R"( +def fib(n): + if n == 0: return 0 + if n == 1: return 1 + return fib(n-1) + fib(n-2) +)"; + +const char *lua_code = R"( +function fib(n) + if n == 0 then return 0 end + if n == 1 then return 1 end + return fib(n-1) + fib(n-2) +end +)"; + +constexpr char korka_code[] = "int fib(int n) { if (n==0) return 0; if (n==1) return 1; return fib(n-1)+fib(n-2); }"; + +struct Result { + int n; + int iterations; + double korka_ms; + double lua_ms; + double python_ms; + double cpp_ms; +}; + +void run_benchmarks() { + // clang-format off + // @formatter:off + const std::vector> test_cases = { + {10, 2'000'000}, + {15, 200'000}, + {20, 20'000}, + {23, 4'500}, + {25, 2'000}, + {28, 400}, + {30, 200} + }; + // @formatter:on + // clang-format on + + std::vector results; + + std::cout << "=== Korka vs Lua vs Python vs C++ ===\n\n"; + + std::cout << "=== Phase 1: Initialization ===\n"; + + auto py_start = std::chrono::high_resolution_clock::now(); + Py_Initialize(); + PyObject *py_main = PyImport_AddModule("__main__"); + PyObject *py_dict = PyModule_GetDict(py_main); + PyRun_String(py_code, Py_file_input, py_dict, py_dict); + PyObject *py_fib_func = PyDict_GetItemString(py_dict, "fib"); + auto py_end = std::chrono::high_resolution_clock::now(); + + auto lua_start = std::chrono::high_resolution_clock::now(); + lua_State *L = luaL_newstate(); + luaL_openlibs(L); + luaL_dostring(L, lua_code); + auto lua_end = std::chrono::high_resolution_clock::now(); + + auto korka_start = std::chrono::high_resolution_clock::now(); + constexpr static auto bindings = korka::make_bindings(); + constexpr auto korka_bytecode = korka::compile(); + constexpr auto korka_fib_addr = korka_bytecode.function<"fib">(); + korka::vm::context ctx{korka_bytecode.bytes, bindings}; + auto korka_end = std::chrono::high_resolution_clock::now(); + + auto t_py = std::chrono::duration(py_end - py_start).count(); + auto t_lua = std::chrono::duration(lua_end - lua_start).count(); + auto t_korka = std::chrono::duration(korka_end - korka_start).count(); + + printf("Python Init: %8.1f µs\n", t_py); + printf("Lua Init: %8.1f µs\n", t_lua); + printf("Korka Init: %8.1f µs\n\n", t_korka); + + std::cout << "=== Phase 2: Execution (different fib depths) ===\n\n"; + std::cout << std::setw(6) << "n" + << std::setw(12) << "iters" + << std::setw(12) << "Korka ms" + << std::setw(12) << "Lua ms" + << std::setw(12) << "Python ms" + << std::setw(12) << "C++ ms" + << std::setw(14) << "Speedup" << "\n"; + std::cout << std::string(72, '-') << "\n"; + + for (const auto &[n, iterations]: test_cases) { + Result r{.n = n, .iterations = iterations}; + + // Korka + auto k_start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < iterations; ++i) { + volatile auto res = ctx.call(korka_fib_addr, static_cast(n)); + } + auto k_end = std::chrono::high_resolution_clock::now(); + r.korka_ms = std::chrono::duration(k_end - k_start).count(); + + // Lua + auto l_start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < iterations; ++i) { + lua_getglobal(L, "fib"); + lua_pushinteger(L, n); + lua_pcall(L, 1, 1, 0); + lua_pop(L, 1); + } + auto l_end = std::chrono::high_resolution_clock::now(); + r.lua_ms = std::chrono::duration(l_end - l_start).count(); + + // Python + auto p_start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < iterations; ++i) { + PyObject *args = PyTuple_Pack(1, PyLong_FromLong(n)); + PyObject *result = PyObject_CallObject(py_fib_func, args); + Py_DECREF(args); + Py_DECREF(result); + } + auto p_end = std::chrono::high_resolution_clock::now(); + r.python_ms = std::chrono::duration(p_end - p_start).count(); + + // C++ + auto cpp_start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < iterations; ++i) { + volatile auto a = native_fib(n); + (void) a; + } + auto cpp_end = std::chrono::high_resolution_clock::now(); + r.cpp_ms = std::chrono::duration(cpp_end - cpp_start).count(); + + results.push_back(r); + + double speedup = r.python_ms / r.korka_ms; + + printf("%6d %11d %11.2f %11.2f %11.2f %11.2f %12.2fx\n", + n, iterations, r.korka_ms, r.lua_ms, r.python_ms, r.cpp_ms, speedup); + } + + std::ofstream csv("fib_benchmark.csv"); + csv << "n,iterations,korka_ms,lua_ms,python_ms,korka_speedup_vs_python\n"; + for (const auto &r: results) { + double speedup = r.python_ms / r.korka_ms; + csv << r.n << "," << r.iterations << "," + << r.korka_ms << "," << r.lua_ms << "," << r.python_ms << "," + << speedup << "\n"; + } + csv.close(); + + std::cout << "=== DONE ==="; + Py_Finalize(); + lua_close(L); +} + +int main() { + run_benchmarks(); + return 0; +} \ No newline at end of file From 3215db876015fe36b8eefc3d4040b7b221c2b396 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Mon, 6 Apr 2026 12:09:11 +0300 Subject: [PATCH 3/4] Optimisations --- include/korka/compiler/binding.hpp | 4 - include/korka/utils/byte_reader.hpp | 4 + include/korka/vm/op_codes.hpp | 4 - include/korka/vm/vm_runtime.hpp | 252 ++++++++++++++++------------ main.cpp | 29 ---- 5 files changed, 147 insertions(+), 146 deletions(-) diff --git a/include/korka/compiler/binding.hpp b/include/korka/compiler/binding.hpp index 972feb2..80c9a80 100644 --- a/include/korka/compiler/binding.hpp +++ b/include/korka/compiler/binding.hpp @@ -101,10 +101,6 @@ namespace korka { } }; - class const_bindings { - - }; - consteval auto make_bindings(auto ...wrapped_functions) { constexpr auto func_max_args = [] { std::size_t max{}; diff --git a/include/korka/utils/byte_reader.hpp b/include/korka/utils/byte_reader.hpp index 378b97a..2ebdf3a 100644 --- a/include/korka/utils/byte_reader.hpp +++ b/include/korka/utils/byte_reader.hpp @@ -43,6 +43,10 @@ namespace korka::vm { m_cursor = pos; } + auto data() noexcept -> const std::byte * { + return m_bytes.data(); + } + private: std::size_t m_cursor; const std::span m_bytes; diff --git a/include/korka/vm/op_codes.hpp b/include/korka/vm/op_codes.hpp index 868183c..94c4536 100644 --- a/include/korka/vm/op_codes.hpp +++ b/include/korka/vm/op_codes.hpp @@ -19,10 +19,6 @@ namespace korka::vm { // lload, - // Load parameters from stack into locals - // - pload, - // Pops a value from stack and saves to the local at index // lsave, diff --git a/include/korka/vm/vm_runtime.hpp b/include/korka/vm/vm_runtime.hpp index 6b13713..9107d9b 100644 --- a/include/korka/vm/vm_runtime.hpp +++ b/include/korka/vm/vm_runtime.hpp @@ -33,7 +33,6 @@ namespace korka::vm { // Create a scope for the call push_scope(); - // TODO: check argument types // Pass arguments std::size_t i = 0; @@ -43,16 +42,7 @@ namespace korka::vm { m_reader.set_cursor(func.start_pos); - // Run until ret - // Check scopes size to ensure it's the original function's return - while (true) { - auto op = execute_op(); - - if (op == op_code::ret && m_scopes.empty()) { - m_scopes.clear(); - break; - } - } + execute_loop(); using return_type = typename Traits::return_type; if constexpr (std::is_void_v) { @@ -65,114 +55,158 @@ namespace korka::vm { protected: bindings_t m_bindings; - auto execute_op() -> op_code { - auto initial_pc = m_reader.cursor(); + void execute_loop() { + const std::byte *ip = m_reader.data() + m_reader.cursor(); + + // Using macros instead of lambdas, because + // templates with lambdas (like lambda) don't work + // well +#define READ(T) (*reinterpret_cast(ip)); ip += sizeof(T) +#define IP_POS() static_cast(ip - m_reader.data()) + + // Using dispatch table instead of switch case, because it's + // much faster for the CPU + // Also instead of calling execute_op every time I do it right here + // without extra calls + + // Also using C99 extensions, I should get rid of them later. + static const void *const dispatch[] = { + [int(op_code::lload)] = &&op_lload, + [int(op_code::lsave)] = &&op_lsave, + [int(op_code::i64_const)] = &&op_i64_const, + [int(op_code::i64_add)] = &&op_i64_add, + [int(op_code::i64_sub)] = &&op_i64_sub, + [int(op_code::i64_mul)] = &&op_i64_mul, + [int(op_code::i64_div)] = &&op_i64_div, + [int(op_code::i64_cmp)] = &&op_i64_cmp, + [int(op_code::jmp)] = &&op_jmp, + [int(op_code::jmpz)] = &&op_jmpz, + [int(op_code::call)] = &&op_call, + [int(op_code::ret)] = &&op_ret, + [int(op_code::trap)] = &&op_trap, + }; + + op_start: + const std::size_t instr_start = IP_POS(); + const op_code code = READ(op_code); + + // @formatter:off + // clang-format off + goto *dispatch[int(code)]; + // clang-format on + // @formatter:on + + op_lload: + { + const auto index = READ(local_index_t); + push_value(current_scope()->get_local_value(index)); + goto op_start; + } - const auto code = m_reader.read(); + op_lsave: + { + const auto index = READ(local_index_t); + current_scope()->set_local_value(index, pop_value()); + goto op_start; + } - switch (code) { - case op_code::lload: { - const auto index = m_reader.read(); - push_value(current_scope()->get_local_value(index)); - } - break; - case op_code::pload: - throw std::runtime_error("Not implemented"); - break; - case op_code::lsave: { - const auto index = m_reader.read(); - current_scope()->set_local_value(index, pop_value()); - } - break; - case op_code::i64_const: { - const auto v = m_reader.read(); - push(v); - } - break; - case op_code::i64_add: { - const auto b = pop(); - const auto a = pop(); - push(a + b); - } - break; - case op_code::i64_sub: { - const auto b = pop(); - const auto a = pop(); - push(a - b); - } - break; - case op_code::i64_mul: { - const auto b = pop(); - const auto a = pop(); - push(a * b); - } - break; - case op_code::i64_div: { - const auto b = pop(); - const auto a = pop(); - push(a / b); - } - break; - case op_code::i64_cmp: { - const auto b = pop(); - const auto a = pop(); - push(static_cast(a == b)); - } - break; - case op_code::jmp: { - const auto offset = m_reader.read(); - m_reader.set_cursor(initial_pc + offset); - } - break; - case op_code::jmpz: { - const auto offset = m_reader.read(); - auto v = pop(); - if (v == 0) { - m_reader.set_cursor(initial_pc + offset); - } - } - break; - case op_code::call: { - auto called_address = m_reader.read(); - current_scope()->suspension_point = m_reader.cursor(); - auto arg_count = pop(); + op_i64_const: + { + const auto v = READ(std::int64_t); + push(v); + goto op_start; + } + + op_i64_add: + { + const auto b = pop(); + const auto a = pop(); + push(a + b); + goto op_start; + } + op_i64_sub: + { + const auto b = pop(); + const auto a = pop(); + push(a - b); + goto op_start; + } + op_i64_mul: + { + const auto b = pop(); + const auto a = pop(); + push(a * b); + goto op_start; + } + op_i64_div: + { + const auto b = pop(); + const auto a = pop(); + push(a / b); + goto op_start; + } + op_i64_cmp: + { + const auto b = pop(); + const auto a = pop(); + push(static_cast(a == b)); + goto op_start; + } - push_scope(); + op_jmp: + { + const auto offset = READ(jump_offset); + ip = m_reader.data() + (instr_start + offset); + goto op_start; + } + + op_jmpz: + { + const auto offset = READ(jump_offset); + const auto v = pop(); + if (__builtin_expect(v == 0, 0)) { + ip = m_reader.data() + (instr_start + offset); + } + goto op_start; + } - // Load args - for (int i = arg_count - 1; i >= 0; --i) { - auto v = pop_value(); - current_scope()->set_local_value(i, v); - } + op_call: + { + auto called_address = READ(address_t); + current_scope()->suspension_point = IP_POS(); + auto arg_count = pop(); - m_reader.set_cursor(called_address); + push_scope(); + for (int i = arg_count - 1; i >= 0; --i) { + current_scope()->set_local_value(i, pop_value()); } - break; - case op_code::ret: - pop_scope(); - if (current_scope()) { - m_reader.set_cursor(current_scope()->suspension_point); - } - break; - case op_code::trap: { - auto id = m_reader.read(); - auto func = m_bindings.get_callable_by_id(id); - (*func)(*this); + ip = m_reader.data() + called_address; + goto op_start; + } + + op_ret: + { + pop_scope(); + if (current_scope()) { + ip = m_reader.data() + current_scope()->suspension_point; + } else { + m_scopes.clear(); + m_reader.set_cursor(IP_POS()); + return; } - break; + goto op_start; } - return code; + op_trap: + { + auto id = READ(vm_external_function_id); + auto func = m_bindings.get_callable_by_id(id); + (*func)(*this); + goto op_start; + } } - }; - - class runtime { - public: -// auto create_context() -> context { -// return {}; -// } - +#undef READ +#undef IP_POS }; - } // korka::vm \ No newline at end of file diff --git a/main.cpp b/main.cpp index ed16b81..37ebd43 100644 --- a/main.cpp +++ b/main.cpp @@ -50,33 +50,4 @@ int main() { std::cout << result << '\n'; ctx.call(script_print_fib, 16L); - -// std::println("{}", ctx.call(fib_func, 12L)); - -// std::ignore = tokens; -// std::println("{:n:02X}", compile_result.bytes | std::views::transform([](auto b) { return static_cast(b); })); - -// auto lexed = korka::lexer{code}.lex(); -// if (not lexed) { -// std::println("{}", korka::to_string(lexed.error())); -// return 0; -// } -// -// auto parsed = korka::parser{lexed.value()}.parse(); -// if (not parsed) { -// std::println("{}", korka::to_string(parsed.error())); -// return 0; -// } -// auto [node_pool, node_root] = parsed.value(); -// std::println("{}", korka::ast_walker{node_pool, node_root, 0}); -// -// korka::compiler compiler{node_pool, node_root}; -// auto bytes = compiler.compile(); -// -// if (bytes) { -// std::println("{::X}", *bytes | std::views::transform([](auto b) { return static_cast(b); })); -// } else { -// std::println("{}", korka::to_string(bytes.error())); -// } - } \ No newline at end of file From b621087fb460d7bd34c954a4928fb1458638e5e0 Mon Sep 17 00:00:00 2001 From: PyXiion Date: Mon, 6 Apr 2026 12:30:33 +0300 Subject: [PATCH 4/4] Update README.md / Add benchmark results, update examples & etc --- README.md | 153 ++++++++++++++++++++++++++------- media/execution_time_graph.png | Bin 0 -> 21070 bytes icon.svg => media/icon.svg | 0 3 files changed, 120 insertions(+), 33 deletions(-) create mode 100644 media/execution_time_graph.png rename icon.svg => media/icon.svg (100%) diff --git a/README.md b/README.md index d22d3b9..010b2ba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -KorkaVM icon +Korka icon -# KorkaVM +# Korka A Virtual Machine where lexing and compilation happen entirely at **compile-time**. @@ -8,22 +8,27 @@ A Virtual Machine where lexing and compilation happen entirely at **compile-time ### What is this -KorkaVM is a project where I'm trying to create a tool that allows to embed logic +Korka is a project where I'm trying to create a tool that allows to embed logic without runtime overhead of parsing or loading external files. You write C-like code right inside C++, and the compiler transforms it into internal bytecode before your program even starts. ### Status -| Component | Stage | Execution context | -|:----------------:|:-------:|:-----------------:| -| Lexer | Done | constexpr | -| Bytecode builder | Done | constexpr | -| Parser | Done | constexpr | -| Compiler | Partially done | constexpr | -| VM runner | Partially done | runtime | +| Component | Stage | Execution context | +|:----------------:|:-----:|:-----------------:| +| Lexer | Done | constexpr | +| Bytecode builder | Done | constexpr | +| Parser | Done | constexpr | +| Compiler | Done | constexpr | +| VM runner | Done | runtime | + +### Features + ++ **Full interop** between Korka & C++, you can bind a C++ function into the script and vice versa. + Type safety guaranteed. ++ **Fast VM**: in my `fib` benchmark Korka surpasses Lua and Python by 30-40% -What's done: ```cpp constexpr char code[] = R"( int main() { @@ -40,7 +45,12 @@ int foo(int a, int b) { } )"; -constexpr auto compile_result = korka::compile(); + +constexpr auto bindings = korka::make_bindings( + korka::wrap("cpp_fib"), + korka::wrap("print_n") +); +constexpr auto compile_result = korka::compile(); // Extracting function types from code // It returns a pointer, bc you can't return a type ._. @@ -51,39 +61,116 @@ auto foo_func = compile_result.function<"foo">(); static_assert(std::is_same_v); ``` - -## Example context +### Code example ```cpp +// Functions that are interoped into Korka +auto fib(std::int64_t n) -> std::int64_t { + if (n == 0) return 0; + if (n == 1) return 1; + + return fib(n - 1) + fib(n - 2); +} -auto foo(int a) -> int { - return a * 2 + 5; +auto print_n(std::int64_t n) -> void { + std::cout << n << '\n'; } +// C-like code +constexpr char code[] = R"( +int fib(int n) { + if (n == 0) return 0; + if (n == 1) return 1; + + return fib(n-1) + cpp_fib(n-2); +} + +void print_fib(int n) { + int result = fib(n); + + print_n(result); + return; +} +)"; + constexpr auto bindings = korka::make_bindings( - "foo", &foo + korka::wrap("cpp_fib"), + korka::wrap("print_n") ); -constexpr auto my_script = korka::compile(bindings, R"( - int calculate(int x) { - if (x <= 0) return 0; +constexpr auto compile_result = korka::compile(); - return foo(x) / 3; - } -)"); +constexpr auto script_fib = compile_result.function<"fib">(); +constexpr auto script_print_fib = compile_result.function<"print_fib">(); -// Simple usage int main() { - korka::runtime vm; - int result = vm.execute(my_script) + korka::vm::context ctx{ + compile_result.bytes, bindings + }; + + // result will have int64_t type + auto result = ctx.call(script_fib, 12L); + std::cout << result << '\n'; + + ctx.call(script_print_fib, 16L); } +``` -// Not so simple usage -int main() { - korka::runtime vm; - - // Byte code gets inserted right into the native instruction flow - // and executed by vm right there - korka::run_embed(vm); +## Benchmark + +See the [bench.cpp](https://github.com/PyXiion/pxkorka/blob/master/bench.cpp) for full code. + + +
+ +Code + +--- + +### Python code + +```python +def fib(n): + if n == 0: return 0 + if n == 1: return 1 + return fib(n - 1) + fib(n - 2) +``` + +### Lua code + +```lua +function fib(n) + if n == 0 then return 0 end + if n == 1 then return 1 end + return fib(n-1) + fib(n-2) +end +``` + +### C code (Korka) + +```c +int fib(int n) { + if (n==0) return 0; + if (n==1) return 1; + return fib(n-1) + fib(n-2); } ``` + +--- + +
+ +### Results: + +| n | Iterations | Korka (ms) | Lua (ms) | Python (ms) | Korka Speedup (vs. Python) | +|---:|------------:|-----------:|----------:|------------:|:--------------------------:| +| 10 | 2,000,000 | 6,503.03 | 9,686.56 | 8,988.40 | 1.38x | +| 15 | 200,000 | 7,113.14 | 10,899.20 | 9,996.17 | 1.41x | +| 20 | 20,000 | 8,546.09 | 11,765.60 | 10,961.60 | 1.28x | +| 23 | 4,500 | 7,964.48 | 11,491.50 | 10,667.10 | 1.34x | +| 25 | 2,000 | 8,900.08 | 12,980.40 | 12,277.30 | 1.38x | +| 28 | 400 | 7,506.88 | 11,101.50 | 10,820.10 | 1.44x | +| 30 | 200 | 9,910.64 | 14,334.40 | 13,697.50 | 1.38x | + +Cool graph here: +![Execution time graph](img.png) \ No newline at end of file diff --git a/media/execution_time_graph.png b/media/execution_time_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..3c24161423c98b242ad21c196c0d614f4322a541 GIT binary patch literal 21070 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 literal 0 HcmV?d00001 diff --git a/icon.svg b/media/icon.svg similarity index 100% rename from icon.svg rename to media/icon.svg