Powrót do strony głównej

Kompilator C w constexpr C++

Artykuł opisuje implementację kompilatora podzbioru C w compile-time C++. Omówiono obejścia ograniczeń constexpr, mapowanie sygnatur, wiązania natywnych funkcji i architekturę lexera/parsara.

Kompilator C całkowicie w constexpr
Advertisement 728x90

Kompilator języka C-podobnego w czasie kompilacji w C++

Opracowano kompilator podzbioru języka C, zaimplementowany w C++ w kontekście constexpr. Kod źródłowy jest przekształcany w niestandardowy bajtkod do wykonania w maszynie wirtualnej podczas działania programu. Podejście zapewnia zerowe narzuty interpretera i pełne bezpieczeństwo typów.

Zalety obejmują brak kodu kompilatora w pliku binarnym, brak kompilacji w czasie działania i ochronę przed niezgodnościami sygnatur funkcji. Wady: brak optymalizacji na poziomie LLVM oraz niemożność hot reload.

Omijanie ograniczeń constexpr

C++20 rozszerza constexpr o dynamiczne alokowanie pamięci i obsługę std::vector, ale wyniki nie mogą być bezpośrednio przenoszone do czasu działania. Wymagane są opakowania szablonowe.

Google AdInline article slot

Przekazywanie ciągów przez szablony

Literały ciągów są przekazywane przez szablonową klasę 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;  
};

// Użycie:
template<const_string str>
auto very_smart_function(...) { /* ... */ }

Ekstrakcja tablic z constexpr

Przekształcenie std::vector w std::array wymaga ustalenia rozmiaru na etapie kompilacji za pomocą funkcji lambda:

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

Obsługa błędów kompilacji

Do C++26 static_assert wymaga literałów ciągów. Rozwiązanie — szablon ErrorMessage z tekstem błędu w parametrze:

Google AdInline article slot
template<const_string Msg>  
struct ErrorMessage {  
  static_assert(false, "Sprawdź parametr szablonu");  
};

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
}

Wyjście w C++26: Błąd leksera: Niezamknięty ciąg w linii 12.

Mapowanie sygnatur funkcji

Do dopasowania nazw ciągów do typów sygnatur używa się haszowania i przeciążania:

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

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

Google AdInline article slot

Powiązania funkcji natywnych

Łączenie funkcji C++ z językiem skryptowym przez 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 kod do wyodrębniania argumentów z VM i zwracania wyniku.

Architektura kompilatora

Podzielony na trzy etapy:

  • Lekser: dzielenie na tokeny w pętli po znakach
  • Parser: budowanie AST
  • Generator bajtkodu: emisja z analizą semantyczną

Przykład leksera:

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 jest ważne

  • Kompilator jest całkowicie constexpr, bajtkod jest osadzony w pliku binarnym
  • Omijanie ograniczeń C++20 przez const_string i opakowania lambda
  • Bezpieczne typowo wyodrębnianie funkcji po haszach nazw
  • Powiązania funkcji C++ z automatyczną generacją kodu VM
  • Prymitywny lekser/parser bez optymalizacji

— Editorial Team

Advertisement 728x90

Czytaj dalej