Source-PR: oven-sh/bun#30412 — "Rewrite Bun in Rust"
X-Method: reasoned read of PR body, PORTING.md, commit log, comments
X-Date: 2026-05-14
Bun ported 1M lines from Zig to Rust in 5 days.
Here's what made it tractable.
Not "AI is magic." A specific combination of pre-existing properties of Bun's codebase, a rigorously-disciplined porting guide, and the Rust compiler used as a verification harness for agent-generated code. The transferable parts are real. The non-transferable parts are also real — and worth being honest about.
The "8 days" framing in circulation isn't supported by the PR's own timestamps — createdAt
2026-05-08, mergedAt 2026-05-14. The "1M LOC" headline is gross additions; in the merged tree,
.rs files sit next to the original .zig (e.g. src/bun.js.rs
+ src/bun.js.zig). The Zig isn't deleted — it's the reference the Rust is diffed against.
Phase split + per-file parallelism + Rust compiler as the verifier.
Phase A writes .rs
files next to every .zig —
files don't have to compile yet, just be diff-able against the original. Phase B fixes
compilation, crate by crate, with the Rust borrow checker acting as the verifier that the
agent's translation is memory-safe. The work is parallel because the
output is read-only at first.
§1 The pre-existing levers
What was already true about Bun's codebase that made this even possible.
Bun is an unusual codebase. Several properties that are taken as cost during normal development paid back enormously when the codebase was treated as an input to an automated translator. None of these are "AI" levers; they are architectural levers that just happen to make the AI work possible.
No async runtime to design around
Bun's Zig is a hand-rolled event loop over uws/JSC — there is no async/await in the source language and PORTING.md hard-bans it in the destination too. The single hardest design decision in any Rust port — pick an async strategy — was unavailable because there was no async strategy to pick.
PORTING.md: "Everything is callbacks + state machines, same as the Zig."
Tiny dependency surface
PORTING.md explicitly bans tokio, hyper, rayon, std::fs, std::net, std::process. They can do this because the Zig didn't use the equivalents either — Bun owns its syscalls. Most server codebases have 200+ transitive deps. This one effectively has none at runtime.
An exhaustive pre-existing test suite
Bun's value proposition is Node.js + Web API compatibility. Its tests already cover end-to-end behavior on every platform — they were built to imitate someone else's runtime, so they're unusually comprehensive. This is the verification feedback loop. You cannot do this without it.
A pre-computed lifetime analysis
Buried in PORTING.md: every pointer field in every Zig struct was pre-classified into a TSV (docs/LIFETIMES.tsv) with columns for ownership category (OWNED/SHARED/INTRUSIVE/FFI) and target Rust type (Box<T>, Rc/Arc<T>, *const T, …). The agent didn't infer lifetimes — it consulted them. The hallucination space collapsed.
"Architecture stays the same"
The PR body is explicit: "The codebase is otherwise largely the same. The same architecture, the same data structures." This isn't a rewrite — it's a typographic transliteration that the compiler then validates. The semantic surface is fixed; only the language changes.
§2 Phase A / Phase B — the move that unlocks parallelism
Most attempts at large rewrites fail because the agent is asked to produce code that compiles end-to-end on first pass. That couples every file to every other file and the loop serializes. The Bun split is to let Phase A produce something that doesn't compile, but is structurally diff-able against the source. Compilation becomes a separate, batchable pass.
Write .rs files next to every .zig
Per-file, parallel, no cross-file compile requirement.
- Match Zig structure exactly: same fn names (snake_case), field order, control flow.
- Phase B reviewers diff
.zig↔.rsside-by-side. - Acronym normalization rules (
toAPI → to_api) so two agents agree on naming. - Borrow-check reshapes allowed, but tagged
// PORT NOTE: reshaped for borrowck.
Wire Cargo, fix compilation crate-by-crate
Sequential per crate (bun_str, bun_sys, bun_jsc, bun_alloc, …), parallel within a crate.
- Grep
// PERF(port):markers — convert Zig perf idioms to idiomatic Rust. - Grep
// TODO(port):markers — escalate to humans. - CI failure spawns a branch named for the run number (
claude/ci-auto-fix-53852), agent fixes, merges back. - Stabilize flaky tests in dedicated branches (
claude/flaky-stabilize-v2).
§3 Three markers that make Phase B grep-able
PORTING.md's most under-appreciated trick: Phase A's output is structured. Every
uncertainty, every perf concession, every unsafe block carries a labeled comment
marker. Phase B doesn't have to find the work; it just greps for the marker. This is
what makes the work parallel and auditable.
Agent flagged something it can't translate confidently. Flagging is better than wrong code. Phase B humans escalate.
Zig perf idiom (appendAssumeCapacity, arena bulk-free, comptime monomorphization) downgraded to idiomatic Rust. Phase B re-optimizes.
Every unsafe block mirrors the Zig invariant. Human-greppable audit trail.
Each .rs file also ends with a PORT STATUS trailer carrying
confidence: high | medium | low and a TODO count. Phase B prioritizes review by
filtering on confidence and TODO density. Structured agent output is the unlock for
auditable bulk work.
§4 The Rust compiler as a verification harness
Not for end users. For the agent.
The load-bearing technical claim of the whole rewrite: the borrow checker is being used to validate that the AI's translation is memory-safe. This is the part everyone misses when they talk about "AI wrote a million lines."
three places where Rust's type system mechanically rejects translation errors
*T→Box<T> / Rc<T> / *const Tdefer x.deinit()→impl Droperrdefer→? auto-dropsThis is the *real* AI productivity story. The agent is not "trusted to write a million lines." The agent is *trusted to draft*, and a much stricter compiler than Zig's then refuses to accept drafts that violate ownership rules. The translation is checked mechanically before any human reads it.
§5 The verification loop, in branch names
The commit log on the PR is the most direct evidence of how the agent was run. Branches are named after the agent's job, not the human's feature:
claude/phase-a-port the per-file Zig→Rust draft branchclaude/ci-auto-fix-N one branch per failing CI run; agent fixes, mergesclaude/bench-until-green perf regression hunting against a baselineclaude/code-dedup post-Phase-A cleanup of duplicated helpers§6 Transferable vs not — the honest read
✓ transferable to most teams
- Phase A / Phase B split. Don't ask the agent to produce something that compiles end-to-end on the first pass. Let it produce diff-able output, then sweep for compilation.
- Structured agent output.
// TODO(port):,// PERF(port):,PORT STATUStrailers — anything you can grep over to triage. - Pre-computed analysis tables. The lifetime TSV pattern — cheap to build, dramatically reduces hallucination space. Generalizes to "any time you can pre-analyze the source so the agent consults rather than infers."
- "Translate, don't redesign" as a hard rule when source and target differ but architecture should not. The rule is what makes diff review tractable.
- Per-file parallelism, not per-feature. One agent per file removes serialization on review queues.
✗ not transferable without Bun's specific properties
- The absence of async. Most server codebases cannot port to Rust without picking an async strategy — and that decision is the migration. Bun sidesteps it because they were never using one.
- Near-zero dependency surface. Banning
tokio,hyper, andrayonis laughable for most apps. Most apps' dependency graph is the migration problem. - An exhaustive end-to-end test suite that was built independently of the rewrite. If you don't have this, you can't safely AI-translate anything non-trivial. This is the cost most teams haven't paid.
- One person with full architectural authority. PR has zero reviews; head branch is
claude/phase-a-portmerged by the author. The top community comment (45 reactions): "1 million of unverified lines of code merged into a runtime used by millions of dependents without any consultation." Most teams cannot ship at this trust level. - License to modify the test harness when failures look flaky. The maintainer accepted "fix the tests when they're flaky" — useful for shipping speed, but it shifts trust from the harness onto the agent.
§7 What this actually proves
Strip away the headline numbers and the lesson is narrower and more useful than "AI did a port in a week."
The combination that worked: (a) a target language with a stricter type system than the source, so the compiler can mechanically reject the agent's worst mistakes; (b) an architectural rule that the rewrite is not allowed to redesign anything, so diff review stays tractable; (c) per-file structured output with grep-able markers so cleanup is queryable rather than archaeological; (d) an existing test suite that's the green-bar, not "we'll write tests as we go." Without (a) and (d) this pattern cannot replicate. With them, it probably can.
The non-obvious move is treating the AI not as the engineer, but as a drafter: the output is verified by other systems (compiler, test suite, lifetime tables, grep-able markers). The AI's job is bulk transliteration with disclosure. Everything else is a pre-existing rigorous engineering substrate that most codebases haven't paid the cost to build.
- PR: oven-sh/bun#30412 — body, commits, comments, timestamps
- PORTING.md: oven-sh/bun/docs/PORTING.md (the agent's porting guide)
- Repo tree post-merge:
.rsand.zigco-exist; crate-style dirsbun_str,bun_alloc,bun_jsc, … - Branch naming:
claude/phase-a-port,claude/ci-auto-fix-N,claude/bench-until-green,claude/code-dedup,claude/flaky-stabilize-v2