C++ 컴파일 타임에서 C 언어 컴파일러 구현하기
C 언어의 부분 집합을 위한 컴파일러가 C++의 constexpr 환경에서 개발되었습니다. 소스 코드는 런타임에 가상 머신에서 실행되도록 맞춤형 바이트코드로 변환됩니다. 이 접근 방식은 인터프리터 오버헤드가 전혀 없고 완전한 타입 안전성을 보장합니다.
장점으로는 바이너리에 컴파일러 코드가 포함되지 않으며, 런타임 컴파일이 필요 없고, 함수 시그니처 불일치로부터 보호됩니다. 단점: LLVM 수준의 최적화가 부족하고 핫 리로드가 불가능합니다.
constexpr 제약 극복하기
C++20은 동적 메모리 할당과 std::vector 지원으로 constexpr을 확장했지만, 결과를 직접 런타임으로 전달할 수는 없습니다. 템플릿 래퍼가 필요합니다.
템플릿을 통한 문자열 전달
문자열 리터럴은 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;
};
// 사용법:
template<const_string str>
auto very_smart_function(...) { /* ... */ }
constexpr에서 배열 추출하기
std::vector를 std::array로 변환하려면 람다 함수를 통해 컴파일 타임에 크기를 고정해야 합니다:
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>();
}
컴파일 오류 처리
C++26 이전에는 static_assert에 문자열 리터럴이 필요합니다. 해결책은 오류 텍스트를 매개변수로 갖는 ErrorMessage 템플릿입니다:
template<const_string Msg>
struct ErrorMessage {
static_assert(false, "템플릿 매개변수를 확인하세요");
};
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
}
C++26에서 출력: Lexer Error: 12번 줄에서 종료되지 않은 문자열.
함수 시그니처 매핑
문자열 이름과 시그니처 타입을 매치시키기 위해 해싱과 오버로딩이 사용됩니다:
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)>{}))>;
};
추출: compile_result.function<"fib">();.
네이티브 함수 바인딩
wrapped_function을 통해 C++ 함수와 스크립팅 언어를 연결합니다:
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는 VM에서 인수를 추출하고 결과를 반환하는 코드를 생성합니다.
컴파일러 아키텍처
세 단계로 나뉩니다:
- 렉서: 문자 루프에서 토큰화
- 파서: AST 구성
- 바이트코드 생성기: 의미 분석과 함께 방출
렉서 예시:
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;
}
}
핵심 포인트
- 컴파일러는 완전히 constexpr이며, 바이트코드는 바이너리에 내장됨
- C++20 제약을 극복하기 위한 const_string과 람다 래퍼 사용
- 이름 해시를 이용한 타입 안전 함수 추출
- 자동 VM 코드 생성과 함께하는 C++ 함수 바인딩
- 최적화 없이 기본적인 렉서/파서
— Editorial Team
아직 댓글이 없습니다.