Retour à l'accueil

Compilateur C en constexpr C++

L'article décrit l'implémentation d'un compilateur pour un sous-ensemble C en C++ à temps de compilation. Il couvre le contournement des limitations de constexpr, la cartographie des signatures, les liaisons de fonctions natives, et l'architecture lexer/parser.

Compilateur C entièrement en constexpr
Advertisement 728x90

Un compilateur de langage de type C au moment de la compilation en C++

Un compilateur pour un sous-ensemble du langage C a été développé, implémenté en C++ dans un contexte constexpr. Le code source est transformé en bytecode personnalisé pour exécution dans une machine virtuelle lors de l'exécution. Cette approche garantit une surcharge d'interpréteur nulle et une sécurité de type totale.

Les avantages incluent l'absence de code de compilation dans le binaire, pas de compilation à l'exécution et une protection contre les incompatibilités de signatures de fonctions. Inconvénients : manque d'optimisations de niveau LLVM et impossibilité de rechargement à chaud.

Contourner les limitations de constexpr

C++20 étend constexpr avec l'allocation dynamique de mémoire et le support de std::vector, mais les résultats ne peuvent pas être directement transférés à l'exécution. Des wrappers de templates sont nécessaires.

Google AdInline article slot

Passer des chaînes via des templates

Les littéraux de chaîne sont passés via la classe template 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;  
};

// Utilisation :
template<const_string str>
auto very_smart_function(...) { /* ... */ }

Extraire des tableaux de constexpr

Convertir std::vector en std::array nécessite de fixer la taille au moment de la compilation via une fonction 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>();
}

Gérer les erreurs de compilation

Jusqu'à C++26, static_assert nécessite des littéraux de chaîne. La solution est un template ErrorMessage avec le texte d'erreur dans le paramètre :

Google AdInline article slot
template<const_string Msg>  
struct ErrorMessage {  
  static_assert(false, "Vérifiez le paramètre template pour les détails");  
};

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
}

Sortie en C++26 : Erreur de lexer : Chaîne non terminée à la ligne 12.

Associer les signatures de fonctions

Pour faire correspondre les noms de chaînes avec les types de signatures, le hachage et la surcharge sont utilisés :

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

Liaisons de fonctions natives

Lier des fonctions C++ avec le langage de script 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 génère du code pour extraire les arguments de la VM et retourner les résultats.

Architecture du compilateur

Divisée en trois étapes :

  • Lexer : Tokenisation dans une boucle de caractères
  • Parser : Construction de l'AST
  • Générateur de bytecode : Émission avec analyse sémantique

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

Points clés

  • Le compilateur est entièrement constexpr, avec le bytecode intégré dans le binaire
  • Contournements des limitations de C++20 via const_string et wrappers lambda
  • Extraction de fonctions type-safe utilisant des hachages de noms
  • Liaisons de fonctions C++ avec génération automatique de code VM
  • Lexer/parser basique sans optimisations

— Editorial Team

Advertisement 728x90

Lire ensuite