Roc 컴파일러 재작성: Rust에서 Zig로의 진행 상황 및 교훈
Roc 컴파일러 재작성: Rust에서 Zig로의 진행 상황 및 교훈
기능 동등성 마일스톤
새로운 Zig 기반 Roc 컴파일러가 이제 원래 Rust 컴파일러와 기능적으로 동일합니다.
- 이 마일스톤을 통해 Rocci Bird WASM‑4 게임(1 K 라인 미만의 Roc 코드)이
roc build --opt=size로 31 KB Wasm 바이너리로 컴파일되었으며, 이는 Rust 버전의 절반 크기입니다. - 이 성과는 올해 말에 예정된 v0.1.0 출시를 향한 사전 릴리스 단계입니다.
- 기여자 중에는 Anthony Bullard, Sam Mohr, Jared Ramirez, Ayaz Hafiz, Aurélien Geron, Stephan, Niclas Åhdén, JRI98, Jasper Woudenberg, 그리고 주요 기여자인 Anton‑4와 Luke Boswell이 포함됩니다.
"그 큰 보라색 픽셀들을 보니 마침내 도달했을 때 얼굴에 미소가 떠올랐어요!" – Joran Greef
핫 코드 로딩 및 교차 컴파일
Roc는 이제 개발 중에 핫‑코드 리로드와 결정론적 교차 컴파일을 지원합니다.
roc server.roc를 실행하면 웹 서버가 시작되고, 소스를 편집하면 서버를 재시작하지 않아도 실시간으로 업데이트됩니다.roc build --target=x64musl은 macOS, Linux, 혹은 어떤 플랫폼에서도 동일한 바이너리를 생성합니다. 이는 모든 컴파일러가 제공하지 않는 보증입니다.
문자열 보간을 이용한 패턴 매칭
문자열 보간을 패턴 매칭 내부에서 직접 사용할 수 있어 할당 없이 라우팅 코드를 작성할 수 있습니다.
match (verb, path) {
("GET", "/users/${id}/${page}") => match page {
"" | "profile" => ok(id)
"settings" => ok(with_default(user_agent, id))
"posts/${post_id}" => ok("Post ID: ${post_id}")
_ => not_found
}
("GET", "/users/${id}") => ok(id)
("POST", "/posts/new") => created(with_default(...))
_ => not_found
}
- 라우팅 로직은 힙 할당을 전혀 하지 않으며 컴파일 시 타입‑안전성을 보장합니다.
- 실시간 데모는 roc‑lang.org 홈페이지에서 브라우저 내 WebAssembly 컴파일러를 통해 이용할 수 있습니다.
왜 처음부터 다시 작성했나요?
Roc는 단순 포팅이 아니라 근본적인 아키텍처 개편이 필요했습니다.
- 기존 컴파일러의 람다‑셋 특수화(다형성 디펑셔널리제이션)가 버그가 많아 여러 단계에 걸쳐 재설계가 필요했습니다.
- 전체 재작성은 "테세우스의 배"와 같은 점진적 리팩터링을 피하고, 컴파일러 프로젝트에서 흔히 보는 "처음부터 다시 쓰기" 관행에 맞추었습니다.
왜 Zig를 선택했나요?
네 가지 실용적인 고려사항이 결정을 이끌었습니다:
- 빌드 시간 – Zig의 증분 빌드는 피드백 루프를 크게 빠르게 합니다.
- 메모리 제어 – Zig의 할당자 중심 생태계가 Roc의 arena 기반 설계와 맞아떨어지며, Rust는 전역 할당자를 가정합니다.
- 생태계 적합성 – 필요한 저수준 LLVM 비트코드 도구가 Zig에는 있었지만 Rust에는 없었습니다.
- 메모리‑안전성 지원 – Zig는 프로젝트가 많이 사용하는 unsafe 섹션에 대해 더 세밀한 unsafe‑code 검사를 제공(
ReleaseSafe등)합니다.
Borrow‑Checking 없이 살아가기
메모리‑안전성 통계와 실제 관찰:
- 2019년 Microsoft 연구에 따르면 연간 보안 패치 중 **~70 %**가 메모리‑안전성 버그와 관련됩니다.
- Zig의
ReleaseSafe는 런타임에 use‑after‑free 를 잡아내고,ReleaseFast는 프로덕션에서 검사를 건너뜁니다. - Rust의 borrow checker 는 많은 unsafe 버그를 없애지만, Roc 300 K 라인 코드에서 ≈1 200 unsafe 블록(≈0.4 % 코드)이 여전히 필요합니다.
- 실제로 두 언어 모두 컴파일러가 잘못된 코드를 생성하면 메모리 손상 버그가 발생할 수 있습니다.
재작성 후 메모리‑안전성
버그 보고서 분석(Claude Opus 4.8 분류):
| 버그 유형 | Rust 컴파일러 | Zig 컴파일러 |
|---|---|---|
| 메모리‑손상 버그 | 21 | 10 |
| 비‑손상 버그 | 2 575 | 421 |
| 총계 | 2 596 | 431 |
- Rust의 21개 손상 버그는 모두 miscompilation(생성된 프로그램이 메모리를 손상)이며, 컴파일러 자체 로직의 버그는 아니었습니다.
- Zig의 10개 손상 버그 중 8은 miscompilation, 2는 실제 컴파일러 버그(에러 보고 코드에서 use‑after‑free)였으며, 이는 Rust의 borrow checker 로는 방지될 수 있었을 것입니다.
- 가상 시나리오:
- Zig
ReleaseSafe: 동일한 두 버그가 panic을 일으킵니다. - Rust: 두 버그 모두 컴파일 시점에 잡힙니다.
- Zig
- 핵심 요약: Roc 사용 사례에서는 실질적인 안전성 차이가 거의 없었습니다.
빌드 시간 비교
| 컴파일러 | 코드 라인 | 콜드 빌드 | 증분 재빌드 |
|---|---|---|---|
| Rust 1.85 (원본) | 354 K | 32.4 s | 10.0 s |
| Rust 1.97 (현재) | 354 K | 25.4 s | 3.4 s |
| Zig 0.16 (기능 동등) | 320 K | 39.6 s | 8.6 s |
| Zig 0.17 (현재) | 464 K | 32.1 s | 0.035 s |
- Zig의 현재 코드베이스에 대한 증분 재빌드는 35 ms로, Rust의 3.4 s보다 100배 이상 빠릅니다(18개월간 Rust가 개선된 뒤에도).
- Zig 0.16에서는
-fincremental플래그가 깨지는 버그가 있었으며, 이는 곧 출시될 0.17에서 해결됩니다. - Rust의 빌드 시간 감소(2/3 감소)는 인상적이지만, Zig의 증분 속도에는 아직 미치지 못합니다.
Zero‑Parse 역직렬화 (메모리 제어)
Roc는 컴파일러 데이터 구조를 직접 디스크에 캐시하고 파싱 없이 다시 로드합니다.
- 데이터 구조는 32‑bit 인덱스와 구조‑오브‑배열 레이아웃을 사용해 직접 바이너리 덤프가 가능합니다.
- 재로드는 본질적으로
memcpy수준의 속도로, 디스크 I/O 혹은 OS 캐시만이 병목이 됩니다. - 이 기법은 Zig가 세밀한 할당자를 관리할 수 있기 때문에 가능했으며, Rust의 전역 할당자 모델에서는 광범위한 unsafe 코드 없이 안전성을 보장하기 어렵습니다.
생태계 적합성
- Zig는 즉시 사용 가능한 LLVM 비트코드 생성기와 할당자 중심 라이브러리를 제공해 Roc의 요구에 바로 맞춥니다.
- Rust의 생태계는 전역 할당자와
Drop기반 정리를 전제로 하여, Roc의 arena 기반 설계와 충돌했습니다. - Roc가 가장 재사용하기 좋은 구성 요소는 Zig 자체 컴파일러였으며, 이미 필요한 LLVM 비트코드 직렬화를 구현하고 있었습니다.
Rust에서 놓친 것들
- 자동 테스트 할당자 정리 – Rust 테스트는 leak을 자동으로 감지해
defer/init코드를 수동으로 작성할 필요가 없습니다. - 파라메트릭 및 임시 다형성 – Rust의 제네릭 시스템은 Zig의
comptime기반 접근보다 풍부합니다. - 프라이빗 구조체 필드 – Rust의 가시성 한정자는 실수로 필드에 접근하는 것을 컴파일 시점에 방지합니다.
- 일관된 하위 호환성 – Rust의 안정적인 릴리스 정책은 업그레이드를 무리 없이 진행하게 하지만, Zig는 아직 1.0 미만이라 파괴적 변경이 예상됩니다.
Zig에서 즐긴 점
- 매크로 없음 – 언어 표면이 더 단순합니다.
- 세밀한 데이터 레이아웃 제어 – 비전형적인 정수 타입(
u7,u5)과 패킹 구조체를 직접 지원합니다. - 강력한 빌드 시스템 – 단일 명령으로 Alpine, WebAssembly, 정적 바이너리까지 교차 컴파일이 가능합니다.
- 오류 처리 – 명시적인
try/catch스타일이 Roc의 합 타입 오류 모델과 잘 맞습니다. - 할당자 중심 API – Roc 설계와 일치해 별도 우회가 필요 없습니다.
Roc의 다음 단계는?
- 올해 말 v0.1.0 릴리스를 목표로 합니다(첫 번째 번호가 매겨진 릴리스).
- 진행 중인 작업에는 문서화, 표준 라이브러리 다듬기, Roc Exercism 연습문제 확장이 포함됩니다.
- 프로젝트는 Roc Programming Language Foundation(501(c)(3) 비영리)과 여러 기업 스폰서의 지원을 받고 있습니다.
커뮤니티 반응 (선정된 HN 댓글)
"Zig의 증분 빌드는 강력한 기능이에요. 단기적으로는 왜 전환했는지 이해하지만, Rust가 따라잡을 수 있을까요?" – onlyrealcuzzo
"메모리‑unsafe 코드는 머신 코드 생성보다는 런타임에 더 큰 영향을 미칩니다." – steveklabnik
"Zig의
ReleaseSafe는 실제로 use‑after‑free 검사를 문서화하지 않아요; 주장에 과장이 있는 것 같습니다." – landr0id
"빌드 시간 수치는 인상적이지만, 비교가 성숙한 Rust 코드베이스와 새로 만든 Zig 코드베이스를 섞고 있습니다." – dminik
"Zig는 아직 1.0 미만이고 Rust는 1.0 이후라, 이것만으로도 많은 개발자 선택에 영향을 줍니다." – dev_l1x_be
결론
Roc 재작성은 Zig가 훨씬 빠른 증분 빌드와 세밀한 메모리 제어를 제공하면서도 원래 Rust 컴파일러와 비슷하거나 경우에 따라 더 나은 메모리‑안전성 결과를 얻을 수 있음을 보여줍니다. 트레이드‑오프는 생태계가 덜 성숙하고 파괴적 변경이 가끔 발생하며, 자동 테스트 할당자 정리와 같은 Rust의 편리함을 잃는다는 점입니다. 전반적으로 프로젝트 진행 상황은 맞춤형 할당자, zero‑parse 역직렬화, 핫‑코드 리로드에 크게 의존하는 컴파일러에 Zig를 채택한 결정이 타당함을 입증합니다.