아마추어 컴파일러 개발자의 시각에서 기계어/어셈블리, 중간 표현, 다른 고수준 언어, 가상 머신/바이트코드, WebAssembly, 메타 트레이싱/메타컴파일 프레임워크, 그리고 비정통 타깃까지 다양한 컴파일러 백엔드 옵션을 간단히 훑어봅니다.
아마추어 컴파일러 개발자로서 제가 늘 고민하는 결정 중 하나는 올바른 컴파일러 타깃을 고르는 일입니다. 사람들이 여러 머신 아키텍처를 직접 대상으로 삼아야 했던 80년대와 달리, 지금은 성숙한 선택지가 많이 있습니다. 이 글은 널리 쓰이거나 흥미로운 몇 가지 옵션을 간단히(그리고 매우 불완전하게) 살펴봅니다.
컴파일러는 언제나 하나 이상의 아키텍처를 대상으로 기계어 또는 어셈블리를 직접 출력할 수 있습니다. 잘 알려진 예로 Tiny C Compiler가 있습니다. 속도와 작은 크기로 유명하며, C 코드를 즉석에서 컴파일하고 실행할 수 있습니다. 또 다른 예로 Turbo Pascal이 있습니다. 여러분의 컴파일러로도 이렇게 할 수 있지만, 대상으로 삼고 싶은 각 아키텍처의 명령어 집합 (ISA)과 레지스터 할당 같은 개념의 미묘한 부분까지 파악해야 합니다.
대부분의 현대 컴파일러는 실제로는 기계어 또는 어셈블리를 직접 내보내지 않습니다. 먼저 소스 코드를 언어에 중립적인 중간 표현 (IR)으로 내리고, 그 뒤 주요 아키텍처(x86-64, ARM64 등)를 위한 기계어를 생성합니다.
이 영역에서 가장 두드러진 도구는 LLVM입니다. 거대한 오픈 소스 라이브러리형 컴파일러로, Rust, Swift, C/C++(Clang 경유), Julia 등 많은 언어의 컴파일러가 IR로 LLVM을 사용해 기계어를 방출합니다.
대안으로는 GNU Compiler Collection (GCC)이 있으며, GIMPLE IR을 통해 접근할 수 있지만 이를 직접 사용하는 컴파일러는 거의 없어 보입니다. GCC는 libgccjit을 통해 LLVM처럼 라이브러리로 코드를 컴파일하는 데 사용할 수 있습니다. 이는 Emacs에서 Elisp을 즉시 컴파일 (JIT)하는 데 쓰입니다. Cranelift도 최근 등장한 옵션이지만 지원하는 ISA는 적습니다.
LLVM이나 GCC가 너무 크거나 컴파일이 느리게 느껴진다면 미니멀한 대안도 있습니다. QBE는 단순함에 초점을 둔 작은 백엔드로, “코드의 10%로 성능의 70%”를 목표로 합니다. 빠른 컴파일 시간을 중시하는 언어 Hare에서 사용합니다. 또 다른 선택지로 libFIRM이 있는데, 선형 IR 대신 그래프 기반의 SSA 표현을 사용합니다.
때로는 다른 컴파일러/런타임에 무거운 일을 맡겨도 괜찮습니다. 코드를 다른 기성 고수준 언어로 트랜스파일하여 그 언어의 기존 컴파일러/런타임과 툴체인을 활용할 수 있습니다.
이런 경우 흔한 타깃은 C입니다. 거의 모든 플랫폼에 C 컴파일러가 존재하기 때문에 C 코드를 생성하면 여러분의 언어는 매우 이식성이 높아집니다. Chicken Scheme과 Vala가 쓰는 전략입니다. 혹은 Jank처럼 C++로 컴파일할 수도 있습니다. C–라는 C의 서브셋도 있는데, GHC와 OCaml이 이를 타깃팅했습니다.
또 다른 보편적 타깃은 JavaScript (JS)입니다. 이는 WebAssembly와 함께 웹 브라우저 또는 JS 런타임(Node, Deno, Bun)에서 코드를 네이티브에 가깝게 실행하는 두 가지 옵션 중 하나입니다. TypeScript, PureScript, Reason, ClojureScript, Dart, Elm 등 여러 언어가 JS로 트랜스파일합니다. 흥미롭게도 Nim은 C, C++, JS 중 원하는 것으로 트랜스파일할 수 있습니다.
JS와 비슷한 타깃으로 Lua가 있습니다. 가볍고 임베드하기 쉬운 스크립팅 언어로, MoonScript와 Fennel 같은 언어가 Lua로 트랜스파일합니다.
좀 더 마니악한 접근으로 Lisp 계열을 타깃으로 삼는 방법도 있습니다. 예를 들어 Chez Scheme로 컴파일하면 그 매크로 시스템, 런타임, 컴파일러를 그대로 활용할 수 있습니다. Idris 2와 Racket은 주된 백엔드 타깃으로 Chez Scheme을 사용합니다.
애플리케이션 언어에서 흔히 선택되는 방식입니다. 가상 머신 (VM)을 위한 이식 가능한 바이트코드로 컴파일합니다. VM은 일반적으로 가비지 컬렉션, JIT 컴파일, 보안 샌드박싱 같은 기능을 제공합니다.
Java Virtual Machine (JVM)은 아마 가장 인기 있는 VM일 것입니다. Java, Kotlin, Scala, Groovy, Clojure 등 많은 언어가 이를 타깃으로 합니다. 주요 경쟁자는 Microsoft가 원래 개발한 Common Language Runtime으로, C#, F#, Visual Basic.NET 같은 언어가 이를 대상으로 합니다.
또 다른 주목할 만한 VM은 BEAM으로, 원래 Erlang을 위해 만들어졌습니다. BEAM VM은 순수 계산 속도를 위해 만들어진 것이 아니라 높은 동시성, 장애 허용성, 신뢰성에 초점을 맞춥니다. 최근에는 Elixir와 Gleam 같은 새로운 언어들이 이를 타깃으로 만들어지고 있습니다.
마지막으로 이 범주에는 Parrot VM의 정신적 후계자로, Raku (이전의 Perl 6) 언어를 위해 만들어진 MoarVM도 포함됩니다.
WebAssembly (Wasm)는 비교적 새로운 타깃입니다. 보안과 효율성에 초점을 맞춘 이식 가능한 바이너리 명령 형식입니다. Wasm은 모든 주요 브라우저에서 지원되지만, 브라우저에만 국한되지는 않습니다. WebAssembly System Interface (WASI) 표준은 비브라우저·비-JS 환경에서 Wasm을 실행하기 위한 API를 제공합니다. 현재 Rust, C/C++, Go, Kotlin, Scala, Zig, Haskell 등 많은 언어가 Wasm을 타깃으로 합니다.
_메타 트레이싱_과 메타컴파일 프레임워크는 더 복잡한 범주입니다. 이들은 여러분의 컴파일러 백엔드 타깃이 아니라, 언어에 대한 인터프리터를 명세함으로써 그 언어를 위한 커스텀 JIT 컴파일러를 구축하는 데 사용하는 도구입니다.
가장 잘 알려진 예는 RPython 프레임워크로 만든 Python 구현체 PyPy입니다. 또 다른 프레임워크로 Oracle의 폴리글롯 VM이자 메타 트레이싱 프레임워크인 GraalVM/Truffle이 있습니다. 주된 특징은 제로 코스트 상호 운용성으로, GraalJS, TruffleRuby, GraalPy의 코드는 모두 같은 VM 위에서 실행되며 서로를 직접 호출할 수 있습니다.
주류를 넘어가 보면, 비정통적이고 에소테릭한 컴파일러 타깃의 세계가 펼쳐집니다. 개발자들은 학술적 호기심, 예술적 표현, 혹은 “컴파일 타깃으로 가능한 것”의 경계를 시험하기 위해 이런 타깃을 고르곤 합니다.
Brainfuck: 단 8개의 명령만 가진 에소테릭 언어 Brainfuck은 _튜링 완전_하며, 도전 과제로서 컴파일러의 타깃이 되곤 합니다. 사람들은 C, Haskell, 람다 계산용 컴파일러를 작성해왔습니다.
람다 계산: 람다 계산은 오로지 함수와 그 적용만으로 계산을 표현하는 최소한의 프로그래밍 언어입니다. 단순함과 계산의 본질과의 연관성 덕분에 교육용 컴파일러의 타깃으로 자주 쓰입니다. Haskell의 서브셋인 Hell은 단순형 람다 계산으로 컴파일됩니다.
SKI 컴비네이터: SKI 컴비네이터 미적분은 람다 계산보다도 더 미니멀합니다. SKI 미적분의 모든 프로그램은 S, K, I 세 가지 컴비네이터만으로 구성할 수 있습니다. MicroHs는 Haskell의 서브셋을 SKI 미적분으로 컴파일합니다.
JSFuck: 단 여섯 글자 []()!+만으로 모든 JavaScript 프로그램을 작성할 수 있다는 사실, 알고 계셨나요? 이제 아시겠죠.
Postscript: Postscript 역시 튜링 완전한 프로그래밍 언어입니다. 다음 컴파일러의 타깃으로 삼아보는 건 어떨까요!
저는 C++에서 JSFuck으로 컴파일하는 컴파일러를 만들려 합니다.
질문이나 의견이 있으시면 아래에 댓글을 남겨주세요. 이 글이 마음에 드셨다면 공유도 부탁드립니다. 읽어주셔서 감사합니다!
작성자: Abhinav Sarkar 게시 위치: https://abhinavsarkar.net/notes/2025-compiler-backend-survey/