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 需要一次根本性的架構大改,而不是簡單的移植。
- 原始編譯器的 lambda‑set specialization(多態去函式化)存在錯誤,且需要在多個階段重新設計。
- 完全重寫避免了「Theseus 船」式的漸進式重構,並與編譯器專案常見的從頭重寫做法保持一致。
為什麼選 Zig 而非 Rust?
四個實務考量驅動了這個決策:
- 建置時間 – Zig 的增量建置承諾大幅加快回饋迴路。
- 記憶體控制 – Zig 以分配器為中心的生態系統與 Roc 的 arena 設計相符,而 Rust 假設全域分配器。
- 生態系相關性 – 需要的低階 LLVM 位元碼工具在 Zig 中已有,但在 Rust 中尚未出現。
- 記憶體不安全協助 – Zig 提供更細緻的 unsafe 程式碼檢查(例如
ReleaseSafe),適合大量使用 unsafe 區段的專案。
沒有 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 |
- 所有 21 件 Rust 的毀損錯誤都是 編譯錯誤(產生的程式會毀損記憶體),而非編譯器本身的邏輯錯誤。
- 在 10 件 Zig 的毀損錯誤中,8 為編譯錯誤;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 倍,即使 Rust 已經經過 18 個月的優化。
- Zig 0.16 曾因
-fincremental失效而出現 bug;此問題已在即將到來的 0.17 版中修正。 - Rust 的建置時間改善(減少至三分之二)固然令人印象深刻,但仍遠不及 Zig 的增量速度。
零解析度反序列化(記憶體控制)
Roc 直接將編譯器資料結構快取到磁碟,並在不解析的情況下重新載入。
- 資料結構使用 32 位元索引 與 結構化陣列(structure‑of‑arrays) 版型,允許直接二進位轉儲。
- 重新載入本質上是
memcpy速度的操作,唯一的瓶頸只會是磁碟 I/O 或 OS 快取。 - 此技巧依賴 Zig 能細緻管理分配器;Rust 的全域分配器模型使得在不大量使用 unsafe 的情況下保證安全變得更困難。
生態系契合度
- Zig 提供即用的 LLVM 位元碼產生器與以分配器為中心的函式庫,直接對應 Roc 的需求。
- Rust 的生態系假設全域分配器與基於
Drop的清理機制,與 Roc 的 arena 設計產生衝突。 - 對 Roc 來說最有價值的可重用元件是 Zig 自身的編譯器,它已經實作了所需的 LLVM 位元碼序列化功能。
從 Rust 中失去的東西
- 自動測試分配器清理 – Rust 測試會自動偵測記憶體洩漏,無需手動
defer/init程式碼。 - 參數化與即席多態 – Rust 的泛型系統比 Zig 的
comptime方法更為豐富。 - 私有結構欄位 – Rust 的可見性修飾子在編譯期提供防止意外存取欄位的安全性。
- 一致的向後相容性 – Rust 的穩定發佈政策讓升級毫無痛感;Zig 尚未 1.0,破壞性變更是預期的。
喜愛 Zig 的地方
- 沒有巨集 – 語言表面更簡潔。
- 細緻的資料版型控制 – 直接支援非二的整數型別(
u7、u5)與緊湊結構。 - 強大的建置系統 – 單指令即可跨編譯至 Alpine、WebAssembly 與靜態二進位檔。
- 錯誤處理 – 明確的
try/catch風格與 Roc 的 sum‑type 錯誤模型相契合。 - 以分配器為中心的 API – 完全符合 Roc 的設計,無需額外的變通方案。
Roc 接下來的規劃
- 目標在今年稍後推出 v0.1.0(第一個有編號的正式版)。
- 持續工作包括文件撰寫、標準函式庫打磨,以及擴充 Roc Exercism 練習題。
- 專案由 Roc Programming Language Foundation(501(c)(3) 非營利組織)以及多家企業贊助者支持。
社群回應(精選 HN 評論)
"Zig 的增量建置是個殺手級功能。短期內我能理解你們為何切換,但 Rust 能追上嗎?" – onlyrealcuzzo
"我認為記憶體不安全的程式碼在產生機器碼時並不是主要問題;重點在執行階段。" – steveklabnik
"Zig 的
ReleaseSafe並未真的文件化 use‑after‑free 檢查;這個說法感覺有點誇大。" – landr0id
"建置時間的數據很吸睛,但比較的是成熟的 Rust 程式碼庫與全新建立的 Zig 程式碼庫。" – dminik
"Zig 還未 1.0,而 Rust 已經超過 1.0;這本身就會影響許多開發者的選擇。" – dev_l1x_be
結論
Roc 的重寫證明 Zig 能提供極速的增量建置與細緻的記憶體控制,同時在記憶體安全上與原始的 Rust 編譯器相當(甚至在某些情況下更好)。代價則是較不成熟的生態系、偶爾的破壞性變更,以及失去 Rust 某些便利功能(如自動測試分配器清理與更豐富的泛型抽象)。總體而言,專案的進展驗證了在高度依賴自訂分配器、零解析度反序列化與熱代碼重新載入的編譯器上採用 Zig 的決策是合理的。