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会启动一个 Web 服务器;编辑源码后服务器会实时更新,无需重启。 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 特化(多态去函数化)存在缺陷,必须在多个阶段重新设计。
- 完全重写避免了“忒修斯之船”式的增量重构,并且符合编译器项目常见的从零开始重写的做法。
为什么选 Zig 而不是 Rust?
四个实际考量驱动了这一决定:
- 构建时间 – Zig 的增量构建承诺显著加快反馈循环。
- 内存控制 – Zig 以分配器为中心的生态与 Roc 基于 arena 的设计相匹配,而 Rust 假设全局分配器。
- 生态相关性 – 所需的低层 LLVM bitcode 工具在 Zig 中已有实现,而 Rust 中缺乏。
- 内存不安全辅助 – Zig 提供更细粒度的 unsafe 代码检查(如
ReleaseSafe),适合大量使用 unsafe 部分的项目。
没有 Borrow‑Checking 的生活
内存安全统计与实际观察:
- 2019 年微软的一项研究显示,约 70 % 的年度安全补丁是针对内存安全缺陷。
- Zig 的
ReleaseSafe在运行时捕获 use‑after‑free;ReleaseFast在生产环境下跳过检查。 - Rust 的 borrow checker 消除了大量 unsafe bug,但在 Roc 30 万行代码中仍需要 ≈1 200 个 unsafe 块(约占代码的 0.4 %)。
- 实际上,两种语言在编译器生成错误代码时都可能导致内存损坏 bug。
重写后的内存安全性
Bug 报告分析(由 Claude Opus 4.8 分类):
| Bug 类型 | Rust 编译器 | Zig 编译器 |
|---|---|---|
| 内存损坏 bug | 21 | 10 |
| 非损坏 bug | 2 575 | 421 |
| 总计 | 2 596 | 431 |
- 所有 21 条 Rust 损坏 bug 均为 误编译(生成的程序导致内存损坏),而非编译器自身逻辑错误。
- 在 10 条 Zig 损坏 bug 中,8 为误编译;2 为真实的编译器 bug(错误报告代码中的 use‑after‑free),这些在 Rust 的 borrow checker 下本会被阻止。
- 假设的替代方案:
- Zig
ReleaseSafe:同样的两条 bug 会触发 panic。 - Rust:会在编译期捕获这两条 bug。
- 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 的增量速度。
- Zig 0.16 中的一个 bug 破坏了
-fincremental,已在即将发布的 0.17 版中修复。 - Rust 的构建时间提升(降低至三分之二)虽令人印象深刻,但仍远落后于 Zig 的增量速度。
零解析反序列化(内存控制)
Roc 将编译器数据结构直接缓存到磁盘,并在无需解析的情况下重新加载。
- 数据结构使用 32 位索引 和 结构体数组(structure‑of‑arrays) 布局,能够直接进行二进制转储。
- 重新加载本质上是一次
memcpy速度的操作,仅受磁盘 I/O 或 OS 缓存限制。 - 该技术依赖于 Zig 对细粒度分配器的管理能力;Rust 的全局分配器模型使得在不大量使用 unsafe 的情况下保证安全更加困难。
生态适配性
- Zig 提供即用的 LLVM bitcode 生成器和以分配器为中心的库,能够直接映射到 Roc 的需求上。
- Rust 的生态假设全局分配器和基于
Drop的清理,这与 Roc 的 arena 设计相冲突。 - 对 Roc 最有价值的可复用组件是 Zig 自身的编译器,它已经实现了所需的 LLVM bitcode 序列化功能。
从 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 是一个合理的决定。