A C-Like Language Compiler at Compile Time in C++
A compiler for a subset of the C language has been developed, implemented in C++ within a constexpr context. Source code is transformed into custom bytecode for execution in a virtual machine during runtime. This approach ensures zero interpreter overhead and full type safety.
Advantages include no compiler code in the binary, no runtime compilation, and protection against function signature mismatches. Drawbacks: lack of LLVM-level optimizations and inability to hot-reload.
Working Around constexpr Limitations
C++20 extends constexpr with dynamic memory allocation and support for std::vector, but results cannot be directly transferred to runtime. Template wrappers are required.
Passing Strings Through Templates
String literals are passed via the const_string template class:
template<std::size_t N>
struct const_string {
constexpr const_string() = default;
constexpr const_string(const char (&str)[N]) {
std::copy_n(str, N, value);
}
constexpr operator std::string_view() const {
return {value, value + N - 1};
}
char value[N]{};
const std::size_t length = N;
};
// Usage:
template<const_string str>
auto very_smart_function(...) { /* ... */ }
Extracting Arrays from constexpr
Converting std::vector to std::array requires fixing the size at compile time via a lambda function:
template<auto data_getter>
constexpr auto to_array() {
using value_type = typename decltype(data_getter())::value_type;
constexpr static std::size_t size = data_getter().size();
std::array<value_type, size> out;
auto in = data_getter();
for (std::size_t i = 0; i < size; ++i) {
out[i] = in[i];
}
return out;
}
template<const_string str>
constexpr auto lex() {
constexpr static auto data_getter = [] constexpr {
return lexer{static_cast<std::string_view>(str)}.lex();
};
return to_array<data_getter>();
}
Handling Compilation Errors
Until C++26, static_assert requires string literals. The solution is an ErrorMessage template with the error text in the parameter:
template<const_string Msg>
struct ErrorMessage {
static_assert(false, "Check the template parameter for details");
};
template<auto err_getter>
consteval auto report_error() -> void {
#ifdef KORKA_FEATURE_FORMATTED_STATIC_ASSERT
static_assert(false, to_string(err_getter()));
#else
constexpr auto msg = const_string_from_string_view<[] { return to_string(err_getter()); }>();
std::ignore = ErrorMessage<msg>{};
#endif
}
Output in C++26: Lexer Error: Unterminated string at line 12.
Mapping Function Signatures
To match string names with signature types, hashing and overloading are used:
template<auto function_info_getter, std::size_t... Is>
struct signature_mapper<function_info_getter, std::index_sequence<Is...>> {
consteval static auto hash(auto &&v) -> std::size_t {
return frozen::elsa<std::string_view>{}(v, 0);
}
constexpr static auto _overloaded = overloaded{
([] (unique_type<hash(function_info_getter(Is).name)>)
-> const_function_info_to_signature_t<[] { return function_info_getter(Is); }> * {
return nullptr;
})...
};
template<const_string name>
using get_signature_t = std::remove_pointer_t<decltype(_overloaded(unique_type<hash(name)>{}))>;
};
Extraction: compile_result.function<"fib">();.
Native Function Bindings
Linking C++ functions with the scripting language via wrapped_function:
consteval auto wrap(std::string_view name) {
return wrapped_function<std::decay_t<decltype(func)>>{
binding_wrapper<func>,
name
};
}
constexpr auto bindings = korka::make_bindings(
korka::wrap<fib>("cpp_fib"),
korka::wrap<print_n>("print_n")
);
binding_wrapper generates code to extract arguments from the VM and return results.
Compiler Architecture
Divided into three stages:
- Lexer: Tokenization in a character loop
- Parser: AST construction
- Bytecode Generator: Emission with semantic analysis
Example lexer:
constexpr auto scan_token() -> std::optional<std::expected<lex_token, error_t>> {
char c = advance();
switch (c) {
case '{': return make_token(lex_kind::kOpenBrace);
case '}': return make_token(lex_kind::kCloseBrace);
case '(': return make_token(lex_kind::kOpenParenthesis);
case ')': /* ... */
case ' ': case '\r': case '\t': return std::nullopt;
}
}
Key Points
- The compiler is fully constexpr, with bytecode embedded in the binary
- Workarounds for C++20 limitations via const_string and lambda wrappers
- Type-safe function extraction using name hashes
- C++ function bindings with automatic VM code generation
- Basic lexer/parser without optimizations
— Editorial Team
No comments yet.