Ein C-ähnlicher Sprachcompiler zur Compile-Zeit in C++
Ein Compiler für eine Teilmenge der C-Sprache wurde entwickelt, der in C++ innerhalb eines constexpr-Kontexts implementiert ist. Quellcode wird in benutzerdefinierten Bytecode umgewandelt, der zur Laufzeit in einer virtuellen Maschine ausgeführt wird. Dieser Ansatz gewährleistet keinen Interpreter-Overhead und volle Typsicherheit.
Vorteile sind: Kein Compiler-Code im Binärformat, keine Laufzeitkompilierung und Schutz vor Funktionssignatur-Fehlern. Nachteile: Fehlen von LLVM-Optimierungen und Unfähigkeit zum Hot-Reload.
Umgang mit constexpr-Einschränkungen
C++20 erweitert constexpr um dynamische Speicherzuweisung und Unterstützung für std::vector, aber Ergebnisse können nicht direkt zur Laufzeit übertragen werden. Template-Wrapper sind erforderlich.
Übergabe von Strings durch Templates
String-Literale werden über die const_string-Template-Klasse übergeben:
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;
};
// Verwendung:
template<const_string str>
auto sehr_schlanke_funktion(...) { /* ... */ }
Extrahieren von Arrays aus constexpr
Die Umwandlung von std::vector in std::array erfordert das Festlegen der Größe zur Compile-Zeit über eine Lambda-Funktion:
template<auto data_getter>
constexpr auto zu_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 zu_array<data_getter>();
}
Behandlung von Kompilierungsfehlern
Bis C++26 erfordert static_assert String-Literale. Die Lösung ist ein ErrorMessage-Template mit dem Fehlertext im Parameter:
template<const_string Msg>
struct ErrorMessage {
static_assert(false, "Details im Template-Parameter prüfen");
};
template<auto err_getter>
consteval auto fehler_melden() -> void {
#ifdef KORKA_FEATURE_FORMATTED_STATIC_ASSERT
static_assert(false, zu_string(err_getter()));
#else
constexpr auto msg = const_string_from_string_view<[] { return zu_string(err_getter()); }>();
std::ignore = ErrorMessage<msg>{};
#endif
}
Ausgabe in C++26: Lexer-Fehler: Unbeendeter String in Zeile 12.
Zuordnung von Funktionssignaturen
Um String-Namen mit Signaturtypen abzugleichen, werden Hashing und Überladung verwendet:
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 _überladen = überladen{
([] (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(_überladen(unique_type<hash(name)>{}))>;
};
Extraktion: compile_result.function<"fib">();.
Native Funktionsbindungen
Verknüpfung von C++-Funktionen mit der Skriptsprache über wrapped_function:
consteval auto umwickeln(std::string_view name) {
return wrapped_function<std::decay_t<decltype(func)>>{
binding_wrapper<func>,
name
};
}
constexpr auto bindings = korka::bindungen_erstellen(
korka::umwickeln<fib>("cpp_fib"),
korka::umwickeln<print_n>("print_n")
);
binding_wrapper generiert Code, um Argumente aus der VM zu extrahieren und Ergebnisse zurückzugeben.
Compiler-Architektur
Aufgeteilt in drei Phasen:
- Lexer: Tokenisierung in einer Zeichenschleife
- Parser: AST-Konstruktion
- Bytecode-Generator: Emission mit semantischer Analyse
Beispiel-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;
}
}
Wichtige Punkte
- Der Compiler ist vollständig constexpr, mit Bytecode im Binärformat eingebettet
- Workarounds für C++20-Einschränkungen über const_string und Lambda-Wrapper
- Typsichere Funktionsextraktion mit Namens-Hashing
- C++-Funktionsbindungen mit automatischer VM-Code-Generierung
- Grundlegender Lexer/Parser ohne Optimierungen
— Editorial Team
Noch keine Kommentare.