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.
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:
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">();.
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
Brak komentarzy.