Zpět na domů

Kompilátor C v constexpr C++

Článek popisuje implementaci kompilátoru podmnožiny C v compile-time C++. Jsou zvážena obcházení omezení constexpr, mapování podpisů, vazby nativních funkcí a architektura lexeru/parseru.

C kompilátor plně v constexpr
Advertisement 728x90

Kompilátor jazyka podobného C v compile-time na C++

Byl vyvinut kompilátor podmnožiny jazyka C, implementovaný v C++ v constexpr-kontextu. Zdrojový kód je převeden na vlastní bajtový kód pro spuštění ve virtuálním stroji během běhu programu. Tento přístup zajišťuje nulové režijní náklady na interpret a plnou typovou bezpečnost.

Výhody zahrnují absenci kódu kompilátoru v binárním souboru, žádnou kompilaci za běhu a ochranu před neodpovídajícími signaturami funkcí. Nevýhody: absence optimalizací na úrovni LLVM a nemožnost hotreloadingu.

Obcházení omezení constexpr

C++20 rozšiřuje constexpr dynamickou alokací paměti a podporou std::vector, ale výsledky nelze přímo přenést do běhu programu. Jsou potřeba šablonové obálky.

Google AdInline article slot

Předávání řetězců přes šablony

Řetězcové literály se předávají přes šablonovou třídu const_string:

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;  
};

// Použití:
template<const_string str>
auto very_smart_function(...) { /* ... */ }

Extrakce polí z constexpr

Převod std::vector na std::array vyžaduje fixaci velikosti ve fázi kompilace přes lambda-funkci:

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>();
}

Zpracování chyb kompilace

Do C++26 static_assert vyžaduje řetězcové literály. Řešením je šablona ErrorMessage s textem chyby v parametru:

Google AdInline article slot
template<const_string Msg>  
struct ErrorMessage {  
  static_assert(false, "Podívejte se na parametr šablony pro podrobnosti");  
};

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
}

Výstup v C++26: Lexer Error: Neukončený řetězec na řádku 12.

Mapování signatur funkcí

Pro přiřazení řetězcových názvů k typům signatur se používá hashování a přetěžování:

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)>{}))>;
};

Extrakce: compile_result.function<"fib">();.

Google AdInline article slot

Vazby nativních funkcí

Propojení C++ funkcí se skriptovacím jazykem přes 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 generuje kód pro extrakci argumentů z VM a vrácení výsledku.

Architektura kompilátoru

Rozdělen do tří fází:

  • Lexer: rozdělení na tokeny v cyklu po znacích
  • Parser: vytvoření AST
  • Generátor bajtového kódu: emise se sémantickou analýzou

Příklad lexeru:

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;
  }
}

Co je důležité

  • Kompilátor je plně constexpr, bajtový kód je vložen do binárního souboru
  • Obcházení omezení C++20 přes const_string a lambda-obálky
  • Typově bezpečná extrakce funkcí podle hashů názvů
  • Vazby C++ funkcí s automatickou generací VM-kódu
  • Primitivní lexer/parser bez optimalizací

— Editorial Team

Advertisement 728x90

Číst dál