Glean 拾遗
Daily /2026-07-11 / Rewriting Bun in Rust: 535K Lines, 11 Days, 64 AI Agents

Rewriting Bun in Rust: 535K Lines, 11 Days, 64 AI Agents

Source bun.com Glean’d 2026-07-11 01:36 Read 65 min
AI summary

Bun's creator Jarred Sumner recounts how he used Anthropic's Claude Fable 5 to rewrite Bun's 535,496 lines of Zig into Rust in 11 days. The motivation: Zig's manual memory management caused numerous use-after-free, double-free, and memory leaks when mixed with JavaScriptCore's GC. Instead of an incremental port, he orchestrated 64 Claude agents in parallel using dynamic workflows and adversarial review. 100% of Bun's test suite (over 600k assertions) passed on all 6 platforms. The rewrite fixed 128 bugs, reduced memory usage by up to 90%, shrank the binary by ~20%, and improved throughput by 2-5%. The article details the workflow, common porting mistakes (e.g., debug_assert! side effects, slice overruns, comptime format differences), and how Rust's Drop systematically prevented memory leaks. A first-hand account of using cutting-edge AI to accomplish a year-long team project in less than two weeks.

Original · 65 min
bun.com ↗
§ 1

Disclosure: Bun was acquired by Anthropic in December 2025. I and others on the Bun team work at Anthropic. I used a pre-release version of Claude Fable 5 for much of the Rust rewrite.

Bun started as a line-for-line port of esbuild's JavaScript & TypeScript transpiler from Go to Zig. I wrote my first line of Zig on April 16, 2021. I bet on Zig after seeing the single-page Zig Language Reference on Hacker News and getting really excited about the low-level control and care for performance.

From the start, Bun's scope was massive:

JavaScript, TypeScript, and CSS transpiler, minifier, and bundler

npm-compatible package manager

Jest-like test runner

Node.js & TypeScript-compatible module resolution

HTTP/1.1 & WebSocket client

Node.js API implementations like fs, net, tls, and dozens of other modules

The initial version of Bun was written by me in 1 year, in a cramped Oakland apartment, pre-LLM, in Zig. The default outcome for ambitiously-scoped projects like Bun is joining the graveyard of dead side projects on a GitHub profile page. Zig made Bun possible. I would never have been able to build this much in 1 year if it wasn't for Zig.

声明: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 的初始版本是我在 2021 年(LLM 出现之前)于奥克兰一个狭小的公寓里,用 Zig 语言在一年内完成的。像 Bun 这样野心勃勃的项目,通常的结局是成为 GitHub 个人主页上已废弃项目墓地中的一员。Zig 让 Bun 成为可能。如果没有 Zig,我绝不可能在一年内完成这么多工作。

§ 2

Nowadays, Bun's CLI gets over 22 million monthly downloads. Popular tools like Claude Code and OpenCode bet on Bun as their runtime. Vercel, Railway, DigitalOcean and more have 1st-party support for Bun.

Bun's scope has also been a challenge for stability. Here's a small sample of bugs we fixed in Bun v1.3.14:

heap-use-after-free crash in node:zlib when calling .reset() on a zlib, Brotli, or Zstd stream while an async .write() is still in progress on the threadpool

use-after-free crash in node:zlib when an onerror callback issued a re-entrant write() followed by close() on native handles

use-after-free crashes in node:http2 when re-entrant JS callbacks (e.g. session.request() inside a timeout listener, an options getter, or a write callback) triggered a hashmap rehash, invalidating internal stream pointers

use-after-free in UDPSocket.send() and sendMany() where user code in valueOf() or toString() callbacks could detach an ArrayBuffer between payload capture and the actual send

crash and out-of-bounds read in Buffer#copy and Buffer#fill when a valueOf callback detaches or resizes the underlying ArrayBuffer during argument coercion

heap out-of-bounds write in UDPSocket.sendMany() when the socket's connection state changed mid-iteration via user JS callbacks

memory leak in crypto.scrypt where the callback and protected password/salt buffers were never released when the output buffer allocation failed

SSLWrapper.init leaked the strdup'd passphrase on error paths

memory leak in tlsSocket.setSession() where each call leaked one SSL_SESSION (~6.5 KB per call) due to a missing SSL_SESSION_free after d2i_SSL_SESSION

memory leak where fs.watch() watchers were never garbage collected after .close(), caused by a reference count underflow that permanently pinned each watcher as a GC root

double-free crash in the CSS parser when background-clip had vendor prefixes and multi-layer backgrounds

DuplexUpgradeContext was never freed — a full leak per tls.connect({ socket: duplex })

race condition crash in MessageEvent where the GC marker thread could observe a torn variant in m_data during concurrent access from a BroadcastChannel or MessagePort

We could have kept fixing these kinds of bugs one-off in perpetuity, but we owe it to our users counting on us to do better than that, and systematically prevent these kinds of bugs from recurring.

如今,Bun 的 CLI 每月下载量超过 2200 万次。Claude Code 和 OpenCode 等流行工具已将 Bun 作为其运行时。Vercel、Railway、DigitalOcean 等公司也提供了对 Bun 的一流支持。

但 Bun 的庞大功能范围也给稳定性带来了挑战。以下是我们在 Bun v1.3.14 中修复的一小部分 bug:

在 node:zlib 中,当对 zlib、Brotli 或 Zstd 流调用 .reset() 而线程池中仍有异步 .write() 正在进行时,发生堆释放后使用崩溃。

node:zlib 中,当 onerror 回调对原生句柄发出重入的 write() 后跟 close() 时,发生释放后使用崩溃。

node:http2 中,当重入的 JS 回调(例如超时监听器中的 session.request()、选项 getter 或 write 回调)触发了哈希表重新哈希,使内部流指针失效时,发生释放后使用崩溃。

UDPSocket.send() 和 sendMany() 中的释放后使用,用户代码在 valueOf() 或 toString() 回调中可以在载荷捕获到实际发送之间分离 ArrayBuffer。

Buffer#copy 和 Buffer#fill 中的崩溃和越界读取,当 valueOf 回调在参数强制转换期间分离或调整底层 ArrayBuffer 的大小时。

UDPSocket.sendMany() 中的堆越界写入,当套接字的连接状态通过用户 JS 回调在迭代过程中发生变化时。

crypto.scrypt 中的内存泄漏,当输出缓冲区分配失败时,回调以及受保护的密码/盐缓冲区从未被释放。

SSLWrapper.init 在错误路径上泄漏了 strdup 后的密码。

tlsSocket.setSession() 中的内存泄漏,每次调用由于缺少 d2i_SSL_SESSION 后的 SSL_SESSION_free 而泄漏一个 SSL_SESSION(每次调用约 6.5 KB)。

fs.watch() 监视器在 .close() 后从未被垃圾回收,这是由于引用计数下溢导致每个监视器永久固定为 GC 根。

CSS 解析器中当 background-clip 带有厂商前缀和多层背景时发生双重释放崩溃。

DuplexUpgradeContext 从未被释放——每次 tls.connect({ socket: duplex }) 都完全泄漏。

MessageEvent 中的竞争条件崩溃,GC 标记线程在从 BroadcastChannel 或 MessagePort 并发访问时可能观察到 m_data 中的撕裂变体。

我们可以继续逐个修复这类 bug,但我们有责任做得更好,系统地防止这类 bug 再次发生。

§ 3

What we were already doing

We patched the Zig compiler to add Address Sanitizer support. We run our test suite with ASAN on every commit.

We ship Zig safety-checked ReleaseSafe builds on Windows

We fuzz Bun's runtime APIs 24/7 using Fuzzilli, the JavaScript engine fuzzer used by V8 & JavaScriptCore

We have a whole lot of end-to-end memory leak tests

This is more than many projects do.

Just be really smart and don't make mistakes?

Our bugfix list felt bad and I was tired of going to sleep worrying about crashes in Bun. I don't blame Zig for that - other users of Zig don't have the bugs we had, and mixing GC with manually-managed memory is an uncommon enough thing for software to need that no language really designs for it. We wouldn't have gotten this far if not for Zig, and I'll always be grateful. Until very recently, programming language choice was a one-way decision for a project like Bun.

JavaScript is a garbage-collected language and modern JavaScript engines like JavaScriptCore (and V8) have strict rules around exception handling and the garbage collector. Zig, like C, doesn't manage memory for you and this is a tradeoff that for many projects is a great reason to use Zig. Zig does not have constructors/destructors, and most cleanup is expected to be written out explicitly at each call site with defer.

For Bun, correctly handling the lifetimes of garbage-collected values and manually-managed values has been a major source of stability issues - most often small memory leaks and occasionally, crashes. Every memory allocation has to be meticulously reviewed. Where do these bytes get freed? How do we ensure it only gets freed once? Did we check for JavaScript exceptions properly? Is this garbage-collected pointer visible to the conservative stack scanner? Is this garbage collected memory or manually managed memory?

For stability issues, knowing as early as possible is best. Fuzzing happens after code is merged. CI happens when code is pushed. Runtime safety checks & address sanitizer happens when code is run (hopefully in development, before CI).

我们已有的措施

我们给 Zig 编译器打了补丁以添加 Address Sanitizer 支持。每次提交都会用 ASAN 运行测试套件。

我们在 Windows 上发布带 Zig 安全检查的 ReleaseSafe 构建。

我们使用 Fuzzilli(V8 和 JavaScriptCore 使用的 JavaScript 引擎模糊测试器)全天候模糊测试 Bun 的运行时 API。

我们有大量的端到端内存泄漏测试。

这已经比许多项目做得更多了。

难道就不能聪明点,不犯错误吗?

看到那么长的 bug 列表让我心情沉重,我不希望每晚都带着对 Bun 崩溃的担忧入睡。我不怪 Zig——其他 Zig 用户没有我们遇到的这些 bug,而且将 GC 与手动管理的内存混合在一起,是软件很少需要做的事情,没有语言会为此而设计。没有 Zig 我们走不到今天,我将永远心存感激。直到最近,对于像 Bun 这样的项目来说,编程语言的选择还是一条单行道。

JavaScript 是一种垃圾回收语言,现代 JavaScript 引擎(如 JavaScriptCore 和 V8)对异常处理和垃圾回收有严格的规定。Zig 像 C 一样,不会为你管理内存,这对许多项目来说是使用 Zig 的一个很好的理由。Zig 没有构造函数/析构函数,大部分清理工作期望在每个调用点显式地用 defer 写出。

对于 Bun 来说,正确管理 GC 值和手动管理内存值的生命周期一直是稳定性问题的主要来源——最常见的是小内存泄漏,偶尔是崩溃。每次内存分配都必须仔细审查:这些字节在哪里释放?如何确保只释放一次?是否正确检查了 JavaScript 异常?这个 GC 指针是否对保守栈扫描器可见?这是 GC 内存还是手动管理的内存?

对于稳定性问题,越早发现越好。模糊测试在代码合并后进行。CI 在代码推送时运行。运行时安全检查与地址清理器在代码运行时(希望是开发阶段,在 CI 之前)进行。

§ 4

One common way to reduce this class of issue is to ensure cleanup code is always run exactly once for code that needs it. Zig is designed to be a simple language with no hidden control flow, and so it prefers the explicit defer keyword to run code at the end of a scope over C++'s implicit ~Destructor or Rust's implicit Drop.

LanguageCleanupZigdefer, errdeferC++~Destructor, &&MoveRustDrop

For Zig code, when exactly should we be running the cleanup code? If we're passing the same *T to many different functions, how do we know when it's no longer accessible and can be cleaned up? How does it work when some functions need to continue to reference the memory after the function is called? Our current approach is a mix of:

arena lifetimes, where the scope of when it's accessible is clear (parser state doesn't escape the calling function and so AST nodes are a good choice there)

reference-counting

pay really close attention

Many projects opt to answer these kinds of questions through a style guide. TigerBeetle's TigerStyle is an example in Zig and Google's 31,000 word C++ style guide is another. The challenge with style guides is enforcement. How do you make sure the style guide is followed? Historically, code review was the answer with best-effort enforcement via linters & static analyzers.

Having a rigid style guide with clear ownership expectations explicitly spelled out in the type system was a real option for Bun. Since Zig has no operator overloading, we would likely end up with a lot of code looking something like this:

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();

// ... }

This is less ergonomic than the Zig we expect:

fn foo(a: *TCPSocket) !void { const b = try do_something_with_a(a); // ... }

减少此类问题的一种常见方法是确保需要清理的代码始终只执行一次。Zig 被设计为一种没有隐藏控制流的简单语言,因此它倾向于使用显式的 defer 关键字在作用域末尾运行代码,而不是 C++ 的隐式 ~Destructor 或 Rust 的隐式 Drop。

语言 清理机制 Zig defer, errdefer C++ ~Destructor, &&Move Rust Drop

对于 Zig 代码,究竟何时应该运行清理代码?如果我们将同一个 *T 传递给许多不同的函数,我们如何知道它不再可访问并可以被清理?当某些函数需要在函数调用后继续引用内存时,这又如何工作?我们目前的方法是混合使用:

arena 生命周期,其中可访问的范围是明确的(解析器状态不会逃逸出调用函数,因此 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); // ... }

§ 5

What about C/C++?

About 20% of Bun's code is written in C++ and Bun embeds several C/C++ libraries:

JavaScriptCore, the JavaScript engine that powers Safari

uWebSockets & usockets - our HTTP/WebSocket server, and event loop

lshpack & lsquic - HPACK and HTTP/3 libraries

BoringSSL, Google's OpenSSL fork

SQLite

C++ instead of Zig would be a reasonable choice for Bun. We would get constructors & destructors. We could delete lots of extern "C" wrapper code.

But, we would still be reliant on style guides enforced through code review, and even with ASAN, memory corruption and memory leaks would still happen.

那么 C/C++ 呢?

Bun 大约 20% 的代码是用 C++ 编写的,并且 Bun 嵌入了几个 C/C++ 库:

JavaScriptCore(驱动 Safari 的 JavaScript 引擎)

uWebSockets 和 usockets(我们的 HTTP/WebSocket 服务器和事件循环)

lshpack 和 lsquic(HPACK 和 HTTP/3 库)

BoringSSL(Google 的 OpenSSL 分支)

SQLite

用 C++ 替代 Zig 对 Bun 来说是一个合理的选择。我们将拥有构造函数和析构函数。我们可以删除大量 extern "C" 包装代码。

但是,我们仍然要依赖通过代码审查来执行的风格指南,而且即使有 ASAN,内存损坏和内存泄漏仍然会发生。

§ 6

Why Rust?

A large percentage of bugs from that list are use-after-free, double-free, and "forgot to free" in an error path. In safe Rust, these are compiler errors and RAII-like automatic cleanup with Drop. Compiler errors are a better feedback loop than a style guide.

Historically, rewrites are a terrible idea. Excluding comments, Bun is 535,496 lines of Zig. A rewrite in another language would take a small team of engineers a full year. It would mean freezing bugfixes, security fixes or feature development for that time. The least risky approach to getting something shippable would be a mechanical port from Zig to Rust, with the minimal number of behavioral changes, using the exact same test suite we already use for testing Bun.

Fortunately, Bun's own test suite is written in TypeScript which means it doesn't depend on the runtime's programming language.

A year of zero user-facing impact is not a realistic option we could consider. So, enforcement through code-style to fix stability issues was our best bet, and was our plan when we added Rust-inspired smart pointers to Bun's codebase.

But honestly, I didn't want to do it. Homegrown smart pointers offer worse ergonomics than Rust, with none of the guarantees.

What if, instead, I spend a week testing if Anthropic's new model can rewrite Bun in Rust?

At first, I didn't expect it to work. A few days in, a high % of the test suite started passing and I saw how much the new Rust code matched up with the original Zig codebase. My opinion went from "this is worth trying" to "I'm going to merge this".

为什么选择 Rust?

那个 bug 列表中的很大一部分是释放后使用、双重释放和错误路径中的“忘记释放”。在安全的 Rust 中,这些是编译器错误,并且有类似 RAII 的自动清理(Drop)。编译器错误比风格指南提供更好的反馈循环。

历史上,重写是一个糟糕的主意。排除注释,Bun 有 535,496 行 Zig 代码。用另一种语言重写将需要一个小的工程师团队整整一年。这意味着在那段时间内必须冻结 bug 修复、安全修复或功能开发。最稳妥的方法是进行一次从 Zig 到 Rust 的机械移植,尽可能少地改变行为,并使用我们已经用于测试 Bun 的完全相同的测试套件。

幸运的是,Bun 自己的测试套件是用 TypeScript 编写的,这意味着它不依赖于运行时的编程语言。

一年没有用户可见的影响不是一个我们可以考虑的可行选项。因此,通过代码风格来强制执行以解决稳定性问题是我们最好的选择,这也是我们向 Bun 代码库添加 Rust 风格的智能指针时的计划。

但老实说,我不想那样做。自制的智能指针比 Rust 的人体工程学更差,而且没有任何保证。

相反,如果我花一周时间测试 Anthropic 的新模型能否用 Rust 重写 Bun 呢?

起初,我并不指望它能成功。但几天后,测试套件的高比例开始通过,我看到了新的 Rust 代码与原始 Zig 代码库的匹配程度。我的想法从“值得一试”变成了“我要合并它”。

§ 7

Claude, rewrite Bun in Rust.

There are a lot of ways to do a terrible job of this. For example, prompting Claude "Rewrite Bun in Rust. Don't make any mistakes." and then praying it would work is not what I did.

Think about how a person would do this. The first big question is:

Incremental rewrite? Or, everything all at once?

In my experience porting esbuild's transpiler from Go to Zig for the initial version of Bun (without LLMs), everything all at once is better. An incremental rewrite adds temporary code that you hope gets deleted eventually, and would be painful in the short-medium term.

The second big question: how?

How do we keep Bun in Rust the same Bun as before, with the same architecture, performance, and feature-set while also getting the language features of Rust like the borrow checker? How do we ensure the team can still maintain it after the rewrite?

Do the rewrite that looks like we transpiled our Zig code to Rust. We can gradually refactor it to reduce unsafe usage and look more like idiomatic Rust after Bun v1.4 ships.

Those are the only two big questions. Everything else is tactics.

Claude,用 Rust 重写 Bun。

有很多方法会把这件事搞砸。例如,提示 Claude“用 Rust 重写 Bun。不要犯任何错误。”然后祈祷它能成功,这不是我的做法。

想想一个人会怎么做。第一个大问题是:

增量重写?还是一次性完成?

根据我在初始版本(没有 LLM)将 esbuild 的转译器从 Go 移植到 Zig 的经验,一次性完成更好。增量重写会增加临时代码,你希望这些代码最终被删除,短期内会很痛苦。

第二个大问题:如何做?

如何让用 Rust 编写的 Bun 与以前一样,具有相同的架构、性能和功能集,同时又能获得 Rust 的语言特性(如借用检查器)?如何确保团队在重写后仍然可以维护它?

进行一次看起来像我们将 Zig 代码转译到 Rust 的重写。我们可以在 Bun v1.4 发布后逐步重构,以减少 unsafe 的使用,并使其看起来更像地道的 Rust。

只有这两个大问题。其他一切都是策略。

§ 8

Loops that write & review code

A lot of day-to-day engineering work as software engineers can be over-simplified into loops.

// 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); }

A task has some context associated with it (a Jira ticket, a GitHub issue, etc). The result is the code you wrote to fix it. Code reviewer(s) review the changes to check for regressions & correctness. And then you address the feedback.

I rewrote Bun in Rust using about 50 dynamic workflows in Claude Code run continuously over the course of 11 days.

Each dynamic workflow was a loop like this - a workflow for:

Generate a porting guide mapping Zig patterns & types to Rust patterns & types

Mechanically port every .zig file to a .rs file, matching the PORTING.md and LIFETIMES.tsv

Fix every crate's compiler errors

Get subcommands like bun test or bun build to work

Get every test in Bun's entire test suite to pass

Several large refactors and cleanup passes

For most of those 11 days (and after), I monitored workflows - manually reading the outputs to check for issues and bugs, and prompting Claude to edit the loop to fix things.

How do you review a PR with +1 million lines added? How do you start to build the confidence needed to responsibly merge large quantities of LLM-authored code?

A language-independent test suite with a million assertions, adversarial code review and when something does go wrong, fixing the process that generates the code instead of hand-fixing the code.

Adversarial review

Adversarial review asks Claude (in a separate context window) to exhaustively come up with reasons why the changes create bugs or do not work.

Split context windows

Usually with humans, the person reviewing the code is not the person who authored the code. The person writing the code wants to merge the code, which can bias their actions to ship before it's ready.

Claude is the same way. The Claude that wrote the code wants the code to get accepted. The Claude that reviews wants to find issues in the code.

1 implementer, 2 or more adversarial reviewers per implementer. The reviewer's only job: find bugs & reasons why the code does not work. The implementer doesn't review. The reviewer doesn't implement.

✻ claude code · dynamic workflowadversarial review3 of the many bugs adversarial review caught before merge

bug 1 of 3 · the async close

✻claudeimplementer

its context: the .zig original, the port plan, its own reasoning

✻claudeadversarial reviewer

its context: only the diff. told to assume the code is wrong.

src/runtime/api/bun/js_bun_spawn_bindings.rs · compiles clean

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 is asynchronous: libuv keeps the raw handle pointer until the next loop tick, then calls on_pipe_close, which frees the allocation. But pipe is a Box that drops at the end of this match arm — libuv is left holding freed memory, and the close callback then frees it a second time. Use-after-free, then 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

Three bugs the adversarial reviewers actually caught — every cited commit carries its review attribution in the subject line. All three compiled; all three looked plausible. The reviewer is a second Claude in its own context window: it gets the diff and nothing else — none of the implementer's reasoning — and is told to find the way it's wrong. Code is condensed from the cited commits; same bugs, same fixes.

编写与审查代码的循环

软件工程师的日常工作可以简化为循环。

// 伪代码,非真实代码 let task; while ((task = todoList.pop())) { const result = task(); const feedback = await Promise.all([review(result), review(result)]); await apply(feedback, result); }

每个任务都有一些上下文(Jira 工单、GitHub issue 等)。结果是你用来修复它的代码。代码审查者审查更改以检查回归和正确性。然后你处理反馈。

我使用 Claude Code 中大约 50 个动态工作流,在 11 天内持续运行,用 Rust 重写了 Bun。

每个动态工作流都是一个这样的循环——用于:

生成一个将 Zig 模式和类型映射到 Rust 模式和类型的移植指南

机械地将每个 .zig 文件移植到 .rs 文件,匹配 PORTING.md 和 LIFETIMES.tsv

修复每个 crate 的编译器错误

让 bun test 或 bun build 等子命令工作

让 Bun 整个测试套件中的每个测试都通过

几次大型重构和清理

在这 11 天的大部分时间里(以及之后),我监控工作流——手动阅读输出以检查问题和 bug,并提示 Claude 编辑循环来修复问题。

如何审查一个添加了超过 100 万行的 PR?如何建立足够的信心,负责任地合并大量 LLM 生成的代码?

一个语言无关的测试套件,包含数百万个断言,对抗性代码审查,以及当出现问题时,修复生成代码的过程而不是手动修复代码。

对抗性审查

对抗性审查要求 Claude(在单独的上下文窗口中)详尽地提出更改可能导致 bug 或无法工作的原因。

分离上下文窗口

通常对于人类,审查代码的人不是编写代码的人。编写代码的人希望合并代码,这可能会使他们在准备好之前就急于发布。

Claude 也是如此。编写代码的 Claude 希望代码被接受。审查的 Claude 希望找到代码中的问题。

1 个实现者,每个实现者配 2 个或更多对抗性审查者。审查者的唯一工作:找到 bug 和代码不工作的原因。实现者不审查。审查者不实现。

✻ claude code · 动态工作流对抗性审查对抗性审查在合并前捕获的 3 个 bug 之一

bug 1 of 3 · 异步关闭

✻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 — 交给 libuv 关闭

pipe.close(Subprocess::on_pipe_close)

}

StdioResult::Fd(fd) => fd.close(),

StdioResult::Unavailable => {}

} }

uv_close 是异步的:libuv 会保留原始句柄指针直到下一个循环滴答,然后调用 on_pipe_close,释放分配的内存。但 pipe 是一个 Box,在这个 match 臂结束时会被 drop——libuv 保留了已释放的内存,然后关闭回调再次释放。释放后使用,然后双重释放。

Box::leak(pipe).close(Subprocess::on_pipe_close)

f0a454376c7 · win-review: js_bun_spawn_bindings.rs 在异步 uv_close 前泄漏 Boxuv::Pipe 以避免 on_pipe_close 中的 UAF/双重释放

对抗性审查者实际捕获的三个 bug——每个引用的 commit 的主题行中都带有审查归属。三个都编译通过,看起来都合理。审查者是第二个 Claude,在自己的上下文窗口中:只获得 diff,没有实现者的任何推理,并被告知找出错误之处。代码摘自引用的 commit;相同的 bug,相同的修复。

§ 9

What does this look like?

If you're about to do something big and expensive, it saves time and money to de-risk it first.

Prep work

Before writing any code, I spent about 3 hours talking to Claude about how to map patterns from our Zig codebase closely to Rust. Claude serialized this discussion into a PORTING.md document, which ended up on Hacker News.

The next question: how do you add Rust lifetimes to code that manually manages memory?

That's where I prompted Claude something like this:

Me: Let's kick off a dynamic workflow to analyze the proper lifetimes of every struct field in the codebase. This workflow should read every struct field within every single file and trace the control flow. First, look for struct fields with complex lifetimes to express in Rust, then propose a lifetime for that field, then use 2 adversarial review agents to review that lifetime, then apply any feedback and serialize into a LIFETIMES.tsv for other claudes to look at.

Then a round of adversarial reviews on the PORTING.md and the LIFETIMES.tsv together to fix any conflicting suggestions and double check everything. I also manually read over it.

Trial run

Before asking Claude to translate all 1,448 .zig files to .rs files, I started with just 3. For each of the 3 files, 1 implementer wrote the new .rs file, 2 adversarial reviewers checked the .rs file matched the behavior of the .zig file and that it followed the PORTING.md & LIFETIMES.tsv. After that, 1 fixer applied any suggestions.

False starts

I asked Claude to loop the workflow on all 1,448 .zig files, and about 2 minutes in, one Claude ran git stash before committing. Another ran git stash pop. And then git reset HEAD --hard. They were stepping on each other! And if I put each Claude into a separate worktree, I would run out of disk space because Bun's git repository is too big and eventually the changes will need to be compiled and seen together.

So, I asked Claude to edit the workflow to instruct Claude to never run git stash or git reset or any git command that doesn't commit a specific file at once. No cargo either. No slow commands at all.

Then, Claude resumed the workflows. And it was working! Too slowly, so I split it into just 4 workflow shards each with their own worktree (4 worktrees total), each running 16 claudes committing and pushing files.

这看起来像什么?

如果你要做一件庞大而昂贵的事情,首先降低风险可以节省时间和金钱。

准备工作

在编写任何代码之前,我花了大约 3 个小时与 Claude 讨论如何将我们 Zig 代码库中的模式紧密地映射到 Rust。Claude 将这次讨论序列化到一个 PORTING.md 文档中,该文档后来发布到了 Hacker News 上。

下一个问题:如何将 Rust 的生命周期添加到手动管理内存的代码中?

于是,我提示 Claude 大致如下:

我:让我们启动一个动态工作流来分析代码库中每个结构体字段的合适生命周期。这个工作流应该读取每个文件中的每个结构体字段并跟踪控制流。首先,寻找在 Rust 中具有复杂生命周期的结构体字段,然后为该字段提出一个生命周期,然后使用 2 个对抗性审查代理审查该生命周期,然后应用任何反馈并序列化为 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,负责提交和推送文件。

§ 10

Finally writing the code

Thanks to all the parallelization & this prep work, at peak Claude wrote about 1,300 lines of code per minute. Every line of code was reviewed by two separate adversarial reviewers (also Claude) and went through a round of fixes before committing. Absolutely none of it worked yet.

11 days × 24 hours · PDT

6,502 commits

1695 commits/hour

12am6am12pm6pmMay 4May 4, 7am–8am PDT — 6 commits, +89,278 linesMay 4, 8am–9am PDT — 2 commits, +50,742 linesMay 4, 9am–10am PDT — 1 commit, +28,149 linesMay 4, 11am–12pm PDT — 1 commit, +39,752 linesMay 4, 12pm–1pm PDT — 3 commits, +251,616 linesMay 4, 1pm–2pm PDT — 2 commits, +161,724 linesMay 4, 3pm–4pm PDT — 3 commits, +136,381 linesMay 4, 5pm–6pm PDT — 5 commits, +895 linesMay 4, 6pm–7pm PDT — 5 commits, +17,027 linesMay 4, 7pm–8pm PDT — 1 commit, +106 linesMay 4, 9pm–10pm PDT — 13 commits, +11,661 linesMay 4, 11pm–12am PDT — 6 commits, +8,516 linesMay 5May 5, 12am–1am PDT — 9 commits, +1,381 linesMay 5, 1am–2am PDT — 7 commits, +1,577 linesMay 5, 2am–3am PDT — 4 commits, +2,035 linesMay 5, 3am–4am PDT — 4 commits, +7,808 linesMay 5, 4am–5am PDT — 1 commit, +2,796 linesMay 5, 5am–6am PDT — 2 commits, +29,370 linesMay 5, 8am–9am PDT — 2 commits, +7,076 linesMay 5, 9am–10am PDT — 2 commits, +308 linesMay 5, 11am–12pm PDT — 2 commits, +1,643 linesMay 5, 12pm–1pm PDT — 4 commits, +1,452 linesMay 5, 1pm–2pm PDT — 1 commit, +2,142 linesMay 5, 2pm–3pm PDT — 4 commits, +7,787 linesMay 5, 3pm–4pm PDT — 2 commits, +5,835 linesMay 5, 4pm–5pm PDT — 1 commit, +3,417 linesMay 5, 5pm–6pm PDT — 4 commits, +3,960 linesMay 5, 6pm–7pm PDT — 4 commits, +9,179 linesMay 5, 7pm–8pm PDT — 4 commits, +1,983 linesMay 5, 8pm–9pm PDT — 4 commits, +18,902 linesMay 5, 9pm–10pm PDT — 43 commits, +40,650 linesMay 5, 10pm–11pm PDT — 139 commits, +64,842 linesMay 5, 11pm–12am PDT — 141 commits, +34,814 linesMay 6May 6, 12am–1am PDT — 60 commits, +10,417 linesMay 6, 1am–2am PDT — 296 commits, +38,530 linesMay 6, 2am–3am PDT — 306 commits, +18,836 linesMay 6, 3am–4am PDT — 196 commits, +10,245 linesMay 6, 4am–5am PDT — 86 commits, +2,655 linesMay 6, 5am–6am PDT — 16 commits, +289 linesMay 6, 8am–9am PDT — 5 commits, +264 linesMay 6, 9am–10am PDT — 458 commits, +16,409 linesMay 6, 10am–11am PDT — 695 commits, +44,000 linesMay 6, 11am–12pm PDT — 102 commits, +21,972 linesMay 6, 12pm–1pm PDT — 19 commits, +2,891 linesMay 6, 1pm–2pm PDT — 3 commits, +56 linesMay 6, 3pm–4pm PDT — 64 commits, +3,606 linesMay 6, 4pm–5pm PDT — 264 commits, +60,132 linesMay 6, 5pm–6pm PDT — 268 commits, +40,953 linesMay 6, 6pm–7pm PDT — 281 commits, +16,283 linesMay 6, 7pm–8pm PDT — 258 commits, +26,654 linesMay 6, 8pm–9pm PDT — 327 commits, +16,599 linesMay 6, 9pm–10pm PDT — 74 commits, +8,331 linesMay 6, 10pm–11pm PDT — 17 commits, +2,200 linesMay 6, 11pm–12am PDT — 11 commits, +3,590 linesMay 7May 7, 12am–1am PDT — 17 commits, +6,577 linesMay 7, 1am–2am PDT — 22 commits, +8,718 linesMay 7, 2am–3am PDT — 21 commits, +11,392 linesMay 7, 3am–4am PDT — 53 commits, +6,476 linesMay 7, 4am–5am PDT — 31 commits, +2,356 linesMay 7, 5am–6am PDT — 9 commits, +1,787 linesMay 7, 6am–7am PDT — 4 commits, +580 linesMay 7, 7am–8am PDT — 5 commits, +181 linesMay 7, 11am–12pm PDT — 3 commits, +421 linesMay 7, 12pm–1pm PDT — 1 commit, +13 linesMay 7, 1pm–2pm PDT — 5 commits, +248 linesMay 7, 2pm–3pm PDT — 9 commits, +2,131 linesMay 7, 3pm–4pm PDT — 51 commits, +3,207 linesMay 7, 4pm–5pm PDT — 56 commits, +2,647 linesMay 7, 5pm–6pm PDT — 159 commits, +2,787 linesMay 7, 6pm–7pm PDT — 42 commits, +1,590 linesMay 7, 7pm–8pm PDT — 46 commits, +4,170 linesMay 7, 8pm–9pm PDT — 52 commits, +2,113 linesMay 7, 9pm–10pm PDT — 27 commits, +1,585 linesMay 7, 10pm–11pm PDT — 27 commits, +2,231 linesMay 7, 11pm–12am PDT — 30 commits, +4,987 linesMay 8May 8, 12am–1am PDT — 27 commits, +1,196 linesMay 8, 1am–2am PDT — 14 commits, +904 linesMay 8, 2am–3am PDT — 8 commits, +536 linesMay 8, 3am–4am PDT — 13 commits, +253 linesMay 8, 4am–5am PDT — 3 commits, +771 linesMay 8, 5am–6am PDT — 15 commits, +1,545 linesMay 8, 6am–7am PDT — 12 commits, +1,965 linesMay 8, 7am–8am PDT — 14 commits, +1,866 linesMay 8, 8am–9am PDT — 55 commits, +3,622 linesMay 8, 9am–10am PDT — 35 commits, +4,778 linesMay 8, 10am–11am PDT — 1 commit, +0 linesMay 8, 12pm–1pm PDT — 1 commit, +116 linesMay 8, 1pm–2pm PDT — 2 commits, +66 linesMay 8, 2pm–3pm PDT — 9 commits, +1,071 linesMay 8, 3pm–4pm PDT — 26 commits, +1,691 linesMay 8, 4pm–5pm PDT — 18 commits, +2,751 linesMay 8, 5pm–6pm PDT — 2 commits, +97 linesMay 8, 6pm–7pm PDT — 2 commits, +135 linesMay 8, 7pm–8pm PDT — 11 commits, +1,763 linesMay 8, 8pm–9pm PDT — 20 commits, +5,272 linesMay 8, 9pm–10pm PDT — 12 commits, +952 linesMay 8, 10pm–11pm PDT — 2 commits, +334 linesMay 8, 11pm–12am PDT — 6 commits, +2,033 linesMay 9May 9, 12am–1am PDT — 9 commits, +387 linesMay 9, 1am–2am PDT — 9 commits, +723 linesMay 9, 2am–3am PDT — 8 commits, +98 linesMay 9, 3am–4am PDT — 63 commits, +2,538 linesMay 9, 4am–5am PDT — 11 commits, +8,861 linesMay 9, 5am–6am PDT — 4 commits, +42 linesMay 9, 6am–7am PDT — 3 commits, +2,616 linesMay 9, 7am–8am PDT — 6 commits, +6,993 linesMay 9, 8am–9am PDT — 1 commit, +3,705 linesMay 9, 9am–10am PDT — 11 commits, +199 linesMay 9, 11am–12pm PDT — 1 commit, +23 linesMay 9, 12pm–1pm PDT — 4 commits, +5,012 linesMay 9, 1pm–2pm PDT — 7 commits, +2,080 linesMay 9, 2pm–3pm PDT — 6 commits, +924 linesMay 9, 3pm–4pm PDT — 5 commits, +248 linesMay 9, 4pm–5pm PDT — 17 commits, +508 linesMay 9, 5pm–6pm PDT — 2 commits, +135 linesMay 9, 6pm–7pm PDT — 4 commits, +822 linesMay 9, 7pm–8pm PDT — 1 commit, +7 linesMay 10May 10, 12am–1am PDT — 4 commits, +497 linesMay 10, 1am–2am PDT — 2 commits, +35 linesMay 10, 2am–3am PDT — 1 commit, +131 linesMay 10, 3am–4am PDT — 2 commits, +322 linesMay 10, 4am–5am PDT — 1 commit, +3 linesMay 10, 5am–6am PDT — 1 commit, +26 linesMay 10, 6am–7am PDT — 2 commits, +81 linesMay 10, 7am–8am PDT — 1 commit, +5 linesMay 10, 8am–9am PDT — 4 commits, +78 linesMay 10, 9am–10am PDT — 1 commit, +1 linesMay 10, 10am–11am PDT — 2 commits, +128 linesMay 10, 11am–12pm PDT — 1 commit, +4 linesMay 10, 12pm–1pm PDT — 2 commits, +413 linesMay 10, 1pm–2pm PDT — 1 commit, +25 linesMay 10, 2pm–3pm PDT — 5 commits, +327 linesMay 10, 3pm–4pm PDT — 6 commits, +1,172 linesMay 10, 4pm–5pm PDT — 4 commits, +752 linesMay 10, 5pm–6pm PDT — 3 commits, +227 linesMay 10, 6pm–7pm PDT — 2 commits, +242 linesMay 10, 7pm–8pm PDT — 1 commit, +306 linesMay 10, 8pm–9pm PDT — 1 commit, +54 linesMay 10, 9pm–10pm PDT — 2 commits, +75 linesMay 10, 10pm–11pm PDT — 1 commit, +134 linesMay 10, 11pm–12am PDT — 5 commits, +103 linesMay 11May 11, 12am–1am PDT — 2 commits, +150 linesMay 11, 1am–2am PDT — 4 commits, +398 linesMay 11, 2am–3am PDT — 2 commits, +364 linesMay 11, 3am–4am PDT — 3 commits, +44 linesMay 11, 4am–5am PDT — 7 commits, +9,367 linesMay 11, 6am–7am PDT — 2 commits, +43 linesMay 11, 7am–8am PDT — 2 commits, +149 linesMay 11, 8am–9am PDT — 10 commits, +2,171 linesMay 11, 9am–10am PDT — 16 commits, +2,047 linesMay 11, 10am–11am PDT — 18 commits, +3,356 linesMay 11, 11am–12pm PDT — 9 commits, +861 linesMay 11, 12pm–1pm PDT — 3 commits, +412 linesMay 11, 1pm–2pm PDT — 12 commits, +2,978 linesMay 11, 2pm–3pm PDT — 157 commits, +10,700 linesMay 11, 3pm–4pm PDT — 16 commits, +1,346 linesMay 11, 4pm–5pm PDT — 3 commits, +78 linesMay 11, 5pm–6pm PDT — 41 commits, +2,568 linesMay 11, 6pm–7pm PDT — 55 commits, +4,912 linesMay 11, 7pm–8pm PDT — 53 commits, +3,475 linesMay 11, 8pm–9pm PDT — 32 commits, +1,732 linesMay 11, 9pm–10pm PDT — 46 commits, +4,506 linesMay 11, 10pm–11pm PDT — 45 commits, +1,711 linesMay 11, 11pm–12am PDT — 52 commits, +10,850 linesMay 12May 12, 12am–1am PDT — 30 commits, +3,760 linesMay 12, 1am–2am PDT — 24 commits, +9,443 linesMay 12, 2am–3am PDT — 41 commits, +1,635 linesMay 12, 3am–4am PDT — 39 commits, +788 linesMay 12, 4am–5am PDT — 27 commits, +651 linesMay 12, 5am–6am PDT — 23 commits, +779 linesMay 12, 6am–7am PDT — 1 commit, +137,576 linesMay 12, 7am–8am PDT — 2 commits, +81 linesMay 12, 8am–9am PDT — 2 commits, +75 linesMay 12, 9am–10am PDT — 2 commits, +130 linesMay 12, 10am–11am PDT — 5 commits, +160 linesMay 12, 11am–12pm PDT — 2 commits, +20 linesMay 12, 12pm–1pm PDT — 1 commit, +2 linesMay 12, 1pm–2pm PDT — 30 commits, +2,677 linesMay 12, 2pm–3pm PDT — 41 commits, +7,022 linesMay 12, 3pm–4pm PDT — 4 commits, +200 linesMay 12, 4pm–5pm PDT — 27 commits, +1,423 linesMay 12, 5pm–6pm PDT — 19 commits, +1,055 linesMay 12, 6pm–7pm PDT — 2 commits, +380 linesMay 12, 7pm–8pm PDT — 2 commits, +84 linesMay 12, 9pm–10pm PDT — 7 commits, +273 linesMay 12, 10pm–11pm PDT — 3 commits, +230 linesMay 12, 11pm–12am PDT — 7 commits, +319 linesMay 13May 13, 12am–1am PDT — 2 commits, +133 linesMay 13, 1am–2am PDT — 14 commits, +2,177 linesMay 13, 2am–3am PDT — 12 commits, +685 linesMay 13, 4am–5am PDT — 10 commits, +657 linesMay 13, 5am–6am PDT — 1 commit, +687 linesMay 13, 6am–7am PDT — 11 commits, +380 linesMay 13, 7am–8am PDT — 12 commits, +5,247 linesMay 13, 8am–9am PDT — 14 commits, +1,051 linesMay 13, 9am–10am PDT — 7 commits, +680 linesMay 13, 10am–11am PDT — 10 commits, +412 linesMay 13, 11am–12pm PDT — 6 commits, +314 linesMay 13, 12pm–1pm PDT — 10 commits, +2,980 linesMay 13, 1pm–2pm PDT — 1 commit, +0 linesMay 13, 2pm–3pm PDT — 3 commits, +439 linesMay 13, 5pm–6pm PDT — 7 commits, +114 linesMay 13, 6pm–7pm PDT — 4 commits, +605 linesMay 13, 9pm–10pm PDT — 1 commit, +13 linesMay 13, 10pm–11pm PDT — 1 commit, +48 linesMay 13, 11pm–12am PDT — 1 commit, +8 linesMay 14May 14, 12am–1am PDT — 1 commit, +150 lines

Every commit on the port branch (merges excluded), bucketed by hour. Peak hour: 695 commits.

Notice the inconsistent timing? I forgot to increase the default IOPS on the EC2 instance this ran on. One slow grep command was all it took to freeze disk reads & writes for minutes.

最终编写代码

多亏了所有这些并行化和准备工作,在峰值时 Claude 每分钟写了大约 1,300 行代码。每行代码都由两个独立的对抗性审查者(也是 Claude)审查,并在提交前经过一轮修复。当然,这些代码当时一个都不能工作。

11 天 × 24 小时 · PDT

6,502 次提交

每小时 1,695 次提交

(此处为图表,包含每个小时桶内的提交次数,详情参见原文)

注意到时间不一致了吗?我忘了增加运行此工作的 EC2 实例的默认 IOPS。一个慢速的 grep 命令就足以使磁盘读写冻结几分钟。

§ 11

Compiler errors as a work queue

After writing all the code, I asked Claude to write a workflow fixing every compiler error. We went crate-by-crate.

✻ claude code · dynamic workflow

≈16,000 errors left

Wed, May 6, 12:40 AM PDT

errors.txt0 fix commits

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

divvied up · 64 claudes

1 fixes2 review1 applies

How phase D worked, replayed from its 1,610 real commits (May 6, PDT): cargo check wrote ≈16,000 errors to a file, grouped by crate; the workflow divvied them up among 64 Claudes — 16 loops across 4 worktrees, each one Claude fixing, two reviewing, one applying. Every chip is a batch of real commits: it lands on its actual crate and only then do the counters move. Error lines are real commit subjects.

The trickiest class of error was cyclical dependencies.

Our Zig codebase was one compilation unit (effectively one crate). I wanted to split the new Rust codebase into ~100 crates so the Rust would compile faster, but this needed to avoid cyclical dependencies while minimizing changes compared to the original Zig implementation. My PR to do this immediately before starting the Rust rewrite was insufficient. Instead of starting over, I ran another workflow to classify where the code with cyclical dependencies should go and write it all down - and then another workflow to do the refactor.

Fixing the cyclical dependencies revealed about 16,000 compiler errors. A massive number for 1 human, but not a crazy number for 64 claudes at once.

To maximize parallelism, the workflow looped over each crate.

For each crate, run cargo check, group the output by file and save the errors to a file

Fix all the compiler errors within that crate

2 adversarial reviewers for the crate's changes

1 fixer applies the fixes

To prevent claudes from stepping on each other, cargo check only ran at the very start and like the other runs, no git until the end.

Another false start

Claude interpreted "let's get all the crates to compile" as "stub out the functions with compilation errors". Claude also started adding suspiciously long explanatory comments to document workarounds, so I added this rule for the adversarial reviewers to reject:

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code.

One prompt edit and a few hours later, these things stopped happening.

编译器错误作为工作队列

在写完所有代码后,我要求 Claude 编写一个工作流来修复每个编译器错误。我们按 crate 逐一进行。

✻ claude code · 动态工作流

约 16,000 个错误剩余

2025 年 5 月 6 日星期三 凌晨 12:40 PDT

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

1 个修复者 2 个审查者 1 个应用者

Phase D 如何运作(根据其 1,610 个真实提交重放,2025 年 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 个编译器错误。对一个人来说是巨大的数字,但对 64 个 Claude 来说并不疯狂。

为了最大化并行性,工作流在每个 crate 上循环。

对于每个 crate,运行 cargo check,将输出按文件分组并保存到文件

修复该 crate 中的所有编译器错误

该 crate 更改的 2 个对抗性审查者

1 个修复者应用修复

为了防止 Claude 互相干扰,cargo check 只在最开始运行,像其他运行一样,直到最后才使用 git。

另一个假启动

Claude 将“让我们让所有 crate 编译”解释为“用编译错误存根函数”。Claude 还开始添加可疑的长篇解释性注释来记录变通方法,所以我为对抗性审查者添加了这条规则:

如果你需要用一整段注释来证明变通方法没问题,那代码就是错的——修复代码。

一次提示编辑和几个小时后,这些情况停止了。

§ 12

Smoke tests

Models love saying "smoke tests"

Once cargo check passed, getting it to compile and run bun --version was next. It had linker errors. Then, it panicked immediately on start.

The next goal was to get it to run bun test <file>. Once that worked, we could start running tests! Time for another workflow, looping over bun CLI subcommands:

Save each failing stacktrace to a file along with its subcommand

For each failing stacktrace grouped by subcommand, have 1 Claude fix

2 adversarial reviewers

1 fixer applies the suggestions

Get the test suite passing locally

This workflow looped on test files.

Run about 100 random test files sharded to one of 4 worktrees by folder in the codebase. For each failing test, save the stacktrace & errors to a file, 1 implementer proposes a fix, 2 adversarial reviewers, then 1 fixer applies.

Even more false starts

Our test suite has lots of memory leak tests and a handful of integration tests that can take more than a minute - for example: a test that runs next dev and checks hot module reloading can pick up on changes 100 times. Several of these tests timeout in debug builds.

We also have stress tests that exhaust the max number of TCP sockets on the machine, tests that read & write gigabytes to disk, and tests that spawn ~10k processes.

This needed stronger isolation than "please", so we used systemd-run (cgroups) to limit memory & CPU usage and isolate pid namespaces. The machine ran out of disk space and crashed several times anyway.

Get the test suite passing in CI

Two days after the first CI run, the failing list was down from 972 test files to 23. A day and a half after that, Linux went fully green — and for the first time, it felt like this Rust rewrite was actually going to work.

✻ claude code · dynamic workflowbuildkite · the race to green, by platformWindows finished last · May 11, 6:23 AM PDT

6 / 6 platforms green

build #54202 · Thu, May 14, 12:23 AM PDT

May 8May 9May 10May 11May 12May 13May 14

macOS x64 · 2 shards

build #52897: shard failuresbuild #52932: shard failuresbuild #52934: shard failuresbuild #52938: shard failuresbuild #52944: shard failuresbuild #52946: shard failuresbuild #52949: shard failuresbuild #52975: shard failuresbuild #52998: shard failuresbuild #53007: shard failuresbuild #53015: shard failuresbuild #53026: shard failuresbuild #53027: shard failuresbuild #53035: shard failuresbuild #53041: shard failuresbuild #53047: shard failuresbuild #53056: shard failuresbuild #53077: shard failuresbuild #53090: shard failuresbuild #53095: shard failuresbuild #53106: shard failuresbuild #53109: shard failuresbuild #53123: shard failuresbuild #53127: shard failuresbuild #53130: shard failuresbuild #53131: shard failuresbuild #53133: shard failuresbuild #53134: shard failuresbuild #53143: shard failuresbuild #53149: all shards passedbuild #53159: all shards passedbuild #53164: all shards passedbuild #53167: all shards passedbuild #53172: all shards passedbuild #53176: all shards passedbuild #53194: all shards passedbuild #53208: all shards passedbuild #53213: shard failuresbuild #53214: shard failuresbuild #53216: no failures (partial run)build #53222: all shards passedbuild #53229: no failures (partial run)build #53241: all shards passedbuild #53265: all shards passedbuild #53271: all shards passedbuild #53304: shard failuresbuild #53327: shard failuresbuild #53340: shard failuresbuild #53401: shard failuresbuild #53431: shard failuresbuild #53491: all shards passedbuild #53503: all shards passedbuild #53748: shard failuresbuild #53753: all shards passedbuild #53787: all shards passedbuild #53811: all shards passedbuild #53933: no failures (partial run)build #53952: all shards passedbuild #53983: shard failuresbuild #53992: shard failuresbuild #53999: shard failuresbuild #54012: all shards passedbuild #54015: all shards passedbuild #54017: all shards passedbuild #54022: shard failuresbuild #54026: shard failuresbuild #54033: all shards passedbuild #54040: all shards passedbuild #54047: shard failuresbuild #54049: shard failuresbuild #54057: shard failuresbuild #54064: all shards passedbuild #54074: all shards passedbuild #54093: all shards passedbuild #54144: no failures (partial run)build #54161: shard failuresbuild #54186: shard failuresbuild #54189: shard failuresbuild #54196: shard failuresbuild #54202: all shards passed✓

Linux arm64 · 60 shards

build #52934: shard failuresbuild #52938: shard failuresbuild #52944: shard failuresbuild #52969: shard failuresbuild #52975: shard failuresbuild #52980: shard failuresbuild #52988: shard failuresbuild #52996: shard failuresbuild #52998: shard failuresbuild #53007: shard failuresbuild #53013: shard failuresbuild #53014: shard failuresbuild #53015: shard failuresbuild #53026: shard failuresbuild #53027: shard failuresbuild #53031: no failures (partial run)build #53032: shard failuresbuild #53035: shard failuresbuild #53041: shard failuresbuild #53047: shard failuresbuild #53056: shard failuresbuild #53059: shard failuresbuild #53077: shard failuresbuild #53083: shard failuresbuild #53086: shard failuresbuild #53090: shard failuresbuild #53095: shard failuresbuild #53106: shard failuresbuild #53109: shard failuresbuild #53123: shard failuresbuild #53127: shard failuresbuild #53130: shard failuresbuild #53131: shard failuresbuild #53133: shard failuresbuild #53134: shard failuresbuild #53135: shard failuresbuild #53143: shard failuresbuild #53149: shard failuresbuild #53159: shard failuresbuild #53164: shard failuresbuild #53167: all shards passedbuild #53172: shard failuresbuild #53176: all shards passedbuild #53188: shard failuresbuild #53194: shard failuresbuild #53208: all shards passedbuild #53212: shard failuresbuild #53213: shard failuresbuild #53214: shard failuresbuild #53216: all shards passedbuild #53222: all shards passedbuild #53229: all shards passedbuild #53236: no failures (partial run)build #53241: all shards passedbuild #53260: all shards passedbuild #53265: all shards passedbuild #53271: all shards passedbuild #53280: no failures (partial run)build #53298: no failures (partial run)build #53304: shard failuresbuild #53327: all shards passedbuild #53340: all shards passedbuild #53360: no failures (partial run)build #53419: shard failuresbuild #53431: shard failuresbuild #53458: no failures (partial run)build #53485: shard failuresbuild #53491: shard failuresbuild #53503: shard failuresbuild #53514: no failures (partial run)build #53570: shard failuresbuild #53583: shard failuresbuild #53599: shard failuresbuild #53748: shard failuresbuild #53753: all shards passedbuild #53762: no failures (partial run)build #53787: no failures (partial run)build #53811: all shards passedbuild #53852: no failures (partial run)build #53863: shard failuresbuild #53893: shard failuresbuild #53914: all shards passedbuild #53933: all shards passedbuild #53952: all shards passedbuild #53983: shard failuresbuild #53992: shard failuresbuild #53999: shard failuresbuild #54008: no failures (partial run)build #54012: all shards passedbuild #54015: all shards passedbuild #54017: all shards passedbuild #54022: shard failuresbuild #54026: all shards passedbuild #54030: shard failuresbuild #54033: all shards passedbuild #54040: all shards passedbuild #54047: shard failuresbuild #54049: shard failuresbuild #54055: shard failuresbuild #54057: shard failuresbuild #54064: all shards passedbuild #54074: all shards passedbuild #54083: no failures (partial run)build #54093: all shards passedbuild #54144: all shards passedbuild #54161: shard failuresbuild #54186: shard failuresbuild #54189: shard failuresbuild #54196: shard failuresbuild #54202: all shards passed✓

Linux x64 · 60 shards

build #52934: shard failuresbuild #52938: shard failuresbuild #52944: shard failuresbuild #52969: shard failuresbuild #52975: shard failuresbuild #52988: shard failuresbuild #52996: shard failuresbuild #52998: shard failuresbuild #53007: shard failuresbuild #53013: shard failuresbuild #53014: shard failuresbuild #53015: shard failuresbuild #53026: shard failuresbuild #53027: shard failuresbuild #53032: shard failuresbuild #53033: shard failuresbuild #53035: shard failuresbuild #53041: shard failuresbuild #53047: shard failuresbuild #53056: shard failuresbuild #53059: no failures (partial run)build #53077: shard failuresbuild #53083: no failures (partial run)build #53086: shard failuresbuild #53090: shard failuresbuild #53095: shard failuresbuild #53106: shard failuresbuild #53109: shard failuresbuild #53123: shard failuresbuild #53127: shard failuresbuild #53130: shard failuresbuild #53131: shard failuresbuild #53133: shard failuresbuild #53134: shard failuresbuild #53135: shard failuresbuild #53143: shard failuresbuild #53149: shard failuresbuild #53159: shard failuresbuild #53164: shard failuresbuild #53167: all shards passedbuild #53172: all shards passedbuild #53176: all shards passedbuild #53188: shard failuresbuild #53194: all shards passedbuild #53208: all shards passedbuild #53212: shard failuresbuild #53213: shard failuresbuild #53214: shard failuresbuild #53216: all shards passedbuild #53222: all shards passedbuild #53229: all shards passedbuild #53236: no failures (partial run)build #53241: all shards passedbuild #53260: all shards passedbuild #53265: all shards passedbuild #53271: all shards passedbuild #53280: no failures (partial run)build #53304: shard failuresbuild #53327: all shards passedbuild #53340: all shards passedbuild #53360: no failures (partial run)build #53419: no failures (partial run)build #53431: shard failuresbuild #53458: no failures (partial run)build #53485: shard failuresbuild #53491: all shards passedbuild #53503: all shards passedbuild #53514: no failures (partial run)build #53570: shard failuresbuild #53583: shard failuresbuild #53599: shard failuresbuild #53748: shard failuresbuild #53753: all shards passedbuild #53759: no failures (partial run)build #53781: shard failuresbuild #53787: no failures (partial run)build #53811: all shards passedbuild #53863: shard failuresbuild #53893: shard failuresbuild #53914: all shards passedbuild #53933: shard failuresbuild #53952: all shards passedbuild #53983: shard failuresbuild #53992: shard failuresbuild #53999: shard failuresbuild #54008: no failures (partial run)build #54012: all shards passedbuild #54015: all shards passedbuild #54017: all shards passedbuild #54022: shard failuresbuild #54026: all shards passedbuild #54030: shard failuresbuild #54033: all shards passedbuild #54040: no failures (partial run)build #54047: shard failuresbuild #54049: shard failuresbuild #54055: shard failuresbuild #54057: shard failuresbuild #54064: all shards passedbuild #54074: all shards passedbuild #54083: no failures (partial run)build #54093: all shards passedbuild #54144: all shards passedbuild #54161: shard failuresbuild #54186: shard failuresbuild #54189: shard failuresbuild #54196: shard failuresbuild #54202: all shards passed✓

macOS arm64 · 4 shards

build #52897: shard failuresbuild #52929: shard failuresbuild #52932: shard failuresbuild #52944: shard failuresbuild #52975: shard failuresbuild #52996: shard failuresbuild #52998: shard failuresbuild #53007: shard failuresbuild #53013: shard failuresbuild #53014: shard failuresbuild #53015: shard failuresbuild #53026: shard failuresbuild #53027: shard failuresbuild #53032: shard failuresbuild #53035: shard failuresbuild #53041: shard failuresbuild #53047: shard failuresbuild #53056: shard failuresbuild #53059: shard failuresbuild #53077: shard failuresbuild #53095: shard failuresbuild #53109: shard failuresbuild #53123: shard failuresbuild #53127: shard failuresbuild #53130: shard failuresbuild #53131: shard failuresbuild #53133: shard failuresbuild #53134: shard failuresbuild #53135: shard failuresbuild #53143: shard failuresbuild #53149: shard failuresbuild #53159: shard failuresbuild #53164: shard failuresbuild #53167: shard failuresbuild #53172: shard failuresbuild #53176: shard failuresbuild #53188: shard failuresbuild #53194: shard failuresbuild #53208: shard failuresbuild #53212: shard failuresbuild #53213: shard failuresbuild #53214: shard failuresbuild #53216: shard failuresbuild #53222: shard failuresbuild #53229: no failures (partial run)build #53236: no failures (partial run)build #53241: all shards passedbuild #53265: all shards passedbuild #53271: no failures (partial run)build #53280: no failures (partial run)build #53304: shard failuresbuild #53327: no failures (partial run)build #53340: no failures (partial run)build #53360: no failures (partial run)build #53368: shard failuresbuild #53379: shard failuresbuild #53383: shard failuresbuild #53401: shard failuresbuild #53431: shard failuresbuild #53458: shard failuresbuild #53491: no failures (partial run)build #53503: shard failuresbuild #53570: shard failuresbuild #53583: shard failuresbuild #53599: shard failuresbuild #53601: shard failuresbuild #53748: shard failuresbuild #53753: all shards passedbuild #53757: no failures (partial run)build #53759: no failures (partial run)build #53787: no failures (partial run)build #53811: all shards passedbuild #53952: no failures (partial run)build #53992: shard failuresbuild #53999: shard failuresbuild #54007: no failures (partial run)build #54012: all shards passedbuild #54015: shard failuresbuild #54017: all shards passedbuild #54022: shard failuresbuild #54026: no failures (partial run)build #54030: shard failuresbuild #54033: all shards passedbuild #54040: shard failuresbuild #54047: shard failuresbuild #54049: shard failuresbuild #54055: shard failuresbuild #54057: shard failuresbuild #54064: shard failuresbuild #54074: shard failuresbuild #54093: shard failuresbuild #54161: shard failuresbuild #54186: shard failuresbuild #54189: shard failuresbuild #54196: shard failuresbuild #54202: all shards passed✓

Windows x64 · 8 shards

build #53090: shard failuresbuild #53094: shard failuresbuild #53095: shard failuresbuild #53106: shard failuresbuild #53109: shard failuresbuild #53123: shard failuresbuild #53127: shard failuresbuild #53130: shard failuresbuild #53131: shard failuresbuild #53133: shard failuresbuild #53134: shard failuresbuild #53135: shard failuresbuild #53143: shard failuresbuild #53149: shard failuresbuild #53159: shard failuresbuild #53164: shard failuresbuild #53167: shard failuresbuild #53172: shard failuresbuild #53176: shard failuresbuild #53188: shard failuresbuild #53194: shard failuresbuild #53208: shard failuresbuild #53212: shard failuresbuild #53213: shard failuresbuild #53214: shard failuresbuild #53216: shard failuresbuild #53222: shard failuresbuild #53229: shard failuresbuild #53236: shard failuresbuild #53241: shard failuresbuild #53260: shard failuresbuild #53265: shard failuresbuild #53271: shard failuresbuild #53280: shard failuresbuild #53298: shard failuresbuild #53304: shard failuresbuild #53327: all shards passedbuild #53340: all shards passedbuild #53360: no failures (partial run)build #53419: shard failuresbuild #53431: shard failuresbuild #53458: shard failuresbuild #53470: no failures (partial run)build #53485: shard failuresbuild #53491: no failures (partial run)build #53503: shard failuresbuild #53514: no failures (partial run)build #53565: no failures (partial run)build #53570: shard failuresbuild #53599: shard failuresbuild #53745: shard failuresbuild #53748: shard failuresbuild #53753: shard failuresbuild #53757: shard failuresbuild #53759: shard failuresbuild #53762: shard failuresbuild #53769: shard failuresbuild #53781: shard failuresbuild #53787: shard failuresbuild #53808: shard failuresbuild #53811: shard failuresbuild #53852: shard failuresbuild #53863: shard failuresbuild #53883: shard failuresbuild #53893: shard failuresbuild #53914: all shards passedbuild #53933: all shards passedbuild #53952: all shards passedbuild #53973: no failures (partial run)build #53983: shard failuresbuild #53992: shard failuresbuild #53999: shard failuresbuild #54002: no failures (partial run)build #54004: no failures (partial run)build #54007: shard failuresbuild #54008: no failures (partial run)build #54012: all shards passedbuild #54015: all shards passedbuild #54017: all shards passedbuild #54022: shard failuresbuild #54026: all shards passedbuild #54030: all shards passedbuild #54033: all shards passedbuild #54040: all shards passedbuild #54047: shard failuresbuild #54049: shard failuresbuild #54055: shard failuresbuild #54057: shard failuresbuild #54064: all shards passedbuild #54074: all shards passedbuild #54083: all shards passedbuild #54093: all shards passedbuild #54144: shard failuresbuild #54161: shard failuresbuild #54186: shard failuresbuild #54189: shard failuresbuild #54196: shard failuresbuild #54202: all shards passed✓

Windows arm64 · 8 shards

build #53090: shard failuresbuild #53095: shard failuresbuild #53106: shard failuresbuild #53109: shard failuresbuild #53123: shard failuresbuild #53127: shard failuresbuild #53130: shard failuresbuild #53131: shard failuresbuild #53134: shard failuresbuild #53135: shard failuresbuild #53149: shard failuresbuild #53159: shard failuresbuild #53164: shard failuresbuild #53167: shard failuresbuild #53172: shard failuresbuild #53176: shard failuresbuild #53188: shard failuresbuild #53194: shard failuresbuild #53208: shard failuresbuild #53212: shard failuresbuild #53213: shard failuresbuild #53214: shard failuresbuild #53216: shard failuresbuild #53222: shard failuresbuild #53229: shard failuresbuild #53236: no failures (partial run)build #53241: shard failuresbuild #53260: shard failuresbuild #53265: shard failuresbuild #53271: shard failuresbuild #53304: shard failuresbuild #53327: all shards passedbuild #53340: all shards passedbuild #53360: no failures (partial run)build #53419: no failures (partial run)build #53431: shard failuresbuild #53458: no failures (partial run)build #53485: shard failuresbuild #53491: no failures (partial run)build #53503: no failures (partial run)build #53599: shard failuresbuild #53748: shard failuresbuild #53753: shard failuresbuild #53757: shard failuresbuild #53759: shard failuresbuild #53762: shard failuresbuild #53787: shard failuresbuild #53808: shard failuresbuild #53811: shard failuresbuild #53852: shard failuresbuild #53863: shard failuresbuild #53883: shard failuresbuild #53893: shard failuresbuild #53914: no failures (partial run)build #53933: all shards passedbuild #53952: all shards passedbuild #53983: shard failuresbuild #53992: shard failuresbuild #53999: shard failuresbuild #54007: no failures (partial run)build #54012: all shards passedbuild #54015: all shards passedbuild #54017: all shards passedbuild #54022: shard failuresbuild #54026: all shards passedbuild #54030: no failures (partial run)build #54033: all shards passedbuild #54040: all shards passedbuild #54047: shard failuresbuild #54049: shard failuresbuild #54055: no failures (partial run)build #54057: shard failuresbuild #54064: all shards passedbuild #54074: all shards passedbuild #54083: no failures (partial run)build #54093: all shards passedbuild #54144: shard failuresbuild #54161: shard failuresbuild #54186: shard failuresbuild #54189: shard failuresbuild #54196: shard failuresbuild #54202: all shards passed✓

Every CI build's test shards, by platform, across 135 builds that ran tests (420 mined from BuildKite). Bright green: every shard passed. Dim green: no failures, but the run was cut short (superseded). Red: at least one shard failed. Each lane is stamped when its full suite first passes — Linux's 60 shards were green almost a full day before Windows. Platforms kept wobbling red until the last failing tests fell; the final all-green build was #54202.

冒烟测试

模型总喜欢说“冒烟测试”

一旦 cargo check 通过,下一步就是让它编译并运行 bun --version。出现链接器错误。然后,启动时立即崩溃。

下一个目标是让它运行 bun test <file>。一旦成功,我们就可以开始运行测试了!是时候运行另一个工作流,循环处理 bun CLI 子命令:

将每个失败的堆栈跟踪及其子命令保存到文件

对于按子命令分组的每个失败堆栈跟踪,让 1 个 Claude 修复

2 个对抗性审查者

1 个修复者应用建议

让测试套件在本地通过

这个工作流在测试文件上循环。

运行大约 100 个随机测试文件,按代码库中的文件夹分片到 4 个 worktree 之一。对于每个失败的测试,将堆栈跟踪和错误保存到文件,1 个实现者提出修复,2 个对抗性审查者,然后 1 个修复者应用。

更多的假启动

我们的测试套件有很多内存泄漏测试和一些集成测试,耗时可能超过一分钟——例如,一个运行 next dev 并检查热模块重新加载的测试可以检测 100 次更改。其中一些测试在调试构建中会超时。

我们还有压力测试,耗尽机器上的最大 TCP 套接字数,测试读写 GB 级数据到磁盘,以及生成约 10k 个进程的测试。

这需要比“请”更强大的隔离,所以我们使用了 systemd-run(cgroups)来限制内存和 CPU 使用,并隔离 pid 命名空间。机器还是因为磁盘空间不足而崩溃了几次。

让测试套件在 CI 中通过

第一次 CI 运行两天后,失败测试文件从 972 个下降到 23 个。又过了一天半,Linux 完全变绿——这是第一次感觉这次 Rust 重写真的能成功。

✻ claude code · 动态工作流 buildkite · 冲向全绿,按平台 Windows 最后完成 · 2025 年 5 月 11 日 上午 6:23 PDT

6 / 6 平台全绿

构建 #54202 · 2025 年 5 月 14 日 星期四 上午 12:23 PDT

(此处为各平台构建状态图,详情参见原文)

每个 CI 构建的测试分片,按平台,来自 135 个运行测试的构建(从 BuildKite 中分析了 420 个)。亮绿色:所有分片通过。暗绿色:没有失败,但运行被截断(被取代)。红色:至少一个分片失败。每个通道在其完整套件首次通过时标记——Linux 的 60 个分片几乎比 Windows 早一整天变绿。各平台一直摇摆红色,直到最后失败的测试消失;最终的全面绿色构建是 #54202。

§ 13

The rest of the time leading up to merging it was straightforward. A workflow that looped on fixing CI test failures for each platform until there were no more test failures. Several workflows for Windows-related cleanup, to deduplicate code, to reduce unsafe usage, and to generally clean up some code.

Merging the Rust rewrite

Once 100% of Bun's test suite passed in CI on all platforms (and I manually verified the tests were in fact running and not being skipped), I ran a bunch of commands locally to test things - and then I pressed the merge button.

Merging into main isn't a versioned release. At this point, I was confident enough to move forward and commit to the rewrite, but not yet confident enough to release it.

Stats

At peak, we were running 4 of these workflows at once each in a separate worktree, each with 16 Claudes per workflow. About 64 Claudes at a time.

git log · claude/phase-a-portpeak: 58 commits in one minute

+0

lines written, rewrites included

Mon, May 4, 7:05 AM PDT

first 100-file draft batchPR #30412 openedmerged

All 6,502 commits (merges excluded), replayed. Pink bars are mostly new code; cyan bars are mostly deletion. The line counter counts every rewrite along the way — the diff that landed was +1,009,272. The log is real commit messages.

0 tests skipped or deleted

11 days (May 3 → merged May 14) · 6,778 commits

Platformexpect() callsTestsFilesDebian 13 x641,386,82660,6244,174macOS 14 arm641,259,95358,8504,175Windows 2019 x641,007,54457,3374,173

Pre-merge, this took 5.9 billion uncached input tokens, 690 million output tokens, and 72 billion cached input token reads — around $165,000 at API pricing. By hand, I think this would've taken 3 engineers with full context on the codebase about a year, during which time we wouldn't be able to improve Node.js compatibility, fix bugs, fix security issues or implement new features. We never would've done that. The realistic alternative was to do nothing and keep fixing the bugs at the top of this post forever.

This is the bleeding edge of what's possible today. I used a pre-release version of Claude Fable 5, a Mythos-class model. Claude Code's dynamic workflows kept 64 Claudes running for 11 days (I would've had to write my own harness to pull this off otherwise).

The work continues

Since merging the Rust port, we've completed 11 rounds of security review from Claude Code Security and addressed the findings.

We've also added 24/7 coverage-guided fuzzing of every parser in Bun — JavaScript, TypeScript, JSX, CSS, JSON5, JSONC, TOML, YAML, Markdown, INI, Bun Shell scripts, semver ranges, .patch files, and CSS colors. The fuzzer automatically sends the bugs it finds to Claude to submit a PR reproducing & fixing, and humans review the PRs. So far, it's executed our parsers 100 billion times which has led to around 15 PRs.

At the time of writing, about 4% of Bun's Rust code sits inside an unsafe block (~13,000 unsafe keywords across ~27,000 lines / ~780,000 lines), and 78% of those blocks are a single line — a pointer that came from C++, or one call into a C library. I expect this number to go down over time as we refactor from a faithful Zig port (which had no greppable unsafe keyword) to idiomatic Rust, but we are going to continue using C & C++ libraries like JavaScriptCore so it will always have more unsafe than pure Rust projects.

合并前剩下的时间就很简单了。一个工作流循环修复每个平台的 CI 测试失败,直到没有更多测试失败。几个用于 Windows 相关清理、去重代码、减少 unsafe 使用以及通常清理一些代码的工作流。

合并 Rust 重写

一旦 100% 的 Bun 测试套件在所有平台的 CI 中通过(并且我手动验证了测试确实运行了,没有被跳过),我在本地运行了一堆命令进行测试——然后按下了合并按钮。

合并到 main 并不是一个版本发布。此时,我已经有足够的信心继续前进并承诺重写,但还没有足够信心发布它。

统计数据

在峰值时,我们同时运行了 4 个工作流,每个在单独的 worktree 中,每个工作流有 16 个 Claude。一次大约 64 个 Claude。

git log · claude/phase-a-port峰值:一分钟内 58 次提交

+0

包括重写在内的编写行数

2025 年 5 月 4 日 上午 7:05 PDT

首批 100 个文件草稿批次PR #30412 打开已合并

所有 6,502 次提交(合并除外)重放。粉红色条主要是新代码;青色条主要是删除。行计数器计算沿途的每次重写——最终的 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 的动态工作流让 64 个 Claude 持续运行了 11 天(否则我不得不编写自己的工具来完成这项工作)。

工作还在继续

自合并 Rust 移植以来,我们已经完成了来自 Claude Code Security 的 11 轮安全审查,并处理了所有发现。

我们还添加了对 Bun 中每个解析器的全天候覆盖引导模糊测试——JavaScript、TypeScript、JSX、CSS、JSON5、JSONC、TOML、YAML、Markdown、INI、Bun Shell 脚本、semver 范围、.patch 文件和 CSS 颜色。模糊测试器自动将其发现的 bug 发送给 Claude,由 Claude 提交一个重现和修复的 PR,然后由人类审查这些 PR。到目前为止,它已经执行了我们的解析器 1000 亿次,产生了大约 15 个 PR。

在撰写本文时,Bun 中大约 4% 的 Rust 代码位于 unsafe 块中(约 13,000 个 unsafe 关键字,分布在约 27,000 行 / 总约 780,000 行中),其中 78% 的块是单行——一个来自 C++ 的指针,或一次对 C 库的调用。我预计这个数字会随着我们从忠实的 Zig 移植(其中没有可 grep 的 unsafe 关键字)重构为地道的 Rust 而下降,但我们将继续使用 C 和 C++ 库(如 JavaScriptCore),因此它总是会比纯 Rust 项目有更多的 unsafe 代码。

§ 14

Porting mistakes

The focus of the Rust rewrite is stability, but it would be impossible to ship a massive change like this and introduce zero regressions.

This rewrite introduced 19 known regressions, each of which has been fixed.

Most of the regressions came from code that's syntactically identical in both languages but semantically different.

Side effect inside debug_assert!

These two snippets look similar but behave differently. Zig's assert is a function, so its argument runs in every build. Rust's debug_assert! is a macro, so in release builds the whole expression is erased, including the insert_stale call.

// 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 adds a file to the frontend dev server's hot reload graph. In release builds it stopped running, and HMR broke in certain cases for projects with HTML routes that use React while a hot reloaded file gets invalidated: Cannot destructure property 'isLikelyComponentType' of 'k'. Debug builds worked. #30678

Slices of odd length

Bun's Zig helper reinterpretSlice(u16, bytes) (predating builtin casts supporting slices) used @divTrunc and ignored a trailing odd byte. bytemuck::cast_slice panics on it instead. Blob.text() on a UTF-16 byte order mark followed by an odd number of bytes stopped returning a string and panicked the process. We went back to ignoring the odd byte: &buf[..buf.len() & !1]. #31188

Bounds checks

On macOS & Linux, we compiled Bun's Zig code with ReleaseFast, which removes bounds checks. Rust's release builds keep them.

Bun's module resolver interns long filenames into a global list that spills into overflow blocks. The original Zig code sized each block at count / 4, or 2048. The port left a placeholder:

/// ... so use a nonzero stand-in until Phase B threads the /// per-instantiation value through. pub const BSS_OVERFLOW_BLOCK_SIZE: usize = 64;

That lowered the ceiling from 8.4 million interned filenames to 270,272, which real projects hit, and made a ptrs[4095] off-by-one we ported from Zig reachable. Rust panicked instead of writing past the end. Zig would also panic in this case, if we used ReleaseSafe (we only did on Windows). #31503

comptime format strings

Output.pretty rewrites <r> and <d> color markers into ANSI escapes. In Zig, fmt is comptime, so the markers are gone before the arguments are substituted. Rust functions don't have comptime parameters, so Output::pretty only ever saw the finished string, and rewrote markers over the arguments too.

// 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 prints package names as OSC 8 hyperlinks, terminated by ESC . That backslash sits right before the < of the trailing <r>, the marker parser eats it, and the r prints as text.

it should say oxfmt, not oxfmtr

In Rust it has to be a macro: bun_core::pretty!("<r>{}<r>", hyperlink). #30693

移植中的错误

Rust 重写的重点是稳定性,但不可能发布如此巨大的更改而不引入任何回归。

这次重写引入了 19 个已知的回归,每个都已修复。

大多数回归来自语法上两种语言相同但语义不同的代码。

debug_assert! 内的副作用

这两段代码看起来相似但行为不同。Zig 的 assert 是一个函数,因此其参数在每个构建中都会运行。Rust 的 debug_assert! 是一个宏,因此在发布构建中整个表达式被擦除,包括 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 将一个文件添加到前端开发服务器的热重载图中。在发布构建中它停止了运行,HMR 在某些情况下对于使用 React 并具有 HTML 路由的项目中断,当热重载文件失效时:Cannot destructure property 'isLikelyComponentType' of 'k'。调试构建正常工作。#30678

奇数长度的切片

Bun 的 Zig 辅助函数 reinterpretSlice(u16, bytes)(早于支持切片的内置转换)使用了 @divTrunc 并忽略了尾部的奇数个字节。bytemuck::cast_slice 对此会 panic。在 UTF-16 字节顺序标记后跟奇数个字节的情况下,Blob.text() 停止返回字符串并导致进程 panic。我们改回忽略奇数个字节:&buf[..buf.len() & !1]。 #31188

边界检查

在 macOS 和 Linux 上,我们使用 ReleaseFast 编译 Bun 的 Zig 代码,该模式会移除边界检查。Rust 的发布构建保留了边界检查。

Bun 的模块解析器将长文件名驻留到一个全局列表中,该列表会溢出到溢出块。原始的 Zig 代码将每个块的大小设置为 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 万降低到 270,272,真实项目达到了这个限制,并使我们从 Zig 移植过来的 ptrs[4095] 超出范围错误变得可触发。Rust 会 panic 而不是写入越界。Zig 在这种情况下也会 panic,如果我们使用 ReleaseSafe(我们只在 Windows 上这样做)。 #31503

编译期格式字符串

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

§ 15

Bun is better in Rust

So far, Bun v1.4.0 fixes 128 bugs that reproduce in v1.3.14. These range from memory leaks to crashes to miscolored help text.

Reduced memory usage

Rust has a powerful language-level tool for cleaning up memory: Drop. When Drop is implemented, the drop function is automatically called every time the value goes out of scope.

impl Drop for Bytes { fn drop(&mut self) { if !self.pinned.is_empty() { JSC__JSValue__unpinArrayBuffer(self.pinned); } } }

In Zig, defer can be used to run code at the end of a scope:

const bytes: ArrayBuffer = try .fromPinned(global, value); defer bytes.unpin();

In Zig, defer needs to be added to every individual call site that might need cleanup. It's easy to end up forgetting to clean up (a memory leak), or to run cleanup code twice in rarely-reached error handling code (a double-free). In Rust, Drop runs automatically when the value is no longer accessible - trading "no hidden control flow" for preventing a common footgun.

Drop fixed several memory leaks in Bun related to file paths in error handling code.

We fixed every instrumentable memory leak

We improved Bun's LeakSanitizer integration to track all native code memory allocations.

Here's an example: every in-process Bun.build() call leaked several megabytes of memory — parsed source text and AST symbol tables that outlived the build they belonged to.

// 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", }); }

In Bun v1.3.14, every build leaks about 3 MB, forever — tools like dev servers that bundle on every request eventually run out of memory. In Bun v1.4.0, memory levels off:

BuildsBun v1.3.14Bun v1.4.05001,914 MB526 MB1,0003,506 MB586 MB1,5005,097 MB608 MB2,0006,745 MB609 MB

A previous attempt to do this in Zig was not merged because the lack of an equivalent of Drop made it more difficult to feel confident merging.

Bun 在 Rust 中变得更好

到目前为止,Bun v1.4.0 修复了 v1.3.14 中可重现的 128 个 bug。这些 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 需要添加到每个可能需要清理的调用点。很容易忘记清理(内存泄漏),或者在很少到达的错误处理代码中运行清理代码两次(双重释放)。在 Rust 中,Drop 在值不再可访问时自动运行——以“无隐藏控制流”的代价换取了防止常见陷阱。

Drop 修复了 Bun 中与错误处理代码中的文件路径相关的几个内存泄漏。

我们修复了每个可检测的内存泄漏

我们改进了 Bun 的 LeakSanitizer 集成,以跟踪所有本地代码内存分配。

这里有一个例子:每次进程内的 Bun.build() 调用都会泄漏几兆字节的内存——已解析的源代码文本和 AST 符号表比它们所属的构建更长寿。

// 在一个进程中捆绑同一个 60 模块项目 2000 次 for (let i = 0; i < 2_000; i++) { await Bun.build({ entrypoints: ["./index.js"], minify: true, sourcemap: "external", }); }

在 Bun v1.3.14 中,每次构建泄漏约 3 MB,永久持续——像在每个请求上捆绑的开发服务器这样的工具最终会耗尽内存。在 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 的等价物使得合并的信心不足。

§ 16

Smaller binary size

The initial changes in the Rust rewrite reduced binary size by 3.8 MB on Windows, 5.5 MB on macOS, and 6.8 MB on Linux. This is largely because we used too much comptime in our Zig code.

After that initial shrinkage, the team explored more opportunities for binary size reduction using linker optimizations like Identical Code Folding, removing unused data from ICU, and lazily decompressing small parts of libicu with a zstd dictionary on-demand.

Combined with the Rust rewrite, ICU changes, and identical code folding, Bun's binary size shrinks by ~20% on Linux & Windows.

VersionPlatformSizeBun v1.4.0 (canary)Windows76 MBBun v1.3.14Windows94 MBBun v1.4.0 (canary)Linux70 MBBun v1.3.14Linux88 MB

Reduced stack space usage

The TOML parser, and all of the other recursive-descent parsers in Bun (JSON, YAML, JavaScript, TypeScript, and more) now use less stack space.

This caused some test failures before merging the Rust rewrite:

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's LLVM IR codegen emits LLVM's llvm.lifetime.start and llvm.lifetime.end intrinsics for stack variables when they are no longer in use, which lets LLVM reuse stack space slots. This lets large functions with nested scopes use significantly less stack space.

Previously, we manually worked around an open issue by refactoring particularly large functions into many smaller functions.

更小的二进制体积

Rust 重写初始更改将二进制体积在 Windows 上减少了 3.8 MB,macOS 上 5.5 MB,Linux 上 6.8 MB。这主要是因为我们在 Zig 代码中使用了过多的 comptime。

在初步缩减之后,团队探索了更多减少二进制体积的机会,例如使用相同代码折叠等链接优化,从 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 内建函数,这使得 LLVM 可以重用栈空间槽位。这使具有嵌套作用域的大型函数使用显著更少的栈空间。

以前,我们通过将特别大的函数重构为许多更小的函数来手动解决一个未解决的 issue。

§ 17

2% - 5% faster

Rust supports cross-language link-time optimization between C/C++ and Rust, which enables inlining across programming languages (how cool is that!!).

We benchmarked Bun v1.3.14 against Bun v1.4.0 on Linux x64 (EC2, Xeon Platinum 8488C). HTTP throughput measured with oha against hello-world servers, app workloads measured with hyperfine.

HTTP throughput (req/s, avg of 3 rounds)

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%

Apps / 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%

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 吞吐量(请求/秒,3 轮平均值)

服务器 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)

工作负载 Bun v1.3.14 | Bun v1.4.0 | Δ next build 13.62s | 13.03s | +4.5% vite build (tsc + vite) 1.69s | 1.65s | +2.2% tsc -b --force 0.94s | 0.89s | +4.7%

§ 18

Production

Prisma launched the Prisma Compute public beta on Bun's Rust rewrite.

"We ran into memory leaks and a connection pool that couldn't recover after a VM was paused and resumed. When the Rust rewrite appeared, we tested it against the same failure modes. It handled them perfectly." - Alexey Orlenko

Claude Code v2.1.181 (released June 17th) and later use the Rust port of Bun. Startup got 10% faster on Linux but otherwise, barely anyone noticed. Boring is good.

Claude Code startup time from production telemetry (Linux p50): v2.1.179 at 517ms vs v2.1.181, the first release on Rust Bun, at 464ms — 10% faster

Shipping

Bun v1.3.14 was the last version of Bun written in Zig. Bun v1.4.0 will be the first version of Bun written in Rust. It's available in canary now - please report any issues you find:

Maintainability

For myself and the team, our new Rust codebase feels very similar to the old Zig codebase. For example, here's a snippet of the original Zig code and the new Rust code:

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; }

// ... }

// ... }

Anyone who understands the original Zig code understands the mechanically translated Rust code. I reviewed the original Rust rewrite PR by checking the adversarial code review agents were correctly catching discrepancies between the Zig code and the Rust code, that they were ensuring the porting guide and lifetime guide were being followed, and also manually reading a lot of the code myself side-by-side with the Zig vs Rust.

生产环境

Prisma 在 Bun 的 Rust 重写上推出了 Prisma Compute 公开测试版。

“我们遇到了内存泄漏和连接池在 VM 暂停和恢复后无法恢复的问题。当 Rust 重写出现时,我们针对相同的故障模式进行了测试。它完美地处理了它们。”——Alexey Orlenko

Claude Code v2.1.181(2025 年 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 v1.4.0 将是第一个用 Rust 编写的版本。它现在可以在 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 代码。我通过检查对抗性代码审查代理是否正确捕获了 Zig 代码和 Rust 代码之间的差异,确保它们遵循了移植指南和生命周期指南,并且我自己也手动对照 Zig 和 Rust 阅读了大量代码,从而审查了原始的 Rust 重写 PR。

§ 19

What's next

Bun v1.4 makes Bun faster, smaller, use less memory and gives the team incredibly powerful tools for systematically improving stability going forward: Rust's borrow checker, Miri (which runs for a growing chunk of code in CI), LeakSanitizer, and 24/7 coverage-guided fuzzing for parsers. There's still more to refactor, but things are off to a great start.

This Rust rewrite would've taken a team of engineers with full-context on the codebase a year of work. With 1 engineer using Fable & closely monitoring Claude Code, we went from start to 100% of the test suite passing on all platforms in 11 days.

One engineer can do a lot more today than a year ago.

未来展望

Bun v1.4 让 Bun 更快、更小、使用更少内存,并为团队提供了极其强大的工具,用于系统地提高未来的稳定性:Rust 的借用检查器、Miri(在 CI 中对越来越多的代码运行)、LeakSanitizer,以及针对解析器的全天候覆盖引导模糊测试。虽然还有更多需要重构,但事情已经有了一个非常好的开端。

这次 Rust 重写原本需要一个对代码库有全面了解的工程师团队一年的工作量。而现在,1 名工程师使用 Fable 并密切监控 Claude Code,从开始到所有平台 100% 测试通过只用了 11 天。

如今,一名工程师可以比一年前完成多得多的任务。

Open source ↗