Volver al inicio

Compilador C en constexpr C++

El artículo describe la implementación de un compilador de subconjunto de C en C++ en tiempo de compilación. Cubre evitando limitaciones de constexpr, mapeo de firmas, enlaces de funciones nativas y arquitectura lexer/parser.

Compilador C completamente en constexpr
Advertisement 728x90

Un compilador de lenguaje similar a C en tiempo de compilación en C++

Se ha desarrollado un compilador para un subconjunto del lenguaje C, implementado en C++ dentro de un contexto constexpr. El código fuente se transforma en bytecode personalizado para su ejecución en una máquina virtual durante el tiempo de ejecución. Este enfoque garantiza cero sobrecarga del intérprete y seguridad de tipos completa.

Las ventajas incluyen la ausencia de código del compilador en el binario, sin compilación en tiempo de ejecución y protección contra desajustes de firmas de funciones. Desventajas: falta de optimizaciones a nivel LLVM e incapacidad para recargar en caliente.

Superando las limitaciones de constexpr

C++20 extiende constexpr con asignación dinámica de memoria y soporte para std::vector, pero los resultados no se pueden transferir directamente al tiempo de ejecución. Se requieren envoltorios de plantillas.

Google AdInline article slot

Pasando cadenas a través de plantillas

Los literales de cadena se pasan mediante la clase de plantilla 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;  
};

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

Extrayendo arreglos de constexpr

Convertir std::vector a std::array requiere fijar el tamaño en tiempo de compilación mediante una función 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>();
}

Manejo de errores de compilación

Hasta C++26, static_assert requiere literales de cadena. La solución es una plantilla ErrorMessage con el texto del error en el parámetro:

Google AdInline article slot
template<const_string Msg>  
struct ErrorMessage {  
  static_assert(false, "Consulta el parámetro de plantilla para más detalles");  
};

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
}

Salida en C++26: Error del lexer: Cadena sin terminar en la línea 12.

Mapeo de firmas de funciones

Para emparejar nombres de cadenas con tipos de firma, se utilizan hashing y sobrecarga:

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

Extracción: compile_result.function<"fib">();.

Google AdInline article slot

Vinculaciones de funciones nativas

Enlace de funciones C++ con el lenguaje de scripting mediante 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 genera código para extraer argumentos de la VM y devolver resultados.

Arquitectura del compilador

Dividida en tres etapas:

  • Lexer: Tokenización en un bucle de caracteres
  • Parser: Construcción del AST
  • Generador de bytecode: Emisión con análisis semántico

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

Puntos clave

  • El compilador es completamente constexpr, con bytecode incrustado en el binario
  • Soluciones para limitaciones de C++20 mediante const_string y envoltorios lambda
  • Extracción de funciones con seguridad de tipos usando hashes de nombres
  • Vinculaciones de funciones C++ con generación automática de código para la VM
  • Lexer/parser básico sin optimizaciones

— Editorial Team

Advertisement 728x90

Leer después