Back to Home

C Compiler in constexpr C++

The article describes the implementation of a C subset compiler in compile-time C++. It covers bypassing constexpr limitations, signature mapping, native function bindings, and lexer/parser architecture.

C compiler fully in constexpr
Advertisement 728x90

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.

Google AdInline article slot

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:

Google AdInline article slot
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">();.

Google AdInline article slot

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

Advertisement 728x90

Read Next