本文介绍 Bun 团队为什么决定将项目核心从 Zig 重写为 Rust:从长期困扰运行时的内存安全问题,到用 Claude Code、Fable 和自动化验证流程在 11 天内完成大规模迁移。原文地址:Rewriting Bun in Rust。本文由LobsterAI自动翻译和发布。

Jarred Sumner · 2026 年 7 月 8 日

原文:Rewriting Bun in Rust

披露: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 实现,例如 fsnettls 以及其他数十个模块

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 与真正发送之间分离 ArrayBuffer
  • Buffer#copyBuffer#fill 中的崩溃与越界读取:当 valueOf 回调在参数强制转换期间分离或调整底层 ArrayBuffer 大小时发生
  • UDPSocket.sendMany() 中的堆越界写入:socket 的连接状态在迭代中途通过用户 JS 回调发生改变时发生
  • crypto.scrypt 中的内存泄漏:当输出缓冲区分配失败时,callback 以及受保护的 password/salt buffer 从未释放
  • SSLWrapper.init 在错误路径上泄漏了 strdup 出来的 passphrase
  • tlsSocket.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 标记线程可能在来自 BroadcastChannelMessagePort 的并发访问期间,观察到 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

LanguageCleanup
Zigdefer, errdefer
C++~Destructor, &&Move
RustDrop

对于 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 testbun 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.mdLIFETIMES.tsv 放在一起再进行一轮对抗性审查,以修复任何互相冲突的建议,并仔细复核所有内容。我也手动通读了一遍。

试运行

在让 Claude 把全部 1,448 个 .zig 文件翻译成 .rs 文件之前,我只先从 3 个文件开始。对于这 3 个文件中的每一个,1 个实现者编写新的 .rs 文件,2 个对抗性审查者检查该 .rs 文件是否匹配 .zig 文件的行为,并且是否遵循 PORTING.mdLIFETIMES.tsv。之后,1 个修复者应用所有建议。

错误的开始

我让 Claude 在全部 1,448 个 .zig 文件上循环执行这个工作流,大约 2 分钟后,一个 Claude 在提交之前运行了 git stash。另一个运行了 git stash pop。然后又运行了 git reset HEAD --hard。它们互相踩脚了!而且如果我把每个 Claude 都放进单独的 worktree,我会用完磁盘空间,因为 Bun 的 git 仓库太大了,而最终这些变更又需要一起编译、一起查看。

所以,我让 Claude 编辑工作流,指示 Claude 永远不要运行 git stashgit 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 x641,386,82660,6244,174
macOS 14 arm641,259,95358,8504,175
Windows 2019 x641,007,54457,3374,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 中,fmtcomptime,所以这些标记会在参数替换前就消失。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.14Bun v1.4.0
5001,914 MB526 MB
1,0003,506 MB586 MB
1,5005,097 MB608 MB
2,0006,745 MB609 MB

此前有一次在 Zig 中实现这一点的尝试没有被合并,因为缺少等价于 Drop 的机制,使人更难对合并它有信心。

更小的二进制体积

Rust 重写带来的初始改动让二进制体积在 Windows 上减少了 3.8 MB,在 macOS 上减少了 5.5 MB,在 Linux 上减少了 6.8 MB。这很大程度上是因为我们在 Zig 代码中过度使用了 comptime

pic.twitter.com/RQiMNMNo8C

— Bun (@bunjavascript) 2026 年 5 月 18 日

在这次初步缩减之后,团队继续探索更多减小二进制体积的机会,包括使用诸如 Identical Code Folding(相同代码折叠)之类的链接器优化、从 ICU 中移除未使用的数据,以及按需用 zstd 字典惰性解压 libicu 的小部分内容。

结合 Rust 重写、ICU 变更以及相同代码折叠,Bun 在 Linux 和 Windows 上的二进制体积缩小了约 20%

版本平台大小
Bun v1.4.0 (canary)Windows76 MB
Bun v1.3.14Windows94 MB
Bun v1.4.0 (canary)Linux70 MB
Bun v1.3.14Linux88 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.startllvm.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 轮平均值)

serverBun v1.3.14Bun v1.4.0Δ
Bun.serve169.6k177.7k+4.8%
node:http103.8k108.5k+4.5%
Elysia158.9k163.3k+2.8%
express64.5k66.6k+3.2%
fastify91.5k95.9k+4.8%

应用 / CLI(hyperfine)

workloadBun v1.3.14Bun v1.4.0Δ
next build13.62 s13.03 s+4.5%
vite build (tsc + vite)1.69 s1.65 s+2.2%
tsc -b –force0.94 s0.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%,但除此之外,几乎没人注意到。无聊就是好事。

来自生产遥测的 Claude Code 启动时间(Linux p50):v2.1.179 为 517ms,v2.1.181,也就是首个使用 Rust Bun 的版本,为 464ms——快 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 天。

今天,一名工程师能做到的事情比一年前多得多。