Bun

Rewriting Bun in Rust  用 Rust 重写 Bun


Jarred Sumner · July 8, 2026
Jarred Sumner · 2026 年 7 月 8 日

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 于 2025 年 12 月被 Anthropic 收购。我和 Bun 团队的其他成员都在 Anthropic 工作。在 Rust 重写过程中,我大量使用了 Claude Fable 5 的预发布版本。

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.
Bun 最初是将 esbuild 的 JavaScript 和 TypeScript 转译器从 Go 逐行移植到 Zig。我在 2021 年 4 月 16 日写下了第一行 Zig 代码。在 Hacker News 上看到单页的 Zig 语言参考后,我对底层控制和性能优化感到非常兴奋,于是押注了 Zig。

From the start, Bun's scope was massive:
从一开始,Bun 的规模就非常庞大:

  • JavaScript, TypeScript, and CSS transpiler, minifier, and bundler
    JavaScript、TypeScript 和 CSS 的转译器、压缩器和打包器
  • npm-compatible package manager
    兼容 npm 的包管理器
  • Jest-like test runner  类似 Jest 的测试运行器
  • Node.js & TypeScript-compatible module resolution
    兼容 Node.js 和 TypeScript 的模块解析
  • HTTP/1.1 & WebSocket client
    HTTP/1.1 和 WebSocket 客户端
  • Node.js API implementations like fs, net, tls, and dozens of other modules
    fsnettls 以及数十个其他模块这样的 Node.js API 实现

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 的初始版本是我在奥克兰一间狭小的公寓里,用 Zig 语言在一年内写成的,那时还没有 LLM。像 Bun 这样野心勃勃的项目,通常的结局就是成为 GitHub 个人主页上那些废弃的 side project 中的一员。是 Zig 让 Bun 成为可能。如果没有 Zig,我绝不可能在一年内完成这么多工作。

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 的 CLI 每月下载量超过 2200 万次。像 Claude Code 和 OpenCode 这样的流行工具都选择 Bun 作为它们的运行时。Vercel、Railway、DigitalOcean 等平台都提供了对 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:
Bun 的规模也给稳定性带来了挑战。以下是我们在 Bun v1.3.14 中修复的一小部分 bug:

  • 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
    在 zlib、Brotli 或 Zstd 流上调用 .reset() 时,如果线程池中仍有异步 .write() 正在执行,就会触发堆释放后使用的崩溃(heap-use-after-free)
  • use-after-free crash in node:zlib when an onerror callback issued a re-entrant write() followed by close() on native handles
    onerror 回调发起可重入的 write() 随后对原生句柄执行 close() 时,会导致释放后使用的崩溃(use-after-free)
  • 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
    当可重入的 JS 回调(例如超时监听器中的 session.request() 、属性 getter 或写回调)触发哈希表重新哈希,进而使内部流指针失效时,就会引发释放后使用的崩溃(use-after-free)
  • 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
    UDPSocket.send()sendMany() 中的释放后使用漏洞,当 valueOf()toString() 回调中的用户代码在载荷捕获与实际发送之间分离了 ArrayBuffer 时触发
  • 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
    Buffer#copyBuffer#fill 中的崩溃与越界读取,当 valueOf 回调在参数强制转换期间分离或调整底层 ArrayBuffer 的大小时发生
  • heap out-of-bounds write in UDPSocket.sendMany() when the socket's connection state changed mid-iteration via user JS callbacks
    UDPSocket.sendMany() 中的堆越界写入,当套接字的连接状态通过用户 JS 回调在迭代中途发生变化时触发
  • memory leak in crypto.scrypt where the callback and protected password/salt buffers were never released when the output buffer allocation failed
    crypto.scrypt 中的内存泄漏,当输出缓冲区分配失败时,回调以及受保护的密码/盐缓冲区从未被释放
  • SSLWrapper.init leaked the strdup'd passphrase on error paths
    在错误路径上, SSLWrapper.init 泄露了通过 strdup 复制的密码短语。
  • 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
    tlsSocket.setSession() 中存在内存泄漏,每次调用泄漏一个 SSL_SESSION (约 6.5 KB),原因是 d2i_SSL_SESSION 之后缺少 SSL_SESSION_free
  • 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
    内存泄漏: .close() 之后, fs.watch() 监听器从未被垃圾回收,原因是引用计数下溢,导致每个监听器被永久固定为 GC 根。
  • double-free crash in the CSS parser when background-clip had vendor prefixes and multi-layer backgrounds
    CSS 解析器中的双重释放崩溃,当 background-clip 包含厂商前缀和多层背景时。
  • DuplexUpgradeContext was never freed — a full leak per tls.connect({ socket: duplex })
    DuplexUpgradeContext 从未被释放——每个 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
    MessageEvent 中的竞态条件崩溃,GC 标记线程在从 BroadcastChannelMessagePort 并发访问时,可能会观察到 m_data 中的撕裂变体

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.
我们本可以一直以一次性修复的方式永无止境地修补这类问题,但用户信任我们,我们必须做得更好,从系统层面杜绝这类 bug 再次出现。

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.
    我们给 Zig 编译器打了补丁,加入了 Address Sanitizer 支持。每次提交都会用 ASAN 跑测试套件。
  • We ship Zig safety-checked ReleaseSafe builds on Windows
    我们在 Windows 上发布经过 Zig 安全检查的 ReleaseSafe 构建版本。
  • We fuzz Bun's runtime APIs 24/7 using Fuzzilli, the JavaScript engine fuzzer used by V8 & JavaScriptCore
    我们使用 Fuzzilli(V8 和 JavaScriptCore 所用的 JavaScript 引擎模糊测试工具)全天候对 Bun 的运行时 API 进行模糊测试。
  • 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.
“我们的 bug 修复列表让人沮丧,而我已厌倦了每晚带着对 Bun 崩溃的担忧入睡。我并不责怪 Zig——其他 Zig 用户并没有遇到我们这些 bug,而且将垃圾回收与手动内存管理混用,是件非常罕见的事,几乎没有语言为此专门设计。没有 Zig,我们走不到今天,我永远心存感激。直到最近,像 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.
JavaScript 是一门垃圾回收语言,现代 JavaScript 引擎(比如 JavaScriptCore 和 V8)对异常处理和垃圾回收有着严格的规则。而 Zig 像 C 语言一样,不会替你管理内存——这种权衡恰恰是很多项目选择 Zig 的重要原因。Zig 没有构造函数和析构函数,大多数清理工作都需要在每个调用点显式地写出来,比如 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?
对于 Bun 来说,正确处理垃圾回收值和手动管理值的生命周期一直是稳定性问题的主要来源——最常见的是小规模内存泄漏,偶尔也会导致崩溃。每个内存分配都必须经过仔细审查:这些字节在哪里被释放?如何确保它们只被释放一次?有没有正确检查 JavaScript 异常?这个垃圾回收指针是否对保守栈扫描器可见?这块内存到底是垃圾回收管理的,还是手动管理的?

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).
对于稳定性问题,尽早发现是最好的。模糊测试在代码合并之后进行。持续集成在代码推送时触发。运行时安全检查和地址消毒器在代码运行时进行(希望是在开发阶段,在 CI 之前)。

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.
减少这类问题的一种常见方法是确保需要清理的代码始终只运行一次。Zig 被设计为一种简单的语言,没有隐藏的控制流,因此它更喜欢在作用域结束时使用显式的 defer 关键字来运行代码,而不是 C++的隐式~析构函数或 Rust 的隐式 Drop

Language  语言Cleanup  清理
Zigdefer, errdefer
C++~Destructor, &&Move  ~析构函数,&&移动
RustDrop

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:
对于 Zig 代码,究竟应该在何时执行清理代码?如果我们将同一个 *T 传递给多个不同的函数,如何知道它不再可访问、可以清理了?当某些函数在调用后仍需继续引用该内存时,这又是如何工作的?我们目前的做法是混合了:

  • 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)
    arena 生命周期,其可访问范围清晰明确(解析器状态不会逃逸出调用函数,因此 AST 节点在此处是很好的选择)
  • 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.
许多项目选择通过风格指南来回答这类问题。TigerBeetle 的 TigerStyle 是 Zig 语言中的一个例子,而 Google 长达 31,000 词的 C++风格指南则是另一个例子。风格指南面临的挑战在于执行。如何确保风格指南得到遵守?历史上,代码审查是答案,并通过 linter 和静态分析器尽力执行。

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:
在类型系统中明确标注所有权归属的严格风格指南,对 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();

  // ...
}

This is less ergonomic than the Zig we expect:
这不如我们期待的 Zig 那样顺手好用:

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

What about C/C++?  那 C/C++呢?

About 20% of Bun's code is written in C++ and Bun embeds several C/C++ libraries:
Bun 约 20% 的代码是用 C++ 编写的,并且 Bun 嵌入了多个 C/C++ 库:

  • JavaScriptCore, the JavaScript engine that powers Safari
    JavaScriptCore,驱动 Safari 的 JavaScript 引擎
  • uWebSockets & usockets - our HTTP/WebSocket server, and event loop
    uWebSockets 和 usockets —— 我们的 HTTP/WebSocket 服务器及事件循环
  • lshpack & lsquic - HPACK and HTTP/3 libraries
    lshpack 和 lsquic —— HPACK 和 HTTP/3 库
  • BoringSSL, Google's OpenSSL fork
    BoringSSL,Google 基于 OpenSSL 的分支
  • 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.
对于 Bun 来说,用 C++ 而不是 Zig 是一个合理的选择。这样我们就能有构造函数和析构函数,还能删掉大量 extern "C" 包装代码。

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.
然而,我们仍然需要依靠代码审查来强制执行风格指南,即便有了 ASAN,内存损坏和内存泄漏的情况也还是会发生。

Why Rust?  为什么选择 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.
那份清单里的大量 bug 都是释放后使用、重复释放,以及错误路径上“忘记释放”这类问题。在安全 Rust 中,这些都会触发编译错误,配合 Drop 这种类似 RAII 的自动清理机制。编译错误可比风格指南的反馈循环高效多了。

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.
从历史上看,重写代码简直是馊主意。不算注释,Bun 有 535,496 行 Zig 代码。换种语言重写,一个小团队得干一整年,期间还得冻结漏洞修复、安全更新和新功能开发。最稳妥的办法,就是把 Zig 机械式移植到 Rust,尽量不改动行为,直接用我们现有的测试套件来验证 Bun。

Fortunately, Bun's own test suite is written in TypeScript which means it doesn't depend on the runtime's programming language.
好在 Bun 自己的测试套件是用 TypeScript 写的,所以它不依赖运行时的编程语言。

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.
一整年对用户没有影响,这不是我们能考虑的现实选项。所以,通过代码风格来强制执行稳定性修复是我们最好的选择,也是我们在 Bun 的代码库中添加受 Rust 启发的智能指针时的计划。

But honestly, I didn't want to do it. Homegrown smart pointers offer worse ergonomics than Rust, with none of the guarantees.
但说实话,我并不想这么做。自研的智能指针在人体工学方面比 Rust 差远了,而且没有任何保障。

What if, instead, I spend a week testing if Anthropic's new model can rewrite Bun in Rust?
如果换种方式,我花一周时间测试 Anthropic 的新模型能否用 Rust 重写 Bun 呢?

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 代码与原来的 Zig 代码库匹配得这么好。我的看法从“这值得一试”变成了“我要合并这个”。

Claude, rewrite Bun in Rust.
Claude,用 Rust 重写 Bun。

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.
有很多方式会把这件事搞砸。比如,让 Claude 重写 Bun 到 Rust,还说“别出任何错”,然后祈祷它能成功——我可没这么干。

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.
根据我当初把 esbuild 的转译器从 Go 移植到 Zig 来构建 Bun 初始版(那时还没用 LLMs)的经验,一次性全搞定是更好的选择。增量重写会引入临时代码,你希望它们最终被删掉,但在中短期内会非常痛苦。

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?
如何用 Rust 重写 Bun,同时保持与之前相同的架构、性能和功能集,还能利用 Rust 的借用检查器等语言特性?如何确保团队在重写后依然能维护它?

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.
先做一次看起来像是把 Zig 代码转译成 Rust 的重写。在 Bun v1.4 发布后,再逐步重构,减少对 unsafe 的使用,让代码更符合 Rust 的习惯写法。

Those are the only two big questions. Everything else is tactics.
只有这两个大问题。其他的都是战术层面的。

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.
一个 task 会关联一些上下文(比如 Jira 工单、GitHub issue 等)。 result 就是你为修复它而写的代码。代码审查者 review 这些变更,以检查回归问题和正确性。然后你再处理反馈意见。

I rewrote Bun in Rust using about 50 dynamic workflows in Claude Code run continuously over the course of 11 days.
我用 Claude Code 里大约 50 个动态工作流,连续跑了 11 天,用 Rust 把 Bun 整个重写了一遍。

Each dynamic workflow was a loop like this - a workflow for:
每个动态工作流都是这样一个循环——一个用于:

  • Generate a porting guide mapping Zig patterns & types to Rust patterns & types
    生成一份迁移指南,将 Zig 的模式和类型映射到 Rust 的模式和类型
  • Mechanically port every .zig file to a .rs file, matching the PORTING.md and LIFETIMES.tsv
    将每个 .zig 文件机械地移植为 .rs 文件,使其与 PORTING.md 和 LIFETIMES.tsv 匹配
  • Fix every crate's compiler errors
    修复每个 crate 的编译器错误
  • Get subcommands like bun test or bun build to work
    让诸如 bun testbun build 之类的子命令正常工作
  • Get every test in Bun's entire test suite to pass
    让 Bun 整个测试套件里的每个测试都跑通
  • 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.
那 11 天里(包括后续),我几乎全程盯着工作流——逐条人工检查输出,揪出各种问题和 bug,再让 Claude 调整循环逻辑来修修补补。

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?
上百万行代码的 PR 怎么审查?要怎么样才能积攒起足够的信心,去稳妥地合并大量由 LLM 写出来的代码?

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.
对抗性审查会让 Claude(在另一个上下文窗口里)绞尽脑汁地想出这些改动会导致 bug 或无法运行的各种理由。

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.
Claude 也是如此。写代码的那个 Claude 巴不得代码赶紧通过,而负责审查的 Claude 却总想挑刺儿。

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 workflow
✻ CLAUDE CODE · 动态工作流
adversarial review  对抗性审查
3 of the many bugs adversarial review caught before merge
合并前对抗性审查发现的众多 bug 中的 3 个
bug 3 of 3 · the eager default
第三个错误 · 急切默认
claudeimplementer  实现者
its context: the .zig original, the port plan, its own reasoning
它的上下文:.zig 原始文件、移植计划、自身的推理逻辑
claudeadversarial reviewer  对抗性审查者
its context: only the diff. told to assume the code is wrong.
其上下文:仅差异,被告知假设代码有误。
src/css/values/color.rs · compiles clean
src/css/values/color.rs · 编译通过
// each side of color-mix() may omit its percentage;
// color-mix() 的每一侧都可以省略其百分比;
// a missing one defaults to the remainder of the other side
// 缺失的百分比默认为另一侧的剩余部分
let p1 = first  首先.percentage  百分比.unwrap_or  或解包(1.0 - second  .percentage  百分比.unwrap());
unwrap_or evaluates its argument eagerly — second.percentage.unwrap() runs even when first.percentage is Some. So color-mix(in srgb, red 40%, blue), where only the second percentage is omitted, panics inside the argument expression before unwrap_or ever gets to ignore it. unwrap_or_else takes a closure and stays lazy.
unwrap_or 会立即求值其参数——即使 first.percentage 是 Some,second.percentage.unwrap() 也会执行。因此,在 color-mix(in srgb, red 40%, blue) 中,只有第二个百分比被省略,在 unwrap_or 有机会忽略它之前,参数表达式内部就会发生 panic。而 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)
90111846a14 · phase-b2: color.rs gated_full_impl 完全解散(验证:parse_color_mix 解包或急切 panic,默认 CurrentColor 与 transparent)
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.
对抗性审查者确实抓到了三个 bug——每个引用的提交都在标题行里标注了审查归属。这三个 bug 都编译通过了,看起来也都合情合理。审查者是另一个 Claude,它有自己的上下文窗口:只拿到 diff,没有其他信息——实现者的推理过程一概不知——并被要求找出代码错在哪里。代码是从引用的提交中精简而来的,保留了相同的 bug 和相同的修复。

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.
在动手写任何代码之前,我花了大约 3 小时和 Claude 讨论如何把我们 Zig 代码库里的模式尽量贴近地映射到 Rust 上。Claude 把这次讨论整理成了一个 PORTING.md 文档,后来这个文档还上了 Hacker News。

The next question: how do you add Rust lifetimes to code that manually manages memory?
接下来的问题是:如何在手动管理内存的代码中引入 Rust 的生命周期?

That's where I prompted Claude something like this:
这时,我给 Claude 输入了类似这样的提示:

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.
我来启动一个动态工作流,分析代码库中每个结构体字段的合理生命周期。这个工作流会读取每个文件里的每个结构体字段,并追踪控制流。首先,找出那些在 Rust 中生命周期表达复杂的结构体字段,然后为该字段提出一个生命周期方案,接着用两个对抗性审查代理来审核这个生命周期,最后应用所有反馈并序列化到 LIFETIMES.tsv 文件中,供其他 Claude 查看。

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.
随后对 PORTING.mdLIFETIMES.tsv 进行一轮交叉审查,解决所有冲突的建议,再全面复核一遍。我还亲自通读了一遍。

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.
在让 Claude 把全部 1,448 个.zig 文件翻译成.rs 文件之前,我先试了 3 个。针对这 3 个文件,每个文件由 1 位实施者编写新的 .rs 文件,2 位对抗性审查者核对 .rs 文件是否与 .zig 文件的行为一致,并确保遵循 PORTING.mdLIFETIMES.tsv 。之后,1 位修复者将收到的建议落实到位。

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.
我让 Claude 对所有 1448 个.zig 文件循环跑工作流,大概两分钟后,一个 Claude 在提交前跑了 git stash ,另一个跑了 git stash pop ,接着又跑了 git reset HEAD --hard 。它们互相踩脚!要是把每个 Claude 放到单独的工作树里,磁盘空间又不够——Bun 的 git 仓库太大了,而且最终这些改动还得一起编译和查看。

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.
于是我跟 Claude 说,把工作流改一下,让它再也不能执行 git stashgit reset 或者任何不会一次性提交特定文件的 git 命令。 cargo 也不行。总之,所有慢吞吞的命令全给我禁了。

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.
然后,Claude 重新启动了工作流。还真跑起来了!就是速度太慢,所以我把它拆成 4 个工作流分片,每个分片都有独立的工作树(总共 4 个工作树),每个工作树跑 16 个 Claude 实例来提交和推送文件。

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.
得益于所有的并行化处理与前期准备,Claude 在高峰期每分钟能写出约 1300 行代码。每行代码都会经过两个独立的、相互挑剔的审查者(同样是 Claude)来把关,并在提交前进行一轮修复。然而,这些代码居然一个都没跑通!

11 days × 24 hours · PDT
11 天×24 小时·PDT
6,436 commits
1695 commits/hour  每小时 695 次提交
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.
移植分支上每小时的提交数(不含合并),最高峰时段达到 695 次。

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.
注意到时间不一致了吗?我忘了提高运行这个的 EC2 实例的默认 IOPS。仅仅一个缓慢的 grep 命令就足以让磁盘读写冻结好几分钟。

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 写了一个工作流来修复每个编译器错误。我们逐个 crate 地排查。

✻ claude code · dynamic workflow
✻ CLAUDE CODE · 动态工作流
0 errors left
Wed, May 6, 12:42 PM PDT
errors.txt1,610 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
已分配 · 64 个 Claude
worktree 1  工作树 1
worktree 2  工作树 2
worktree 3
worktree 4
1 fixes  1 修复2 review  2 审查1 applies  1 应用
→ commits land per crate
→ 每箱提交的土地
bun_runtime1,162
bun_bundler252
bun_sql67
bun_js_parser40
bun_css10
bun_http7
bun_interchange7
bun_sys6
bun_core4
bun_string3
· phase-d(bun_runtime): Body.rs — allow invalid_reference_casting on opaque FetchHeaders FFI
· phase-d(bun_runtime): allow improper_ctypes on HTMLBundle JsClass extern blocks
· phase-d: final link dedup → BINARY
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.
Phase D 的运行方式是这样的:基于 1610 次真实提交(5 月 6 日 PDT)进行回放——cargo check 把约 16000 条错误写入一个文件,并按 crate 分组;工作流将这些错误分配给 64 个 Claude——在 4 个工作树中循环 16 轮,每个 Claude 负责修复,两个做审查,一个负责应用。每一个“芯片”都是一批真实提交:只有当它落到对应的 crate 上时,计数器才会更新。错误行对应的就是真实提交的主题。

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.
我们的 Zig 代码库原本是一个编译单元(效果上相当于一个 crate)。我想把新的 Rust 代码库拆成 ~100 个 crate,这样 Rust 编译能更快,但得避免循环依赖,同时尽量少改动原本 Zig 的实现。在开始重写 Rust 之前,我立马提了个 PR 来做这件事,结果发现根本不够用。于是我没从头再来,而是又跑了一个工作流,把有循环依赖的代码该放哪儿分类清楚,并全部记下来——接着再跑另一个工作流来做重构。

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.
修复循环依赖后,共暴露了约 16,000 个编译器错误。这对一个人来说是天文数字,但 64 个 Claude 同时上手,倒也不算夸张。

To maximize parallelism, the workflow looped over each crate.
为了最大化并行性,工作流遍历了每个 crate。

  • For each crate, run cargo check, group the output by file and save the errors to a file
    对于每个 crate,运行 cargo check ,按文件分组输出并将错误保存到文件中
  • Fix all the compiler errors within that crate
    修复该 crate 中的所有编译器错误
  • 2 adversarial reviewers for the crate's changes
    2 个对抗性审查者审查 crate 的更改
  • 1 fixer applies the fixes
    1 个修复者应用修复

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.
为了避免 Claude 们互相干扰, cargo check 只在最开始运行,并且和其他运行一样,直到最后都没有 git

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:
Claude 把“让我们把所有 crate 都编译通过”理解成了“把有编译错误的函数都改成 stub”。Claude 还开始添加可疑的长篇解释性注释来记录变通方案,所以我给对抗性审查者加了这条规则,要求驳回:

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.
只需修改一次提示词,几小时后,这些问题就不再出现了。

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.
一旦 cargo check 通过,接下来就是让它编译并运行 bun --version 。它出现了链接器错误。然后,启动时立即崩溃。

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:
接下来的目标是让它能跑起 bun test <file> 。一旦这个搞定了,我们就能开始跑测试了!是时候开始另一个工作流程了,遍历 bun CLI 子命令:

  • Save each failing stacktrace to a file along with its subcommand
    将每个失败的堆栈跟踪及其子命令保存到文件中
  • For each failing stacktrace grouped by subcommand, have 1 Claude fix
    对于按子命令分组的每个失败堆栈跟踪,让 1 个 Claude 进行修复
  • 2 adversarial reviewers  2 个对抗性审查者
  • 1 fixer applies the suggestions
    一个 fixer 应用了这些建议

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.
在代码库中,按文件夹将大约 100 个随机测试文件分片到 4 个工作树之一。对于每个失败的测试,将堆栈跟踪和错误保存到文件中,然后由 1 名实现者提出修复,2 名对抗性审查者进行审查,最后 1 名修复者应用修复。

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.
我们的测试套件里塞满了大量内存泄漏测试,还有一小撮集成测试,跑起来动不动就超过一分钟——比如有个测试会运行 next dev ,然后检查热模块重载能不能连续 100 次捕捉到代码变更。其中好几个测试在调试构建下直接超时了。

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.
我们还有压力测试,会耗尽机器上所有 TCP 套接字的上限,测试读写数 GB 数据到磁盘,以及生成~10k 个进程。

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.
这需要比“请”更强的隔离,因此我们使用了 systemd-run (cgroups)来限制内存和 CPU 使用,并隔离 PID 命名空间。不过,机器还是多次因磁盘空间耗尽而崩溃。

Get the test suite passing in CI
让测试套件在 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.
首次 CI 运行两天后,失败文件列表从 972 个降至 23 个。又过了一天半,Linux 全部变绿——这是第一次让人感觉这个 Rust 重写真的能行。

✻ claude code · dynamic workflow
✻ CLAUDE CODE · 动态工作流
buildkite · the race to green, by platform
BUILDKITE · 平台引领绿色竞赛
Windows finished last · May 11, 6:23 AM PDT
Windows 最后一名完赛 · 5 月 11 日 太平洋夏令时上午 6:23
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
macOS x64 · 2 个分片
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
Linux arm64 · 60 个分片
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
Linux x64 · 60 个分片
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
macOS arm64 · 4 分片
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
Windows x64 · 8 分片
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
Windows arm64 · 8 个分片
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
✓ all 6 platforms green · build #54202 → merged
✓ 全部 6 个平台通过 · 构建 #54202 → 已合并
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.
每个 CI 构建的测试分片,按平台划分,横跨 135 个运行了测试的构建(从 BuildKite 收集到 420 个)。亮绿色:所有分片通过。暗绿色:没有失败,但运行被提前终止(被取代)。红色:至少有一个分片失败。每个通道在全套测试首次通过时打上标记——Linux 的 60 个分片比 Windows 早了几乎一整天全部变绿。各大平台持续红黄闪烁,直到最后的失败测试倒下;最终全绿的构建是#54202。

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.
合并前的其余时间就简单多了。工作流就是不断修复各平台的 CI 测试失败,直到所有测试都通过。还有几个工作流专门处理 Windows 相关的清理工作,比如去重代码、减少不安全使用,以及整体上清理一些代码。

Merging the Rust rewrite
合并 Rust 重写代码

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.
当 Bun 的测试套件在 CI 上所有平台都 100%通过后(我还手动确认了测试确实在跑,没被跳过),我在本地跑了一堆命令来验证,然后就直接按下了合并按钮。

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.
合并到 main 并不算一次版本发布。这时候,我已经有足够的信心推进重写并提交上去,但还没自信到直接发布。

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.
高峰期时,我们同时跑了 4 个这样的工作流,每个工作流放在独立的工作区里,每个工作流又有 16 个 Claude。加起来,一次大约要用 64 个 Claude。

git log · claude/phase-a-port
Git 日志 · Claude/Phase-A-Port
peak: 58 commits in one minute
峰值:一分钟内提交了 58 次
6,502
commits  提交次数
+1,780,453
lines written, rewrites included
编写的行数(含重写)
Thu, May 14, 12:35 AM PDT
· perf(collections): add precomputed-hash raw-entry probe/insert to StringHashMap+53−1
· perf(startup): refresh symbol-ordering-file and add suffix sync helper+186−29
· perf(startup): restore hot-startup symbol-ordering file+2,395−0
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.
全部 6,502 次提交(合并提交除外)已重放。粉色条代表新增代码,青色条代表删除代码。行数统计记录了每一次重写——最终差异为 +1,009,272 行。日志中的提交信息均为真实内容。

0 tests skipped or deleted
没有测试被跳过或删除

11 days (May 3 → merged May 14) · 6,778 commits
历时 11 天(5 月 3 日→5 月 14 日合并)· 6,778 次提交

Platform  平台expect() calls  expect() 调用Tests  测验Files  文件
Debian 13 x641,386,82660,6244,174
macOS 14 arm641,259,95358,8504,175
Windows 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.
合并前,这耗费了 59 亿次未缓存的输入 token、6.9 亿次输出 token 以及 720 亿次已缓存的输入 token 读取——按 API 定价算,大概要 16.5 万美元。要是人工来做,我觉得得让三个对代码库了如指掌的工程师干上差不多一年,而且在这段时间里,我们根本没法去提升 Node.js 兼容性、修 bug、搞安全修复或者上新功能。我们压根儿不会选那条路。现实点的选择就是啥也不干,然后永远接着修文章最开头列的那些 bug。

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).
这是当今技术所能达到的最前沿水平。我使用了 Claude Fable 5 的预发布版本,这是一款 Mythos 级模型。Claude Code 的动态工作流让 64 个 Claude 实例持续运行了 11 天(否则我得自己编写工具才能实现这一点)。

The work continues  工作仍在继续

Since merging the Rust port, we've completed 11 rounds of security review from Claude Code Security and addressed the findings.
自合并 Rust 移植版本以来,我们已完成 Claude Code Security 的 11 轮安全审查,并处理了所有发现的问题。

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.
我们还为 Bun 中的每个解析器添加了 24/7 覆盖引导的模糊测试——包括 JavaScript、TypeScript、JSX、CSS、JSON5、JSONC、TOML、YAML、Markdown、INI、Bun Shell 脚本、semver 范围、.patch 文件和 CSS 颜色。模糊测试器会自动将发现的漏洞发送给 Claude,由其提交包含复现步骤和修复方案的 PR,再由人工审核这些 PR。截至目前,它已执行了 1000 亿次解析器调用,由此产生了约 15 个 PR。

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.
在撰写本文时,Bun 的 Rust 代码中约有 4% 位于 unsafe 块内(在 ~27,000 行 / ~780,000 行代码中散布着约 13,000 个 unsafe 关键字),其中 78% 的块只有一行——要么是来自 C++ 的指针,要么是对某个 C 库的一次调用。我预计随着我们从忠实的 Zig 移植(里面根本搜不到 unsafe 关键字)逐步重构为地道的 Rust,这个比例会慢慢降下来,但我们会继续使用 JavaScriptCore 这类 C 和 C++ 库,所以它始终会比纯 Rust 项目拥有更多的 unsafe

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.
Rust 重写的核心是稳定性,但要一口气完成如此大规模的改动,还指望完全不引入任何回归问题,这几乎是不可能的。

This rewrite introduced 19 known regressions, each of which has been fixed.
这次重写引入了 19 个已知的回归问题,目前均已修复。

Most of the regressions came from code that's syntactically identical in both languages but semantically different.
大多数回归问题都出在那些语法上完全一样、但语义却不同的代码上。

Side effect inside debug_assert!   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 的 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 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
insert_stale 将文件添加到前端开发服务器的热重载图中。在发布构建中,它停止了运行,导致某些使用 React 的 HTML 路由项目在热重载文件失效时,HMR 出现异常: Cannot destructure property 'isLikelyComponentType' of 'k' 。调试构建则正常工作。#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
Bun 的 Zig 辅助函数 reinterpretSlice(u16, bytes) (早于支持切片的内置转换)使用了 @divTrunc ,并且会忽略末尾的奇数个字节。而 bytemuck::cast_slice 则会直接恐慌。当 Blob.text() 遇到一个 UTF-16 字节序标记后跟奇数个字节时,它不再返回字符串,而是导致进程崩溃。我们最终回到了忽略奇数个字节的做法: &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.
在 macOS 和 Linux 上,我们用 ReleaseFast 编译了 Bun 的 Zig 代码,这会移除边界检查。而 Rust 的发布构建保留了它们。

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

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
这一改动将内部化文件名的上限从 840 万降到了 270,272——实际项目确实会触及这个数字——同时让我们从 Zig 移植过来的一个 ptrs[4095] 差一错误变得可触发。Rust 在写入越界时会直接 panic,而不是继续写下去。如果用了 ReleaseSafe (我们只在 Windows 上用了),Zig 在这种情况下也会 panic。#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.
Output.pretty<r><d> 颜色标记重写为 ANSI 转义序列。在 Zig 中, fmtcomptime ,因此参数替换前标记就已消失。Rust 函数没有编译时参数,所以 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 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.
bun update -i 将包名打印为 OSC 8 超链接,并以 ESC \ 结尾。那个反斜杠刚好位于尾部 <r>< 前面,标记解析器会将其吞掉,于是 r 就被原样打印出来。

it should say oxfmt, not oxfmtr
应该是 oxfmt,而不是 oxfmtr

In Rust it has to be a macro: bun_core::pretty!("<r>{}<r>", hyperlink). #30693
在 Rust 中,它必须是一个宏: bun_core::pretty!("<r>{}<r>", hyperlink) 。#30693

Bun is better in Rust
Bun 用 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.
到目前为止,Bun v1.4.0 修复了在 v1.3.14 中可复现的 128 个 bug。这些 bug 包括内存泄漏、崩溃以及帮助文本颜色错误等。

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.
Rust 在语言层面提供了一个强大的内存清理工具: Drop 。一旦实现了 Drop ,每次值离开作用域时, drop 函数就会自动被调用。

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:
在 Zig 中,可以使用 defer 在作用域结束时运行代码:

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:
在 Bun v1.3.14 中,每次构建都会泄漏约 3 MB 内存,且永不停歇——像那些每次请求都要进行打包操作的开发服务器这类工具,最终会耗尽内存。而在 Bun v1.4.0 中,内存使用趋于稳定:

BuildsBun 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

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.
之前用 Zig 尝试实现这个功能时没有被合并,因为缺少类似 Drop 的等价机制,导致对合并的可靠性没有把握。

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.
用 Rust 重写后,最初的变化就让 Windows 上的二进制体积缩小了 3.8 MB,macOS 上缩小了 5.5 MB,Linux 上缩小了 6.8 MB。这主要是因为我们在 Zig 代码中用了太多 comptime

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.
在初步瘦身之后,团队进一步探索了更多缩减二进制体积的机会,比如利用链接器优化(如相同代码折叠)、移除 ICU 中未使用的数据,以及借助 zstd 字典按需延迟解压 libicu 的小部分内容。

Combined with the Rust rewrite, ICU changes, and identical code folding, Bun's binary size shrinks by ~20% on Linux & Windows.
结合 Rust 重写、ICU 变更和相同代码折叠,Bun 在 Linux 和 Windows 上的二进制体积缩小了~20%。

Version  版本Platform  平台Size  大小
Bun v1.4.0 (canary)  Bun v1.4.0(金丝雀版)Windows76 MB
Bun v1.3.14Windows94 MB
Bun v1.4.0 (canary)  Bun v1.4.0 (金丝雀版)Linux70 MB
Bun 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.
Bun 中的 TOML 解析器以及所有其他递归下降解析器(JSON、YAML、JavaScript、TypeScript 等)现在占用的栈空间更少了。

This caused some test failures before merging the Rust rewrite:
这导致在合并 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'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.
Rust 的 LLVM IR 代码生成会在栈变量不再使用时,发出 LLVM 的 llvm.lifetime.startllvm.lifetime.end 内建函数,让 LLVM 能够复用栈空间槽位。这样一来,带有嵌套作用域的大型函数就能显著减少栈空间的使用。

Previously, we manually worked around an open issue by refactoring particularly large functions into many smaller functions.
之前,我们通过将特别大的函数拆分成许多小函数,手动绕开了一个未决问题。

2% - 5% faster
速度提升 2%到 5%

Rust supports cross-language link-time optimization between C/C++ and Rust, which enables inlining across programming languages (how cool is that!!).
Rust 支持 C/C++ 与 Rust 之间的跨语言链接时优化,这就能实现跨语言内联(这简直太酷了!!)。

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.
我们在 Linux x64(EC2,Xeon Platinum 8488C)上对 Bun v1.3.14 和 Bun v1.4.0 进行了基准测试。HTTP 吞吐量通过 oha 对 hello-world 服务器进行测量,应用工作负载则使用 hyperfine 测量。

HTTP throughput (req/s, avg of 3 rounds)
HTTP 吞吐量(请求/秒,3 轮平均)

server  服务器Bun v1.3.14Bun v1.4.0Δ
Bun.serve169.6k177.7k+4.8%
node:http103.8k  10.38 万108.5k  10.85 万+4.5%
Elysia158.9k  15.89 万163.3k  16.33 万+2.8%
express  快递 【注意:仅输出译文,无其他内容】64.5k66.6k+3.2%
fastify91.5k95.9k  9.59 万+4.8%

Apps / CLI (hyperfine)  应用 / 命令行工具 (hyperfine)

workload  工作负载Bun v1.3.14Bun v1.4.0Δ
next build13.62 s  13.62 秒13.03 s  13.03 秒+4.5%
vite build (tsc + vite)
vite 构建 (tsc + vite)
1.69 s  1.69 秒1.65 s  1.65 秒+2.2%
tsc -b --force0.94 s  0.94 秒0.89 s  0.89 秒+4.7%

Production  生产环境

Prisma launched the Prisma Compute public beta on Bun's Rust rewrite.
Prisma 基于 Bun 的 Rust 重写,推出了 Prisma Compute 公开测试版。

"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
"我们遇到了内存泄漏,还有一个虚拟机暂停再恢复后就无法恢复的连接池。等 Rust 重写版出来后,我们用同样的故障模式测试了一遍,它完美搞定了所有问题。" —— 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 v2.1.181(6 月 17 日发布)及之后的版本都用了 Bun 的 Rust 移植版。Linux 上启动快了 10%,但除此之外几乎没人注意到。平淡无奇才是好事。

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:
Bun v1.3.14 是最后一个用 Zig 编写的 Bun 版本。Bun v1.4.0 将是第一个用 Rust 编写的 Bun 版本。目前已在 canary 版本中可用——如果发现任何问题,请及时反馈:

bun upgrade --canary

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:
对我个人和团队来说,新的 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;
        }

        // ...
    }

    // ...
}

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.
任何能看懂原始 Zig 代码的人,也都能看懂机械翻译过来的 Rust 代码。我审查了最初的 Rust 重写 PR,检查了对抗性代码审查代理是否正确捕捉到了 Zig 代码与 Rust 代码之间的差异,确保它们遵循了移植指南和生命周期指南,同时我自己也手动对照着 Zig 和 Rust 代码逐行阅读了大量内容。

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.
Bun v1.4 让 Bun 更快、更小、内存占用更少,并为团队提供了极其强大的工具,用于系统性地提升后续稳定性:Rust 的借用检查器、Miri(在 CI 中运行越来越多的代码)、LeakSanitizer,以及针对解析器的 24/7 覆盖率引导模糊测试。虽然还有更多代码需要重构,但一切已经开了个好头。

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.
这次用 Rust 重写,原本需要一个对整个代码库了如指掌的工程师团队花一年时间才能完成。而仅靠一位工程师使用 Fable 并密切监控 Claude Code,我们从零开始,在 11 天内就让所有平台上的测试套件全部通过。

One engineer can do a lot more today than a year ago.
如今,一位工程师能做的事情比一年前多得多。