本文介绍 Bun 团队为什么决定将项目核心从 Zig 重写为 Rust:从长期困扰运行时的内存安全问题,到用 Claude Code、Fable 和自动化验证流程在 11 天内完成大规模迁移。原文地址:Rewriting Bun in Rust。本文由LobsterAI自动翻译和发布。
Jarred Sumner · 2026 年 7 月 8 日
披露:Bun 已于 2025 年 12 月被 Anthropic 收购。我和 Bun 团队的其他成员在 Anthropic 工作。在 Rust 重写的大部分过程中,我使用了 Claude Fable 5 的预发布版本。
Bun 最初是把 esbuild 的 JavaScript 和 TypeScript 转译器从 Go 逐行移植到 Zig。我在 2021 年 4 月 16 日写下了第一行 Zig。看到 Hacker News 上那份单页的 Zig 语言参考后,我押注了 Zig;它对底层控制和性能的重视让我非常兴奋。
从一开始,Bun 的范围就极其庞大:
- JavaScript、TypeScript 和 CSS 转译器、压缩器和打包器
- 兼容 npm 的包管理器
- 类似 Jest 的测试运行器
- 与 Node.js 和 TypeScript 兼容的模块解析
- HTTP/1.1 和 WebSocket 客户端
- Node.js API 实现,例如
fs、net、tls以及其他数十个模块
Bun 的最初版本由我在 1 年内写成,当时是在奥克兰一间狭小的公寓里,还没有 LLM,用的是 Zig。像 Bun 这样范围雄心勃勃的项目,默认结局通常是加入 GitHub 个人主页上那些死掉的副项目墓园。Zig 让 Bun 成为可能。如果不是 Zig,我绝不可能在 1 年内做出这么多东西。
如今,Bun 的 CLI 每月下载量超过 2200 万次。Claude Code 和 OpenCode 等流行工具都押注 Bun 作为它们的运行时。Vercel、Railway、DigitalOcean 等也为 Bun 提供第一方支持。
Bun 的范围也给稳定性带来了挑战。下面是我们在 Bun v1.3.14 中修复的一小部分 bug:
- 在
node:zlib中,当 zlib、Brotli 或 Zstd 流的异步.write()仍在线程池中进行时调用.reset(),会导致 heap-use-after-free 崩溃 - 在
node:zlib中,当onerror回调对原生句柄发起重入的write()后又调用close()时,会导致 use-after-free 崩溃 - 在
node:http2中,当重入的 JS 回调(例如在 timeout 监听器、options getter 或 write 回调中调用session.request())触发 hashmap rehash,使内部 stream 指针失效时,会导致 use-after-free 崩溃 UDPSocket.send()和sendMany()中的 use-after-free:用户代码可在valueOf()或toString()回调中,在捕获 payload 与真正发送之间分离ArrayBufferBuffer#copy和Buffer#fill中的崩溃与越界读取:当valueOf回调在参数强制转换期间分离或调整底层ArrayBuffer大小时发生UDPSocket.sendMany()中的堆越界写入:socket 的连接状态在迭代中途通过用户 JS 回调发生改变时发生crypto.scrypt中的内存泄漏:当输出缓冲区分配失败时,callback 以及受保护的 password/salt buffer 从未释放SSLWrapper.init在错误路径上泄漏了 strdup 出来的 passphrasetlsSocket.setSession()中的内存泄漏:由于在d2i_SSL_SESSION后缺少SSL_SESSION_free,每次调用都会泄漏一个SSL_SESSION(每次约 6.5 KB)fs.watch()watcher 在.close()后从未被垃圾回收的内存泄漏:原因是引用计数下溢,导致每个 watcher 被永久固定为 GC root- CSS 解析器中的 double-free 崩溃:当
background-clip带有厂商前缀且存在多层背景时发生 DuplexUpgradeContext从未释放——每次tls.connect({ socket: duplex })都会完整泄漏一次MessageEvent中的竞态条件崩溃:GC 标记线程可能在来自BroadcastChannel或MessagePort的并发访问期间,观察到m_data中被撕裂的 variant
我们本可以永远逐个修这些 bug,但用户依赖我们,我们有责任做得更好,并系统性地防止这类 bug 反复出现。
我们已经在做的事
- 我们给 Zig 编译器打补丁,加入了 Address Sanitizer 支持。我们在每次提交上都用 ASAN 运行测试套件。
- 我们在 Windows 上发布带 Zig 安全检查的 ReleaseSafe 构建
- 我们使用 Fuzzilli 7×24 小时 fuzz Bun 的运行时 API;Fuzzilli 是 V8 和 JavaScriptCore 使用的 JavaScript 引擎 fuzzer
- 我们有大量端到端的内存泄漏测试
这已经比很多项目做得更多了。
只要特别聪明、别犯错就行?
我们的 bug 修复列表让人感觉很糟,我也厌倦了睡前还担心 Bun 会崩溃。我并不怪 Zig——其他 Zig 用户没有我们遇到的这些 bug,而且把 GC 与手动管理内存混在一起,是软件中并不常见的需求,以至于没有哪门语言真正为它而设计。如果没有 Zig,我们走不到今天,我也会一直心怀感激。直到非常近以前,对于像 Bun 这样的项目来说,编程语言选择都是一个单向决定。
JavaScript 是一门垃圾回收语言,而 JavaScriptCore(以及 V8)等现代 JavaScript 引擎对异常处理和垃圾回收器有严格规则。Zig 和 C 一样,不会替你管理内存;对很多项目来说,这种取舍正是使用 Zig 的充分理由。Zig 没有构造函数/析构函数,大多数清理工作都预期在每个调用点用 defer 显式写出来。
对 Bun 来说,正确处理垃圾回收值和手动管理值的生命周期,一直是稳定性问题的主要来源——最常见的是小的内存泄漏,偶尔则是崩溃。每一次内存分配都必须被仔细审查。这些字节在哪里释放?我们如何确保它只释放一次?我们是否正确检查了 JavaScript 异常?这个垃圾回收指针对保守栈扫描器可见吗?这块内存是垃圾回收管理的,还是手动管理的?
对于稳定性问题,越早知道越好。Fuzzing 发生在代码合并之后。CI 发生在代码推送时。运行时安全检查和 address sanitizer 发生在代码运行时(希望是在开发阶段、CI 之前)。
减少这类问题的一种常见方式,是确保需要清理的代码,其清理逻辑总是恰好运行一次。Zig 被设计成一门简单、没有隐藏控制流的语言,因此它偏好显式的 defer 关键字,在作用域结束时运行代码,而不是 C++ 隐式的 ~Destructor 或 Rust 隐式的 Drop。
| Language | Cleanup |
|---|---|
| Zig | defer, errdefer |
| C++ | ~Destructor, &&Move |
| Rust | Drop |
对于 Zig 代码,我们到底应该在什么时候运行清理代码?如果我们把同一个 *T 传给许多不同函数,怎么知道它何时不再可访问、可以清理?当某些函数需要在函数调用之后继续引用这块内存时,它又该如何工作?我们目前的方法是几种方式混用:
- arena 生命周期,即可访问范围很明确的情况(parser state 不会逃逸出调用函数,因此 AST 节点适合这样处理)
- 引用计数
- 极其仔细地留意
许多项目选择通过风格指南来回答这类问题。TigerBeetle 的 TigerStyle 是 Zig 里的一个例子,Google 那份 31,000 字的 C++ 风格指南 是另一个。风格指南的挑战在于执行。你如何确保风格指南被遵守?历史上,答案是代码审查,并通过 linter 和静态分析器尽力执行。
对 Bun 来说,拥有一套严格的风格指南,并在类型系统中明确写出清晰的所有权预期,确实是一个可行选项。由于 Zig 没有运算符重载,我们很可能最终会写出大量类似这样的代码:
fn foo(a_ptr: SharedPtr(TCPSocket)) !void {
const a: *TCPSocket = a_ptr.get();
defer a_ptr.deref();
const b = try do_something_with_a(a);
defer b.deref();
// ...
}
这比我们期望的 Zig 写法更不符合人体工学:
fn foo(a: *TCPSocket) !void {
const b = try do_something_with_a(a);
// ...
}
那 C/C++ 呢?
Bun 大约 20% 的代码是用 C++ 写的,并且 Bun 嵌入了几个 C/C++ 库:
- JavaScriptCore,为 Safari 提供动力的 JavaScript 引擎
- uWebSockets 和 usockets——我们的 HTTP/WebSocket 服务器以及事件循环
- lshpack 和 lsquic——
HPACK和 HTTP/3 库 - BoringSSL,Google 的 OpenSSL fork
- SQLite
对 Bun 来说,用 C++ 而不是 Zig 会是一个合理选择。我们会得到构造函数和析构函数。我们可以删除大量 extern "C" 包装代码。
但是,我们仍然要依赖通过代码审查执行的风格指南;即便有 ASAN,内存破坏和内存泄漏仍然会发生。
为什么是 Rust?
上述列表中的很大一部分 bug 都是 use-after-free、double-free,以及在错误路径上“忘记释放”。在安全 Rust 中,这些都是编译器错误,并且有类似 RAII 的 Drop 自动清理。编译器错误比风格指南拥有更好的反馈循环。
从历史上看,重写都是糟糕的主意。排除注释,Bun 有 535,496 行 Zig。用另一门语言重写,需要一个小型工程师团队花整整一年。这意味着在这段时间里冻结 bug 修复、安全修复或功能开发。让东西可发布的最低风险方式,是从 Zig 到 Rust 做机械式移植,尽量减少行为变化,并使用我们已经用于测试 Bun 的同一套测试套件。
幸运的是,Bun 自己的测试套件是用 TypeScript 写的,这意味着它不依赖运行时所用的编程语言。
一年时间对用户没有任何可见影响,并不是一个现实可选项。因此,通过代码风格执行来修复稳定性问题,是我们最好的赌注;这也是我们把受 Rust 启发的智能指针加入 Bun 代码库时的计划。
但说实话,我并不想这么做。自制智能指针的人体工学比 Rust 更差,却没有任何相同的保证。
如果换个做法:我花一周时间测试 Anthropic 的新模型是否能把 Bun 重写成 Rust,会怎样?
起初,我并不期待它能成功。几天后,测试套件中很高比例开始通过,而且我看到新的 Rust 代码与原始 Zig 代码库非常匹配。我的看法从“这值得试试”变成了“我要把它合并进去”。
Claude,把 Bun 用 Rust 重写。
有很多方法可以把这件事做得非常糟糕。比如,提示 Claude:“Rewrite Bun in Rust. Don’t make any mistakes.”,然后祈祷它能成功——这不是我做的事。
想想一个人会怎么做这件事。第一个大问题是:
增量重写?还是一次性全部重写?
根据我在 Bun 初始版本中把 esbuild 的转译器从 Go 移植到 Zig 的经验(那时没有 LLM),一次性全部重写更好。增量重写会加入你希望最终能被删除的临时代码,而且在短中期会非常痛苦。
第二个大问题是:怎么做?
我们如何让 Rust 版 Bun 仍然是之前那个 Bun,拥有相同的架构、性能和功能集,同时又能获得 Rust 的语言特性,比如借用检查器?我们如何确保团队在重写之后仍然能维护它?
做一次看起来像是把我们的 Zig 代码转译成 Rust 的重写。等 Bun v1.4 发布之后,我们可以逐步重构它,减少 unsafe 的使用,并让它看起来更像惯用的 Rust。
大问题就这两个。其他一切都是战术。
编写与审查代码的循环
软件工程师的许多日常工程工作,都可以被过度简化为循环。
// Pseudocode, not real code:
let task;
while ((task = todoList.pop())) {
const result = task();
const feedback = await Promise.all([review(result), review(result)]);
await apply(feedback, result);
}
一个 task 会带有一些相关上下文(一个 Jira 工单、一个 GitHub issue 等)。result 是你为修复它而写的代码。代码审查者会 review 这些变更,检查回归问题与正确性。然后你处理反馈。
我使用了大约 50 个在 Claude Code 中持续运行的动态工作流,在 11 天里把 Bun 用 Rust 重写了。
每个动态工作流都是这样的一个循环——一个用于以下任务的工作流:
- 生成一份移植指南,将 Zig 模式与类型映射到 Rust 模式与类型
- 按照 PORTING.md 和 LIFETIMES.tsv,把每个
.zig文件机械式地移植成一个.rs文件 - 修复每个 crate 的编译器错误
- 让
bun test或bun build这样的子命令能够工作 - 让 Bun 整个测试套件中的每个测试都通过
- 若干大型重构和清理轮次
在这 11 天中的大部分时间里(以及之后),我都在监控这些工作流——手动阅读输出,检查问题和 bug,并提示 Claude 编辑循环来修复问题。
你如何审查一个新增了 100 多万行代码的 PR?你如何开始建立足够的信心,负责任地合并大量由 LLM 编写的代码?
答案是:一个与语言无关、包含一百万个断言的测试套件;对抗性代码审查;以及当确实出错时,修复生成代码的流程,而不是手动修补代码。
对抗性审查
对抗性审查会要求 Claude(在一个单独的上下文窗口中)穷尽式地找出这些变更会产生 bug 或无法工作的理由。
拆分上下文窗口
通常对人类来说,审查代码的人不是编写代码的人。写代码的人想要合并代码,这可能会让他们的行动产生偏向,在代码尚未准备好之前就发出去。
Claude 也是一样。写代码的 Claude 希望代码被接受。负责审查的 Claude 则希望在代码中找到问题。
每个实现者配 1 个,另有 2 个或更多对抗性审查者。审查者唯一的工作:找出 bug,以及代码无法工作的理由。实现者不做审查。审查者不做实现。
✻ claude code · 动态工作流对抗性审查合并前由对抗性审查捕获的众多 bug 中的 3 个
bug 1 of 3 · 异步关闭▶ play
✻claude实现者
它的上下文:原始 .zig 文件、移植计划、它自己的推理
✻claude对抗性审查者
它的上下文:只有 diff。被告知要假设代码是错的。
✻
src/runtime/api/bun/js_bun_spawn_bindings.rs · 编译通过
for stdio in [spawned_stdout, spawned_stderr] {
match stdio {
StdioResult::Buffer(mut pipe) => {
// pipe: Boxuv::Pipe — hand it to libuv to close
pipe.close(Subprocess::on_pipe_close)
}
StdioResult::Fd(fd) => fd.close(),
StdioResult::Unavailable => {}
}
}
✻
uv_close 是异步的:libuv 会一直持有原始句柄指针,直到下一个事件循环 tick,然后调用 on_pipe_close,释放这块分配。但 pipe 是一个 Box,会在这个 match 分支结束时 drop——于是 libuv 持有了已释放的内存,而 close 回调随后又会再次释放它。先 use-after-free,然后 double-free。
✻
Box::leak(pipe).close(Subprocess::on_pipe_close)
f0a454376c7 · win-review: js_bun_spawn_bindings.rs leak Boxuv::Pipe before async uv_close to avoid UAF/double-free in on_pipe_close
✻
src/runtime/node/node_fs.rs · 编译通过
// split f64 seconds into a timespec-style {sec, nsec}
let sec = t.trunc();
TimeLike {
sec: sec as i64,
nsec: ((t - sec) * 1e9) as i64,
}
✻
对于一个负的非整数时间——即 1970 年之前的文件 mtime——trunc 会向零取整:-1.5 会变成 {sec: -1, nsec: -500_000_000}。负的 nsec 是无效的 timespec。floor 会让 nsec 保持在 [0, 1e9) 内:{sec: -2, nsec: 500_000_000}。
✻
let sec = t.floor();
nsec: ((t - sec) * 1e9).round() as i64,
7cc88f00141 · crossplat review fixes: … node_fs win to_sys_time_like floor() so nsec∈[0,1e9) for negative t …
✻
src/css/values/color.rs · 编译通过
// each side of color-mix() may omit its percentage;
// a missing one defaults to the remainder of the other side
let p1 = first.percentage.unwrap_or(1.0 - second.percentage.unwrap());
✻
unwrap_or 会急切地求值它的参数——即使 first.percentage 是 Some,second.percentage.unwrap() 也会运行。所以对于 color-mix(in srgb, red 40%, blue) 这种只有第二个百分比被省略的情况,它会在参数表达式内部 panic,根本还没等 unwrap_or 有机会忽略它。unwrap_or_else 接收一个闭包,并保持惰性。
✻
let p1 = first
.percentage
.unwrap_or_else(|| 1.0 - second.percentage.unwrap());
90111846a14 · phase-b2: color.rs gated_full_impl FULLY DISSOLVED (verify: parse_color_mix unwrap_or eager panic, Default CurrentColor vs transparent)
对抗性审查者实际抓到的三个 bug——每个引用的 commit 都在主题行中带有其审查归因。三个都能编译;三个看起来都很合理。审查者是在自己上下文窗口里的第二个 Claude:它拿到 diff,除此之外什么都没有——没有实现者的任何推理——并被要求找出它错在哪里。代码是从引用的 commit 中压缩整理的;bug 相同,修复也相同。
这看起来是什么样?
如果你要做一件又大又昂贵的事,先降低风险可以节省时间和金钱。
准备工作
在编写任何代码之前,我花了大约 3 小时和 Claude 讨论如何把我们 Zig 代码库中的模式紧密映射到 Rust。Claude 把这次讨论序列化成了一份 PORTING.md 文档,后来还上了 Hacker News。
下一个问题:你如何给手动管理内存的代码添加 Rust 生命周期?
于是我大概这样提示 Claude:
我:让我们启动一个动态工作流,分析代码库中每个 struct 字段的正确生命周期。这个工作流应该读取每个文件里的每一个 struct 字段,并追踪控制流。首先,寻找那些生命周期在 Rust 中难以表达的 struct 字段,然后为该字段提出一个生命周期,再使用 2 个对抗性审查 agent 审查该生命周期,然后应用所有反馈,并序列化到一个 LIFETIMES.tsv 中,供其他 Claude 查看。
然后,把 PORTING.md 和 LIFETIMES.tsv 放在一起再进行一轮对抗性审查,以修复任何互相冲突的建议,并仔细复核所有内容。我也手动通读了一遍。
试运行
在让 Claude 把全部 1,448 个 .zig 文件翻译成 .rs 文件之前,我只先从 3 个文件开始。对于这 3 个文件中的每一个,1 个实现者编写新的 .rs 文件,2 个对抗性审查者检查该 .rs 文件是否匹配 .zig 文件的行为,并且是否遵循 PORTING.md 和 LIFETIMES.tsv。之后,1 个修复者应用所有建议。
错误的开始
我让 Claude 在全部 1,448 个 .zig 文件上循环执行这个工作流,大约 2 分钟后,一个 Claude 在提交之前运行了 git stash。另一个运行了 git stash pop。然后又运行了 git reset HEAD --hard。它们互相踩脚了!而且如果我把每个 Claude 都放进单独的 worktree,我会用完磁盘空间,因为 Bun 的 git 仓库太大了,而最终这些变更又需要一起编译、一起查看。
所以,我让 Claude 编辑工作流,指示 Claude 永远不要运行 git stash、git reset,或任何不是一次性提交某个特定文件的 git 命令。也不要运行 cargo。所有慢命令都不准运行。
然后,Claude 恢复了这些工作流。而且它确实在工作!只是太慢了,于是我把它拆成仅 4 个工作流分片,每个分片都有自己的 worktree(总共 4 个 worktree),每个分片运行 16 个 Claude 来提交并推送文件。
终于开始写代码
得益于所有这些并行化和前期准备工作,在峰值时 Claude 每分钟大约写出 1,300 行代码。每一行代码都由两个独立的对抗式审查员(同样是 Claude)审查,并在提交前经过一轮修复。可它还一点都不能运行。
11 天 × 24 小时 · PDT
6,502 次提交
1695 次提交/小时
12am6am12pm6pm5月4日5月4日,7am–8am PDT — 6 次提交,+89,278 行5月4日,8am–9am PDT — 2 次提交,+50,742 行5月4日,9am–10am PDT — 1 次提交,+28,149 行5月4日,11am–12pm PDT — 1 次提交,+39,752 行5月4日,12pm–1pm PDT — 3 次提交,+251,616 行5月4日,1pm–2pm PDT — 2 次提交,+161,724 行5月4日,3pm–4pm PDT — 3 次提交,+136,381 行5月4日,5pm–6pm PDT — 5 次提交,+895 行5月4日,6pm–7pm PDT — 5 次提交,+17,027 行5月4日,7pm–8pm PDT — 1 次提交,+106 行5月4日,9pm–10pm PDT — 13 次提交,+11,661 行5月4日,11pm–12am PDT — 6 次提交,+8,516 行5月5日5月5日,12am–1am PDT — 9 次提交,+1,381 行5月5日,1am–2am PDT — 7 次提交,+1,577 行5月5日,2am–3am PDT — 4 次提交,+2,035 行5月5日,3am–4am PDT — 4 次提交,+7,808 行5月5日,4am–5am PDT — 1 次提交,+2,796 行5月5日,5am–6am PDT — 2 次提交,+29,370 行5月5日,8am–9am PDT — 2 次提交,+7,076 行5月5日,9am–10am PDT — 2 次提交,+308 行5月5日,11am–12pm PDT — 2 次提交,+1,643 行5月5日,12pm–1pm PDT — 4 次提交,+1,452 行5月5日,1pm–2pm PDT — 1 次提交,+2,142 行5月5日,2pm–3pm PDT — 4 次提交,+7,787 行5月5日,3pm–4pm PDT — 2 次提交,+5,835 行5月5日,4pm–5pm PDT — 1 次提交,+3,417 行5月5日,5pm–6pm PDT — 4 次提交,+3,960 行5月5日,6pm–7pm PDT — 4 次提交,+9,179 行5月5日,7pm–8pm PDT — 4 次提交,+1,983 行5月5日,8pm–9pm PDT — 4 次提交,+18,902 行5月5日,9pm–10pm PDT — 43 次提交,+40,650 行5月5日,10pm–11pm PDT — 139 次提交,+64,842 行5月5日,11pm–12am PDT — 141 次提交,+34,814 行5月6日5月6日,12am–1am PDT — 60 次提交,+10,417 行5月6日,1am–2am PDT — 296 次提交,+38,530 行5月6日,2am–3am PDT — 306 次提交,+18,836 行5月6日,3am–4am PDT — 196 次提交,+10,245 行5月6日,4am–5am PDT — 86 次提交,+2,655 行5月6日,5am–6am PDT — 16 次提交,+289 行5月6日,8am–9am PDT — 5 次提交,+264 行5月6日,9am–10am PDT — 458 次提交,+16,409 行5月6日,10am–11am PDT — 695 次提交,+44,000 行5月6日,11am–12pm PDT — 102 次提交,+21,972 行5月6日,12pm–1pm PDT — 19 次提交,+2,891 行5月6日,1pm–2pm PDT — 3 次提交,+56 行5月6日,3pm–4pm PDT — 64 次提交,+3,606 行5月6日,4pm–5pm PDT — 264 次提交,+60,132 行5月6日,5pm–6pm PDT — 268 次提交,+40,953 行5月6日,6pm–7pm PDT — 281 次提交,+16,283 行5月6日,7pm–8pm PDT — 258 次提交,+26,654 行5月6日,8pm–9pm PDT — 327 次提交,+16,599 行5月6日,9pm–10pm PDT — 74 次提交,+8,331 行5月6日,10pm–11pm PDT — 17 次提交,+2,200 行5月6日,11pm–12am PDT — 11 次提交,+3,590 行5月7日5月7日,12am–1am PDT — 17 次提交,+6,577 行5月7日,1am–2am PDT — 22 次提交,+8,718 行5月7日,2am–3am PDT — 21 次提交,+11,392 行5月7日,3am–4am PDT — 53 次提交,+6,476 行5月7日,4am–5am PDT — 31 次提交,+2,356 行5月7日,5am–6am PDT — 9 次提交,+1,787 行5月7日,6am–7am PDT — 4 次提交,+580 行5月7日,7am–8am PDT — 5 次提交,+181 行5月7日,11am–12pm PDT — 3 次提交,+421 行5月7日,12pm–1pm PDT — 1 次提交,+13 行5月7日,1pm–2pm PDT — 5 次提交,+248 行5月7日,2pm–3pm PDT — 9 次提交,+2,131 行5月7日,3pm–4pm PDT — 51 次提交,+3,207 行5月7日,4pm–5pm PDT — 56 次提交,+2,647 行5月7日,5pm–6pm PDT — 159 次提交,+2,787 行5月7日,6pm–7pm PDT — 42 次提交,+1,590 行5月7日,7pm–8pm PDT — 46 次提交,+4,170 行5月7日,8pm–9pm PDT — 52 次提交,+2,113 行5月7日,9pm–10pm PDT — 27 次提交,+1,585 行5月7日,10pm–11pm PDT — 27 次提交,+2,231 行5月7日,11pm–12am PDT — 30 次提交,+4,987 行5月8日5月8日,12am–1am PDT — 27 次提交,+1,196 行5月8日,1am–2am PDT — 14 次提交,+904 行5月8日,2am–3am PDT — 8 次提交,+536 行5月8日,3am–4am PDT — 13 次提交,+253 行5月8日,4am–5am PDT — 3 次提交,+771 行5月8日,5am–6am PDT — 15 次提交,+1,545 行5月8日,6am–7am PDT — 12 次提交,+1,965 行5月8日,7am–8am PDT — 14 次提交,+1,866 行5月8日,8am–9am PDT — 55 次提交,+3,622 行5月8日,9am–10am PDT — 35 次提交,+4,778 行5月8日,10am–11am PDT — 1 次提交,+0 行5月8日,12pm–1pm PDT — 1 次提交,+116 行5月8日,1pm–2pm PDT — 2 次提交,+66 行5月8日,2pm–3pm PDT — 9 次提交,+1,071 行5月8日,3pm–4pm PDT — 26 次提交,+1,691 行5月8日,4pm–5pm PDT — 18 次提交,+2,751 行5月8日,5pm–6pm PDT — 2 次提交,+97 行5月8日,6pm–7pm PDT — 2 次提交,+135 行5月8日,7pm–8pm PDT — 11 次提交,+1,763 行5月8日,8pm–9pm PDT — 20 次提交,+5,272 行5月8日,9pm–10pm PDT — 12 次提交,+952 行5月8日,10pm–11pm PDT — 2 次提交,+334 行5月8日,11pm–12am PDT — 6 次提交,+2,033 行5月9日5月9日,12am–1am PDT — 9 次提交,+387 行5月9日,1am–2am PDT — 9 次提交,+723 行5月9日,2am–3am PDT — 8 次提交,+98 行5月9日,3am–4am PDT — 63 次提交,+2,538 行5月9日,4am–5am PDT — 11 次提交,+8,861 行5月9日,5am–6am PDT — 4 次提交,+42 行5月9日,6am–7am PDT — 3 次提交,+2,616 行5月9日,7am–8am PDT — 6 次提交,+6,993 行5月9日,8am–9am PDT — 1 次提交,+3,705 行5月9日,9am–10am PDT — 11 次提交,+199 行5月9日,11am–12pm PDT — 1 次提交,+23 行5月9日,12pm–1pm PDT — 4 次提交,+5,012 行5月9日,1pm–2pm PDT — 7 次提交,+2,080 行5月9日,2pm–3pm PDT — 6 次提交,+924 行5月9日,3pm–4pm PDT — 5 次提交,+248 行5月9日,4pm–5pm PDT — 17 次提交,+508 行5月9日,5pm–6pm PDT — 2 次提交,+135 行5月9日,6pm–7pm PDT — 4 次提交,+822 行5月9日,7pm–8pm PDT — 1 次提交,+7 行5月10日5月10日,12am–1am PDT — 4 次提交,+497 行5月10日,1am–2am PDT — 2 次提交,+35 行5月10日,2am–3am PDT — 1 次提交,+131 行5月10日,3am–4am PDT — 2 次提交,+322 行5月10日,4am–5am PDT — 1 次提交,+3 行5月10日,5am–6am PDT — 1 次提交,+26 行5月10日,6am–7am PDT — 2 次提交,+81 行5月10日,7am–8am PDT — 1 次提交,+5 行5月10日,8am–9am PDT — 4 次提交,+78 行5月10日,9am–10am PDT — 1 次提交,+1 行5月10日,10am–11am PDT — 2 次提交,+128 行5月10日,11am–12pm PDT — 1 次提交,+4 行5月10日,12pm–1pm PDT — 2 次提交,+413 行5月10日,1pm–2pm PDT — 1 次提交,+25 行5月10日,2pm–3pm PDT — 5 次提交,+327 行5月10日,3pm–4pm PDT — 6 次提交,+1,172 行5月10日,4pm–5pm PDT — 4 次提交,+752 行5月10日,5pm–6pm PDT — 3 次提交,+227 行5月10日,6pm–7pm PDT — 2 次提交,+242 行5月10日,7pm–8pm PDT — 1 次提交,+306 行5月10日,8pm–9pm PDT — 1 次提交,+54 行5月10日,9pm–10pm PDT — 2 次提交,+75 行5月10日,10pm–11pm PDT — 1 次提交,+134 行5月10日,11pm–12am PDT — 5 次提交,+103 行5月11日5月11日,12am–1am PDT — 2 次提交,+150 行5月11日,1am–2am PDT — 4 次提交,+398 行5月11日,2am–3am PDT — 2 次提交,+364 行5月11日,3am–4am PDT — 3 次提交,+44 行5月11日,4am–5am PDT — 7 次提交,+9,367 行5月11日,6am–7am PDT — 2 次提交,+43 行5月11日,7am–8am PDT — 2 次提交,+149 行5月11日,8am–9am PDT — 10 次提交,+2,171 行5月11日,9am–10am PDT — 16 次提交,+2,047 行5月11日,10am–11am PDT — 18 次提交,+3,356 行5月11日,11am–12pm PDT — 9 次提交,+861 行5月11日,12pm–1pm PDT — 3 次提交,+412 行5月11日,1pm–2pm PDT — 12 次提交,+2,978 行5月11日,2pm–3pm PDT — 157 次提交,+10,700 行5月11日,3pm–4pm PDT — 16 次提交,+1,346 行5月11日,4pm–5pm PDT — 3 次提交,+78 行5月11日,5pm–6pm PDT — 41 次提交,+2,568 行5月11日,6pm–7pm PDT — 55 次提交,+4,912 行5月11日,7pm–8pm PDT — 53 次提交,+3,475 行5月11日,8pm–9pm PDT — 32 次提交,+1,732 行5月11日,9pm–10pm PDT — 46 次提交,+4,506 行5月11日,10pm–11pm PDT — 45 次提交,+1,711 行5月11日,11pm–12am PDT — 52 次提交,+10,850 行5月12日5月12日,12am–1am PDT — 30 次提交,+3,760 行5月12日,1am–2am PDT — 24 次提交,+9,443 行5月12日,2am–3am PDT — 41 次提交,+1,635 行5月12日,3am–4am PDT — 39 次提交,+788 行5月12日,4am–5am PDT — 27 次提交,+651 行5月12日,5am–6am PDT — 23 次提交,+779 行5月12日,6am–7am PDT — 1 次提交,+137,576 行5月12日,7am–8am PDT — 2 次提交,+81 行5月12日,8am–9am PDT — 2 次提交,+75 行5月12日,9am–10am PDT — 2 次提交,+130 行5月12日,10am–11am PDT — 5 次提交,+160 行5月12日,11am–12pm PDT — 2 次提交,+20 行5月12日,12pm–1pm PDT — 1 次提交,+2 行5月12日,1pm–2pm PDT — 30 次提交,+2,677 行5月12日,2pm–3pm PDT — 41 次提交,+7,022 行5月12日,3pm–4pm PDT — 4 次提交,+200 行5月12日,4pm–5pm PDT — 27 次提交,+1,423 行5月12日,5pm–6pm PDT — 19 次提交,+1,055 行5月12日,6pm–7pm PDT — 2 次提交,+380 行5月12日,7pm–8pm PDT — 2 次提交,+84 行5月12日,9pm–10pm PDT — 7 次提交,+273 行5月12日,10pm–11pm PDT — 3 次提交,+230 行5月12日,11pm–12am PDT — 7 次提交,+319 行5月13日5月13日,12am–1am PDT — 2 次提交,+133 行5月13日,1am–2am PDT — 14 次提交,+2,177 行5月13日,2am–3am PDT — 12 次提交,+685 行5月13日,4am–5am PDT — 10 次提交,+657 行5月13日,5am–6am PDT — 1 次提交,+687 行5月13日,6am–7am PDT — 11 次提交,+380 行5月13日,7am–8am PDT — 12 次提交,+5,247 行5月13日,8am–9am PDT — 14 次提交,+1,051 行5月13日,9am–10am PDT — 7 次提交,+680 行5月13日,10am–11am PDT — 10 次提交,+412 行5月13日,11am–12pm PDT — 6 次提交,+314 行5月13日,12pm–1pm PDT — 10 次提交,+2,980 行5月13日,1pm–2pm PDT — 1 次提交,+0 行5月13日,2pm–3pm PDT — 3 次提交,+439 行5月13日,5pm–6pm PDT — 7 次提交,+114 行5月13日,6pm–7pm PDT — 4 次提交,+605 行5月13日,9pm–10pm PDT — 1 次提交,+13 行5月13日,10pm–11pm PDT — 1 次提交,+48 行5月13日,11pm–12am PDT — 1 次提交,+8 行5月14日5月14日,12am–1am PDT — 1 次提交,+150 行
移植分支上的每一次提交(不包括合并提交),按小时分桶。峰值小时:695 次提交。
注意到时间分布不一致了吗?我忘了提高运行它的 EC2 实例上的默认 IOPS。只要一个很慢的 grep 命令,就足以让磁盘读写冻结好几分钟。
把编译器错误当作工作队列
写完所有代码之后,我让 Claude 编写一个工作流来修复每一个编译器错误。我们逐个 crate 推进。
✻ claude code · 动态工作流
剩余 ≈16,000 个错误
▶ 回放阶段 DWed,5 月 6 日,太平洋夏令时 12:40 AM
errors.txt0 个修复提交
error: deref *mut EventLoop before field access
error: js_parser/ast/E.rs: port json_stringify for Number/BigInt/RegExp
error: NodeHTTPResponse.rs: wire JSNodeHTTPResponse cached accessors vi
error[E0034]: multiple applicable items in scope
error: test_command.rs: wire coverage façade to bun_sourcemap_jsc::code
error: bundler/ungate_support.rs: un-gate bun_css shim to real ::bun_cs
error: dns.rs: implement pending_cache_for/get_key/get_or_put_into_reso
error: css/css_parser.rs: port DefineShorthand contract, parse_bundler,
error: runtime/crypto/mod.rs: create_crypto_error delegates to boringss
error: bun_core/fmt.rs: implement format_ip reborrow (offset-based slic
error: event_loop/EventLoopTimer.rs: port Timespec::ns from bun.zig
分配给 · 64 个 Claude
worktree 1
→→
→→
→→
→→
worktree 2
→→
→→
→→
→→
worktree 3
→→
→→
→→
→→
worktree 4
→→
→→
→→
→→
1 个修复2 个审查1 个应用
→ 提交按 crate 落地
bun_runtime0
bun_bundler0
bun_sql0
bun_js_parser0
bun_css0
bun_http0
bun_interchange0
bun_sys0
bun_core0
bun_string0
bun_logger0
bun_uws_sys0
bun_alloc0
bun_collections0
bun_ptr0
bun_sourcemap0
bun_safety0
bun_glob0
bun_dotenv0
bun_router0
bun_uws0
bun_io0
bun_ini0
bun_lolhtml_sys0
bun_test_runner0
bun_cares_sys0
bun_url0
bun_picohttp0
bun_clap0
bun_boringssl0
bun_watcher0
bun_analytics0
bun_libarchive0
bun_paths0
bun_aio0
bun_options_types0
bun_zlib0
bun_crash_handler0
bun_js_printer0
bun_resolver0
bun_http_jsc0
bun_install0
这是阶段 D 的工作方式,按其 1,610 个真实提交回放(5 月 6 日,PDT):cargo check 将 ≈16,000 个错误写入一个文件,并按 crate 分组;工作流把它们分配给 64 个 Claude —— 4 个 worktree 上共 16 个循环,每个循环里一个 Claude 修复、两个审查、一个应用。每个小块都是一批真实提交:它会落到实际对应的 crate 上,只有这样计数器才会移动。错误行是真实的提交标题。
最棘手的一类错误是循环依赖。
我们的 Zig 代码库是一个编译单元(实际上就是一个 crate)。我想把新的 Rust 代码库拆成约 100 个 crate,这样 Rust 编译会更快,但这需要避免循环依赖,同时尽量减少相对于原始 Zig 实现的改动。我在开始 Rust 重写前立刻提交的这个 PR并不足以做到这一点。我没有推倒重来,而是又跑了一个工作流,用来分类存在循环依赖的代码应该放到哪里,并把这些都写下来——然后再跑另一个工作流来完成重构。
修复循环依赖暴露出了大约 16,000 个编译器错误。对 1 个人来说这是个巨大的数字,但对 64 个 Claude 同时工作来说并不离谱。
为了最大化并行度,这个工作流会遍历每个 crate。
- 对每个 crate,运行
cargo check,按文件对输出分组,并把错误保存到一个文件中 - 修复该 crate 内的所有编译器错误
- 2 个对抗式审查者审查该 crate 的改动
- 1 个修复者应用修复
为了防止 Claude 之间互相踩踏,cargo check 只在最开始运行一次,并且和其他运行一样,直到最后才用 git。
又一次错误的开局
Claude 把“让所有 crate 都能编译”理解成了“把有编译错误的函数都 stub 掉”。Claude 还开始添加可疑地冗长的解释性注释来记录变通方案,所以我给对抗式审查者加了这样一条拒绝规则:
如果你需要一段很长的注释来解释为什么这个 workaround 没问题,那代码就是错的——修代码。
改了一次提示词,几个小时后,这些情况就不再发生了。
冒烟测试
模型很爱说“冒烟测试”。
一旦 cargo check 通过,下一步就是让它能够编译并运行 bun --version。它出现了链接器错误。然后,它在启动时立刻 panic。
接下来的目标是让它能够运行 bun test <file>。一旦这个跑通,我们就可以开始跑测试了!又到了另一个工作流的时间,这次围绕 bun CLI 子命令循环:
- 将每个失败的 stacktrace 连同其子命令一起保存到文件中
- 对按子命令分组的每个失败 stacktrace,让 1 个 Claude 修复
- 2 个对抗式审查者
- 1 个修复者应用建议
让测试套件在本地通过
这个工作流围绕测试文件循环。
运行大约 100 个随机测试文件,并按代码库中的文件夹分片到 4 个 worktree 之一。对于每个失败的测试,将 stacktrace 和错误保存到文件中,由 1 个实现者提出修复方案,2 个对抗式审查者审查,然后 1 个修复者应用。
更多的错误开端
我们的测试套件里有大量内存泄漏测试,还有少数集成测试可能会运行超过一分钟——例如:一个运行 next dev 并检查热模块重载是否能 100 次捕捉到变更的测试。其中几个测试在 debug 构建中会超时。
我们还有一些压力测试,会耗尽机器上 TCP socket 的最大数量;有些测试会向磁盘读写数 GB 数据;还有些测试会生成约 1 万个进程。
这需要比“拜托别这样”更强的隔离,所以我们使用 systemd-run(cgroups)来限制内存和 CPU 使用,并隔离 pid 命名空间。即便如此,机器还是好几次耗尽磁盘空间并崩溃。
让测试套件在 CI 中通过
第一次 CI 运行两天后,失败列表从 972 个测试文件降到了 23 个。又过了一天半,Linux 完全变绿了——也是第一次,让人感觉这次 Rust 重写真的能成。
✻ claude code · 动态工作流BuildKite · 按平台统计的转绿竞赛Windows 最后完成 · 5 月 11 日,PDT 上午 6:23
6 / 6 个平台变绿
▶ 回放这场竞赛构建 #54202 · 周四,5 月 14 日,PDT 上午 12:23
5 月 8 日5 月 9 日5 月 10 日5 月 11 日5 月 12 日5 月 13 日5 月 14 日
macOS x64 · 2 个分片构建 #52897: 分片失败构建 #52932: 分片失败构建 #52934: 分片失败构建 #52938: 分片失败构建 #52944: 分片失败构建 #52946: 分片失败构建 #52949: 分片失败构建 #52975: 分片失败构建 #52998: 分片失败构建 #53007: 分片失败构建 #53015: 分片失败构建 #53026: 分片失败构建 #53027: 分片失败构建 #53035: 分片失败构建 #53041: 分片失败构建 #53047: 分片失败构建 #53056: 分片失败构建 #53077: 分片失败构建 #53090: 分片失败构建 #53095: 分片失败构建 #53106: 分片失败构建 #53109: 分片失败构建 #53123: 分片失败构建 #53127: 分片失败构建 #53130: 分片失败构建 #53131: 分片失败构建 #53133: 分片失败构建 #53134: 分片失败构建 #53143: 分片失败构建 #53149: 全部分片通过构建 #53159: 全部分片通过构建 #53164: 全部分片通过构建 #53167: 全部分片通过构建 #53172: 全部分片通过构建 #53176: 全部分片通过构建 #53194: 全部分片通过构建 #53208: 全部分片通过构建 #53213: 分片失败构建 #53214: 分片失败构建 #53216: 无失败(部分运行)构建 #53222: 全部分片通过构建 #53229: 无失败(部分运行)构建 #53241: 全部分片通过构建 #53265: 全部分片通过构建 #53271: 全部分片通过构建 #53304: 分片失败构建 #53327: 分片失败构建 #53340: 分片失败构建 #53401: 分片失败构建 #53431: 分片失败构建 #53491: 全部分片通过构建 #53503: 全部分片通过构建 #53748: 分片失败构建 #53753: 全部分片通过构建 #53787: 全部分片通过构建 #53811: 全部分片通过构建 #53933: 无失败(部分运行)构建 #53952: 全部分片通过构建 #53983: 分片失败构建 #53992: 分片失败构建 #53999: 分片失败构建 #54012: 全部分片通过构建 #54015: 全部分片通过构建 #54017: 全部分片通过构建 #54022: 分片失败构建 #54026: 分片失败构建 #54033: 全部分片通过构建 #54040: 全部分片通过构建 #54047: 分片失败构建 #54049: 分片失败构建 #54057: 分片失败构建 #54064: 全部分片通过构建 #54074: 全部分片通过构建 #54093: 全部分片通过构建 #54144: 无失败(部分运行)构建 #54161: 分片失败构建 #54186: 分片失败构建 #54189: 分片失败构建 #54196: 分片失败构建 #54202: 全部分片通过✓
Linux arm64 · 60 个分片构建 #52934: 分片失败构建 #52938: 分片失败构建 #52944: 分片失败构建 #52969: 分片失败构建 #52975: 分片失败构建 #52980: 分片失败构建 #52988: 分片失败构建 #52996: 分片失败构建 #52998: 分片失败构建 #53007: 分片失败构建 #53013: 分片失败构建 #53014: 分片失败构建 #53015: 分片失败构建 #53026: 分片失败构建 #53027: 分片失败构建 #53031: 无失败(部分运行)构建 #53032: 分片失败构建 #53035: 分片失败构建 #53041: 分片失败构建 #53047: 分片失败构建 #53056: 分片失败构建 #53059: 分片失败构建 #53077: 分片失败构建 #53083: 分片失败构建 #53086: 分片失败构建 #53090: 分片失败构建 #53095: 分片失败构建 #53106: 分片失败构建 #53109: 分片失败构建 #53123: 分片失败构建 #53127: 分片失败构建 #53130: 分片失败构建 #53131: 分片失败构建 #53133: 分片失败构建 #53134: 分片失败构建 #53135: 分片失败构建 #53143: 分片失败构建 #53149: 分片失败构建 #53159: 分片失败构建 #53164: 分片失败构建 #53167: 全部分片通过构建 #53172: 分片失败构建 #53176: 全部分片通过构建 #53188: 分片失败构建 #53194: 分片失败构建 #53208: 全部分片通过构建 #53212: 分片失败构建 #53213: 分片失败构建 #53214: 分片失败构建 #53216: 全部分片通过构建 #53222: 全部分片通过构建 #53229: 全部分片通过构建 #53236: 无失败(部分运行)构建 #53241: 全部分片通过构建 #53260: 全部分片通过构建 #53265: 全部分片通过构建 #53271: 全部分片通过构建 #53280: 无失败(部分运行)构建 #53298: 无失败(部分运行)构建 #53304: 分片失败构建 #53327: 全部分片通过构建 #53340: 全部分片通过构建 #53360: 无失败(部分运行)构建 #53419: 分片失败构建 #53431: 分片失败构建 #53458: 无失败(部分运行)构建 #53485: 分片失败构建 #53491: 分片失败构建 #53503: 分片失败构建 #53514: 无失败(部分运行)构建 #53570: 分片失败构建 #53583: 分片失败构建 #53599: 分片失败构建 #53748: 分片失败构建 #53753: 全部分片通过构建 #53762: 无失败(部分运行)构建 #53787: 无失败(部分运行)构建 #53811: 全部分片通过构建 #53852: 无失败(部分运行)构建 #53863: 分片失败构建 #53893: 分片失败构建 #53914: 全部分片通过构建 #53933: 全部分片通过构建 #53952: 全部分片通过构建 #53983: 分片失败构建 #53992: 分片失败构建 #53999: 分片失败构建 #54008: 无失败(部分运行)构建 #54012: 全部分片通过构建 #54015: 全部分片通过构建 #54017: 全部分片通过构建 #54022: 分片失败构建 #54026: 全部分片通过构建 #54030: 分片失败构建 #54033: 全部分片通过构建 #54040: 全部分片通过构建 #54047: 分片失败构建 #54049: 分片失败构建 #54055: 分片失败构建 #54057: 分片失败构建 #54064: 全部分片通过构建 #54074: 全部分片通过构建 #54083: 无失败(部分运行)构建 #54093: 全部分片通过构建 #54144: 全部分片通过构建 #54161: 分片失败构建 #54186: 分片失败构建 #54189: 分片失败构建 #54196: 分片失败构建 #54202: 全部分片通过✓
Linux x64 · 60 个分片构建 #52934: 分片失败构建 #52938: 分片失败构建 #52944: 分片失败构建 #52969: 分片失败构建 #52975: 分片失败构建 #52988: 分片失败构建 #52996: 分片失败构建 #52998: 分片失败构建 #53007: 分片失败构建 #53013: 分片失败构建 #53014: 分片失败构建 #53015: 分片失败构建 #53026: 分片失败构建 #53027: 分片失败构建 #53032: 分片失败构建 #53033: 分片失败构建 #53035: 分片失败构建 #53041: 分片失败构建 #53047: 分片失败构建 #53056: 分片失败构建 #53059: 无失败(部分运行)构建 #53077: 分片失败构建 #53083: 无失败(部分运行)构建 #53086: 分片失败构建 #53090: 分片失败构建 #53095: 分片失败构建 #53106: 分片失败构建 #53109: 分片失败构建 #53123: 分片失败构建 #53127: 分片失败构建 #53130: 分片失败构建 #53131: 分片失败构建 #53133: 分片失败构建 #53134: 分片失败构建 #53135: 分片失败构建 #53143: 分片失败构建 #53149: 分片失败构建 #53159: 分片失败构建 #53164: 分片失败构建 #53167: 全部分片通过构建 #53172: 全部分片通过构建 #53176: 全部分片通过构建 #53188: 分片失败构建 #53194: 全部分片通过构建 #53208: 全部分片通过构建 #53212: 分片失败构建 #53213: 分片失败构建 #53214: 分片失败构建 #53216: 全部分片通过构建 #53222: 全部分片通过构建 #53229: 全部分片通过构建 #53236: 无失败(部分运行)构建 #53241: 全部分片通过构建 #53260: 全部分片通过构建 #53265: 全部分片通过构建 #53271: 全部分片通过构建 #53280: 无失败(部分运行)构建 #53304: 分片失败构建 #53327: 全部分片通过构建 #53340: 全部分片通过构建 #53360: 无失败(部分运行)构建 #53419: 无失败(部分运行)构建 #53431: 分片失败构建 #53458: 无失败(部分运行)构建 #53485: 分片失败构建 #53491: 全部分片通过构建 #53503: 全部分片通过构建 #53514: 无失败(部分运行)构建 #53570: 分片失败构建 #53583: 分片失败构建 #53599: 分片失败构建 #53748: 分片失败构建 #53753: 全部分片通过构建 #53759: 无失败(部分运行)构建 #53781: 分片失败构建 #53787: 无失败(部分运行)构建 #53811: 全部分片通过构建 #53863: 分片失败构建 #53893: 分片失败构建 #53914: 全部分片通过构建 #53933: 分片失败构建 #53952: 全部分片通过构建 #53983: 分片失败构建 #53992: 分片失败构建 #53999: 分片失败构建 #54008: 无失败(部分运行)构建 #54012: 全部分片通过构建 #54015: 全部分片通过构建 #54017: 全部分片通过构建 #54022: 分片失败构建 #54026: 全部分片通过构建 #54030: 分片失败构建 #54033: 全部分片通过构建 #54040: 无失败(部分运行)构建 #54047: 分片失败构建 #54049: 分片失败构建 #54055: 分片失败构建 #54057: 分片失败构建 #54064: 全部分片通过构建 #54074: 全部分片通过构建 #54083: 无失败(部分运行)构建 #54093: 全部分片通过构建 #54144: 全部分片通过构建 #54161: 分片失败构建 #54186: 分片失败构建 #54189: 分片失败构建 #54196: 分片失败构建 #54202: 全部分片通过✓
macOS arm64 · 4 个分片构建 #52897: 分片失败构建 #52929: 分片失败构建 #52932: 分片失败构建 #52944: 分片失败构建 #52975: 分片失败构建 #52996: 分片失败构建 #52998: 分片失败构建 #53007: 分片失败构建 #53013: 分片失败构建 #53014: 分片失败构建 #53015: 分片失败构建 #53026: 分片失败构建 #53027: 分片失败构建 #53032: 分片失败构建 #53035: 分片失败构建 #53041: 分片失败构建 #53047: 分片失败构建 #53056: 分片失败构建 #53059: 分片失败构建 #53077: 分片失败构建 #53095: 分片失败构建 #53109: 分片失败构建 #53123: 分片失败构建 #53127: 分片失败构建 #53130: 分片失败构建 #53131: 分片失败构建 #53133: 分片失败构建 #53134: 分片失败构建 #53135: 分片失败构建 #53143: 分片失败构建 #53149: 分片失败构建 #53159: 分片失败构建 #53164: 分片失败构建 #53167: 分片失败构建 #53172: 分片失败构建 #53176: 分片失败构建 #53188: 分片失败构建 #53194: 分片失败构建 #53208: 分片失败构建 #53212: 分片失败构建 #53213: 分片失败构建 #53214: 分片失败构建 #53216: 分片失败构建 #53222: 分片失败构建 #53229: 无失败(部分运行)构建 #53236: 无失败(部分运行)构建 #53241: 全部分片通过构建 #53265: 全部分片通过构建 #53271: 无失败(部分运行)构建 #53280: 无失败(部分运行)构建 #53304: 分片失败构建 #53327: 无失败(部分运行)构建 #53340: 无失败(部分运行)构建 #53360: 无失败(部分运行)构建 #53368: 分片失败构建 #53379: 分片失败构建 #53383: 分片失败构建 #53401: 分片失败构建 #53431: 分片失败构建 #53458: 分片失败构建 #53491: 无失败(部分运行)构建 #53503: 分片失败构建 #53570: 分片失败构建 #53583: 分片失败构建 #53599: 分片失败构建 #53601: 分片失败构建 #53748: 分片失败构建 #53753: 全部分片通过构建 #53757: 无失败(部分运行)构建 #53759: 无失败(部分运行)构建 #53787: 无失败(部分运行)构建 #53811: 全部分片通过构建 #53952: 无失败(部分运行)构建 #53992: 分片失败构建 #53999: 分片失败构建 #54007: 无失败(部分运行)构建 #54012: 全部分片通过构建 #54015: 分片失败构建 #54017: 全部分片通过构建 #54022: 分片失败构建 #54026: 无失败(部分运行)构建 #54030: 分片失败构建 #54033: 全部分片通过构建 #54040: 分片失败构建 #54047: 分片失败构建 #54049: 分片失败构建 #54055: 分片失败构建 #54057: 分片失败构建 #54064: 分片失败构建 #54074: 分片失败构建 #54093: 分片失败构建 #54161: 分片失败构建 #54186: 分片失败构建 #54189: 分片失败构建 #54196: 分片失败构建 #54202: 全部分片通过✓
Windows x64 · 8 个分片构建 #53090: 分片失败构建 #53094: 分片失败构建 #53095: 分片失败构建 #53106: 分片失败构建 #53109: 分片失败构建 #53123: 分片失败构建 #53127: 分片失败构建 #53130: 分片失败构建 #53131: 分片失败构建 #53133: 分片失败构建 #53134: 分片失败构建 #53135: 分片失败构建 #53143: 分片失败构建 #53149: 分片失败构建 #53159: 分片失败构建 #53164: 分片失败构建 #53167: 分片失败构建 #53172: 分片失败构建 #53176: 分片失败构建 #53188: 分片失败构建 #53194: 分片失败构建 #53208: 分片失败构建 #53212: 分片失败构建 #53213: 分片失败构建 #53214: 分片失败构建 #53216: 分片失败构建 #53222: 分片失败构建 #53229: 分片失败构建 #53236: 分片失败构建 #53241: 分片失败构建 #53260: 分片失败构建 #53265: 分片失败构建 #53271: 分片失败构建 #53280: 分片失败构建 #53298: 分片失败构建 #53304: 分片失败构建 #53327: 全部分片通过构建 #53340: 全部分片通过构建 #53360: 无失败(部分运行)构建 #53419: 分片失败构建 #53431: 分片失败构建 #53458: 分片失败构建 #53470: 无失败(部分运行)构建 #53485: 分片失败构建 #53491: 无失败(部分运行)构建 #53503: 分片失败构建 #53514: 无失败(部分运行)构建 #53565: 无失败(部分运行)构建 #53570: 分片失败构建 #53599: 分片失败构建 #53745: 分片失败构建 #53748: 分片失败构建 #53753: 分片失败构建 #53757: 分片失败构建 #53759: 分片失败构建 #53762: 分片失败构建 #53769: 分片失败构建 #53781: 分片失败构建 #53787: 分片失败构建 #53808: 分片失败构建 #53811: 分片失败构建 #53852: 分片失败构建 #53863: 分片失败构建 #53883: 分片失败构建 #53893: 分片失败构建 #53914: 全部分片通过构建 #53933: 全部分片通过构建 #53952: 全部分片通过构建 #53973: 无失败(部分运行)构建 #53983: 分片失败构建 #53992: 分片失败构建 #53999: 分片失败构建 #54002: 无失败(部分运行)构建 #54004: 无失败(部分运行)构建 #54007: 分片失败构建 #54008: 无失败(部分运行)构建 #54012: 全部分片通过构建 #54015: 全部分片通过构建 #54017: 全部分片通过构建 #54022: 分片失败构建 #54026: 全部分片通过构建 #54030: 全部分片通过构建 #54033: 全部分片通过构建 #54040: 全部分片通过构建 #54047: 分片失败构建 #54049: 分片失败构建 #54055: 分片失败构建 #54057: 分片失败构建 #54064: 全部分片通过构建 #54074: 全部分片通过构建 #54083: 全部分片通过构建 #54093: 全部分片通过构建 #54144: 分片失败构建 #54161: 分片失败构建 #54186: 分片失败构建 #54189: 分片失败构建 #54196: 分片失败构建 #54202: 全部分片通过✓
Windows arm64 · 8 个分片构建 #53090: 分片失败构建 #53095: 分片失败构建 #53106: 分片失败构建 #53109: 分片失败构建 #53123: 分片失败构建 #53127: 分片失败构建 #53130: 分片失败构建 #53131: 分片失败构建 #53134: 分片失败构建 #53135: 分片失败构建 #53149: 分片失败构建 #53159: 分片失败构建 #53164: 分片失败构建 #53167: 分片失败构建 #53172: 分片失败构建 #53176: 分片失败构建 #53188: 分片失败构建 #53194: 分片失败构建 #53208: 分片失败构建 #53212: 分片失败构建 #53213: 分片失败构建 #53214: 分片失败构建 #53216: 分片失败构建 #53222: 分片失败构建 #53229: 分片失败构建 #53236: 无失败(部分运行)构建 #53241: 分片失败构建 #53260: 分片失败构建 #53265: 分片失败构建 #53271: 分片失败构建 #53304: 分片失败构建 #53327: 全部分片通过构建 #53340: 全部分片通过构建 #53360: 无失败(部分运行)构建 #53419: 无失败(部分运行)构建 #53431: 分片失败构建 #53458: 无失败(部分运行)构建 #53485: 分片失败构建 #53491: 无失败(部分运行)构建 #53503: 无失败(部分运行)构建 #53599: 分片失败构建 #53748: 分片失败构建 #53753: 分片失败构建 #53757: 分片失败构建 #53759: 分片失败构建 #53762: 分片失败构建 #53787: 分片失败构建 #53808: 分片失败构建 #53811: 分片失败构建 #53852: 分片失败构建 #53863: 分片失败构建 #53883: 分片失败构建 #53893: 分片失败构建 #53914: 无失败(部分运行)构建 #53933: 全部分片通过构建 #53952: 全部分片通过构建 #53983: 分片失败构建 #53992: 分片失败构建 #53999: 分片失败构建 #54007: 无失败(部分运行)构建 #54012: 全部分片通过构建 #54015: 全部分片通过构建 #54017: 全部分片通过构建 #54022: 分片失败构建 #54026: 全部分片通过构建 #54030: 无失败(部分运行)构建 #54033: 全部分片通过构建 #54040: 全部分片通过构建 #54047: 分片失败构建 #54049: 分片失败构建 #54055: 无失败(部分运行)构建 #54057: 分片失败构建 #54064: 全部分片通过构建 #54074: 全部分片通过构建 #54083: 无失败(部分运行)构建 #54093: 全部分片通过构建 #54144: 分片失败构建 #54161: 分片失败构建 #54186: 分片失败构建 #54189: 分片失败构建 #54196: 分片失败构建 #54202: 全部分片通过✓
✓ 全部 6 个平台变绿 · 构建 #54202 → 已合并
按平台展示每次 CI 构建的测试分片,覆盖了运行测试的 135 次构建(从 BuildKite 挖出了 420 次)。亮绿色:每个分片都通过。暗绿色:没有失败,但运行被提前截断(被取代)。红色:至少有一个分片失败。每条泳道都标注了其完整套件首次通过的时间——Linux 的 60 个分片比 Windows 早了将近一整天变绿。各个平台一直在红色状态中摇摆,直到最后几个失败的测试倒下;最终全绿的构建是 #54202。
在合并之前,剩下的时间就很直接了。一个工作流反复修复每个平台上的 CI 测试失败,直到不再有测试失败为止。还有几个工作流用于 Windows 相关的清理、代码去重、减少 unsafe 的使用,以及总体上清理一些代码。
合并 Rust 重写
一旦 Bun 的测试套件在所有平台的 CI 上 100% 通过(并且我手动确认这些测试确实在运行,而不是被跳过),我就在本地跑了一堆命令来测试各种东西——然后按下了合并按钮。
合并到 main 并不是一个带版本号的发布。到这个时候,我已经有足够信心继续推进并投入到这次重写里,但还没有足够信心把它发布出去。
统计数据
在峰值时,我们同时运行了 4 个这样的 workflow,每个都在独立的 worktree 中,每个 workflow 有 16 个 Claude。大约同时有 64 个 Claude 在跑。
git log · claude/phase-a-portpeak:一分钟内 58 次提交
0
次提交
+0
行写入,包括重写
▶ 在 30 秒内回放 11 天Mon, May 4, 7:05 AM PDT
第一批 100 文件草稿PR #30412 打开已合并
所有 6,502 次提交(不含 merge)均已回放。粉色条主要是新代码;青色条主要是删除。行数计数器会把过程中的每次重写都算进去——最终落地的 diff 是 +1,009,272。日志是真实的提交信息。
0 个测试被跳过或删除
11 天(5 月 3 日 → 5 月 14 日合并)· 6,778 次提交
| 平台 | expect() 调用 | 测试 | 文件 |
|---|---|---|---|
| Debian 13 x64 | 1,386,826 | 60,624 | 4,174 |
| macOS 14 arm64 | 1,259,953 | 58,850 | 4,175 |
| Windows 2019 x64 | 1,007,544 | 57,337 | 4,173 |
合并前,这消耗了 59 亿未缓存输入 token、6.9 亿输出 token,以及 720 亿缓存输入 token 读取——按 API 定价约为 165,000 美元。如果靠手工做,我认为需要 3 位完全了解代码库上下文的工程师大约一年时间,在这期间我们将无法改进 Node.js 兼容性、修 bug、修安全问题或实现新功能。我们绝不会那样做。现实中的替代方案就是不做这件事,然后永远继续修这篇文章开头提到的那些 bug。
这是当今可实现能力的最前沿。我使用的是预发布版本的 Claude Fable 5,一个 Mythos 级模型。Claude Code 的动态 workflow 让 64 个 Claude 连续跑了 11 天(否则我得自己写一套 harness 才能做到)。
工作仍在继续
自从合并 Rust 移植以来,我们已经完成了 11 轮来自 Claude Code Security 的安全审查,并处理了发现的问题。
我们还为 Bun 中的每个解析器增加了 24/7 的覆盖率引导模糊测试——JavaScript、TypeScript、JSX、CSS、JSON5、JSONC、TOML、YAML、Markdown、INI、Bun Shell 脚本、semver 范围、.patch 文件以及 CSS 颜色。fuzzer 会自动把它发现的 bug 发送给 Claude,让它提交一个复现并修复问题的 PR,然后由人工审查这些 PR。到目前为止,它已经执行了我们的解析器 1000 亿次,并带来了大约 15 个 PR。
在撰写本文时,Bun 的 Rust 代码中约有 4% 位于 unsafe 块内(约 13,000 个 unsafe 关键字,分布在约 27,000 行 / 总计约 780,000 行代码中),其中 78% 的块只有一行——一个来自 C++ 的指针,或者一次对 C 库的调用。我预计随着我们从忠实的 Zig 移植(Zig 里没有可 grep 的 unsafe 关键字)重构成惯用 Rust,这个数字会随时间下降,但我们会继续使用 JavaScriptCore 这样的 C 和 C++ 库,所以它的 unsafe 永远会比纯 Rust 项目更多。
移植错误
Rust 重写的重点是稳定性,但发布这样一个巨大变更却引入零回归,这是不可能的。
这次重写引入了 19 个已知回归,每一个都已经修复。
大多数回归来自那些在两种语言中语法相同、但语义不同的代码。
debug_assert! 内的副作用
这两段代码看起来相似,但行为不同。Zig 的 assert 是一个函数,所以它的参数会在每种 build 中运行。Rust 的 debug_assert! 是一个宏,所以在 release build 中,整个表达式都会被抹掉,包括 insert_stale 调用。
// Zig:
if (dev.framework.react_fast_refresh) |rfr| {
assert(try dev.client_graph.insertStale(rfr.import_source, false) == IncrementalGraph(.client).react_refresh_index);
}
// Rust:
if let Some(rfr) = &dev.framework.react_fast_refresh {
debug_assert!(dev.client_graph.insert_stale(&rfr.import_source, false)? == react_refresh_index);
}
insert_stale 会把一个文件添加到前端开发服务器的热重载图中。在 release build 中它停止运行了,于是在使用 React 的 HTML routes 项目里,当某个热重载文件被失效时,HMR 在某些情况下会坏掉:Cannot destructure property 'isLikelyComponentType' of 'k'。Debug build 则正常。#30678
奇数长度的切片
Bun 的 Zig 辅助函数 reinterpretSlice(u16, bytes)(早于内置 cast 支持 slice)使用 @divTrunc,并忽略末尾多出的一个奇数字节。而 bytemuck::cast_slice 遇到这种情况会 panic。对于一个 UTF-16 字节序标记后跟奇数个字节的输入,Blob.text() 不再返回字符串,而是让进程 panic。我们改回了忽略那个奇数字节:&buf[..buf.len() & !1]。#31188
边界检查
在 macOS 和 Linux 上,我们用 ReleaseFast 编译 Bun 的 Zig 代码,这会移除边界检查。Rust 的 release build 会保留边界检查。
Bun 的模块解析器会把长文件名 intern 到一个全局列表里,该列表会溢出到 overflow block。原始 Zig 代码把每个 block 的大小设为 count / 4,也就是 2048。移植版本留下了一个占位值:
/// ... so use a nonzero stand-in until Phase B threads the
/// per-instantiation value through.
pub const BSS_OVERFLOW_BLOCK_SIZE: usize = 64;
这把上限从 840 万个 intern 后的文件名降低到了 270,272 个,真实项目会触达这个上限,并且让我们从 Zig 移植过来的一个 ptrs[4095] off-by-one 问题变得可触发。Rust 会 panic,而不是写越界。如果我们使用 ReleaseSafe(我们只在 Windows 上这样做),Zig 在这种情况下也会 panic。#31503
comptime 格式字符串
Output.pretty 会把 <r> 和 <d> 颜色标记重写成 ANSI 转义序列。在 Zig 中,fmt 是 comptime,所以这些标记会在参数替换前就消失。Rust 函数没有 comptime 参数,所以 Output::pretty 只能看到最终完成的字符串,于是也会把参数里的标记一起重写。
// Zig:
pub inline fn pretty(comptime fmt: string, args: anytype) void;
Output.pretty("<r>{f}<r>", .{hyperlink});
// Rust:
pub fn pretty(payload: impl PrettyFmtInput);
Output::pretty(format_args!("<r>{}<r>", hyperlink));
bun update -i 会把包名打印为 OSC 8 超链接,并以 ESC \ 结尾。那个反斜杠正好位于尾部 <r> 的 < 之前,标记解析器把它吃掉了,于是 r 被当成文本打印出来。
它应该显示 oxfmt,而不是 oxfmtr
在 Rust 中它必须是一个宏:bun_core::pretty!("<r>{}<r>", hyperlink)。#30693
Bun 用 Rust 会更好
到目前为止,Bun v1.4.0 修复了 128 个能在 v1.3.14 中复现的 bug。这些问题涵盖从内存泄漏到崩溃,再到帮助文本颜色错误。
降低内存使用
Rust 有一个强大的语言级内存清理工具:Drop。当实现了 Drop 时,每次值离开作用域,drop 函数都会被自动调用。
impl Drop for Bytes {
fn drop(&mut self) {
if !self.pinned.is_empty() {
JSC__JSValue__unpinArrayBuffer(self.pinned);
}
}
}
在 Zig 中,可以使用 defer 在作用域末尾运行代码:
const bytes: ArrayBuffer = try .fromPinned(global, value);
defer bytes.unpin();
在 Zig 中,defer 需要添加到每一个可能需要清理的单独调用点。最终很容易忘记清理(内存泄漏),或者在很少触达的错误处理代码中运行两次清理代码(double-free)。在 Rust 中,当值不再可访问时,Drop 会自动运行——这是用“存在隐藏控制流”换取避免一个常见 footgun。
Drop 修复了 Bun 中几个与错误处理代码里的文件路径相关的内存泄漏。
我们修复了每一个可检测的内存泄漏
我们改进了 Bun 的 LeakSanitizer 集成,以追踪所有原生代码内存分配。
下面是一个例子:每次进程内 Bun.build() 调用都会泄漏数 MB 内存——解析后的源码文本和 AST 符号表会比它们所属的 build 活得更久。
// Bundle the same 60-module project 2,000 times in one process
for (let i = 0; i < 2_000; i++) {
await Bun.build({
entrypoints: ["./index.js"],
minify: true,
sourcemap: "external",
});
}
在 Bun v1.3.14 中,每次 build 都会永久泄漏大约 3 MB——像 dev server 这种每个请求都 bundle 的工具最终会耗尽内存。在 Bun v1.4.0 中,内存会趋于稳定:
| 构建次数 | Bun v1.3.14 | Bun v1.4.0 |
|---|---|---|
| 500 | 1,914 MB | 526 MB |
| 1,000 | 3,506 MB | 586 MB |
| 1,500 | 5,097 MB | 608 MB |
| 2,000 | 6,745 MB | 609 MB |
此前有一次在 Zig 中实现这一点的尝试没有被合并,因为缺少等价于 Drop 的机制,使人更难对合并它有信心。
更小的二进制体积
Rust 重写带来的初始改动让二进制体积在 Windows 上减少了 3.8 MB,在 macOS 上减少了 5.5 MB,在 Linux 上减少了 6.8 MB。这很大程度上是因为我们在 Zig 代码中过度使用了 comptime。
— Bun (@bunjavascript) 2026 年 5 月 18 日
在这次初步缩减之后,团队继续探索更多减小二进制体积的机会,包括使用诸如 Identical Code Folding(相同代码折叠)之类的链接器优化、从 ICU 中移除未使用的数据,以及按需用 zstd 字典惰性解压 libicu 的小部分内容。
结合 Rust 重写、ICU 变更以及相同代码折叠,Bun 在 Linux 和 Windows 上的二进制体积缩小了约 20%。
| 版本 | 平台 | 大小 |
|---|---|---|
| Bun v1.4.0 (canary) | Windows | 76 MB |
| Bun v1.3.14 | Windows | 94 MB |
| Bun v1.4.0 (canary) | Linux | 70 MB |
| Bun v1.3.14 | Linux | 88 MB |
减少栈空间使用
TOML 解析器,以及 Bun 中所有其他递归下降解析器(JSON、YAML、JavaScript、TypeScript 等)现在使用的栈空间更少了。
这在合并 Rust 重写之前导致了一些测试失败:
bun test v1.3.14-canary.1 (e99311e58)
.......
105 | });
106 |
107 | it("Bun.TOML.parse throws on deeply nested inline tables instead of crashing", () => {
108 | const depth = 25_000;
109 | const deepToml = "a = " + "{ b = ".repeat(depth) + "1" + " }".repeat(depth);
110 | expect(() => Bun.TOML.parse(deepToml)).toThrow(RangeError);
^
error: expect(received).toThrow(expected)
Expected constructor: RangeError
Received function did not throw
Received value: {
a: {
b: {
b: {
b: {
b: {
b: {
b: {
b: {
b: [Object ...],
},
},
},
},
},
},
},
},
}
at <anonymous> (/var/lib/buildkite-agent/build/test/js/bun/resolve/toml/toml.test.js:110:42)
✗ Bun.TOML.parse throws on deeply nested inline tables instead of crashing [2907.64ms]
Rust 的 LLVM IR 代码生成会在栈变量不再使用时,为它们发出 LLVM 的 llvm.lifetime.start 和 llvm.lifetime.end intrinsic,这让 LLVM 能够复用栈空间槽位。这使得带有嵌套作用域的大型函数可以显著减少栈空间使用。
此前,我们通过将特别大的函数重构成许多更小的函数,手动绕过了一个未解决的问题。
快 2% - 5%
Rust 支持 C/C++ 与 Rust 之间的跨语言链接时优化,这可以实现跨编程语言内联(这也太酷了!!)。
我们在 Linux x64(EC2,Xeon Platinum 8488C)上对 Bun v1.3.14 和 Bun v1.4.0 进行了基准测试。HTTP 吞吐量使用 oha 针对 hello-world 服务器测量,应用工作负载使用 hyperfine 测量。
HTTP 吞吐量(req/s,3 轮平均值)
| server | Bun v1.3.14 | Bun v1.4.0 | Δ |
|---|---|---|---|
| Bun.serve | 169.6k | 177.7k | +4.8% |
| node:http | 103.8k | 108.5k | +4.5% |
| Elysia | 158.9k | 163.3k | +2.8% |
| express | 64.5k | 66.6k | +3.2% |
| fastify | 91.5k | 95.9k | +4.8% |
应用 / CLI(hyperfine)
| workload | Bun v1.3.14 | Bun v1.4.0 | Δ |
|---|---|---|---|
| next build | 13.62 s | 13.03 s | +4.5% |
| vite build (tsc + vite) | 1.69 s | 1.65 s | +2.2% |
| tsc -b –force | 0.94 s | 0.89 s | +4.7% |
生产环境
Prisma 在 Bun 的 Rust 重写版本上发布了 Prisma Compute 的公开 beta。
“我们遇到过内存泄漏,以及一个在 VM 暂停并恢复后无法恢复的连接池。当 Rust 重写版本出现时,我们用相同的故障模式对它进行了测试。它完美地处理了这些问题。”——Alexey Orlenko
Claude Code v2.1.181(6 月 17 日发布)及之后的版本使用 Bun 的 Rust 移植版。在 Linux 上启动速度提升了 10%,但除此之外,几乎没人注意到。无聊就是好事。
发布
Bun v1.3.14 是最后一个用 Zig 编写的 Bun 版本。Bun v1.4.0 将是第一个用 Rust 编写的 Bun 版本。它现在已在 canary 中可用——请报告你发现的任何问题:
bun upgrade --canary
可维护性
对我和团队来说,我们新的 Rust 代码库感觉与旧的 Zig 代码库非常相似。例如,下面是一段原始 Zig 代码和新的 Rust 代码:
pub fn canMergeSymbols(
scope: *Scope,
existing: Symbol.Kind,
new: Symbol.Kind,
comptime is_typescript_enabled: bool,
) SymbolMergeResult {
if (existing == .unbound) {
return .replace_with_new;
}
if (comptime is_typescript_enabled) {
// In TypeScript, imports are allowed to silently collide with symbols within
// the module. Presumably this is because the imports may be type-only:
//
// import {Foo} from 'bar'
// class Foo {}
//
if (existing == .import) {
return .replace_with_new;
}
// ...
}
// ...
}
pub fn can_merge_symbol_kinds<const IS_TYPESCRIPT_ENABLED: bool>(
scope_kind: Kind,
existing: symbol::Kind,
new: symbol::Kind,
) -> SymbolMergeResult {
if existing == symbol::Kind::Unbound {
return SymbolMergeResult::ReplaceWithNew;
}
if IS_TYPESCRIPT_ENABLED {
// In TypeScript, imports are allowed to silently collide with symbols within
// the module. Presumably this is because the imports may be type-only:
//
// import {Foo} from 'bar'
// class Foo {}
//
if existing == symbol::Kind::Import {
return SymbolMergeResult::ReplaceWithNew;
}
// ...
}
// ...
}
任何理解原始 Zig 代码的人,也能理解机械翻译后的 Rust 代码。我审查最初的 Rust 重写 PR 时,检查的是那些对抗式代码审查 agent 是否正确捕捉到了 Zig 代码与 Rust 代码之间的差异,是否确保遵循了移植指南和生命周期指南,同时我自己也手动并排阅读了大量 Zig 与 Rust 代码。
下一步
Bun v1.4 让 Bun 更快、更小、使用更少内存,并为团队提供了极其强大的工具,用于今后系统性地提升稳定性:Rust 的 borrow checker、Miri(它会在 CI 中针对越来越多的代码运行)、LeakSanitizer,以及面向解析器的 24/7 覆盖率引导模糊测试。仍然还有更多需要重构的地方,但一切已经有了非常好的开端。
如果由一支对代码库拥有完整上下文的工程师团队来做,这次 Rust 重写本来会需要一年的工作。借助 1 名工程师使用 Fable 并密切监控 Claude Code,我们从开始到在所有平台上 100% 通过测试套件,只用了 11 天。
今天,一名工程师能做到的事情比一年前多得多。

