ANALYSIS /bun-rust-rewrite
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
<htmlbin> — case study
how it actually happened

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.

5d 10h
opened → merged
+1,009,257
lines added · 2,188 files
3–8 MB
binary size delta (smaller)
0
reviews on the PR
Two factual corrections worth flagging

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.

tl;dr — the actual playbook

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.

1

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.

PR body: "No async rust."
PORTING.md: "Everything is callbacks + state machines, same as the Zig."
2

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.

3

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.

PR body: "It passes Bun's pre-existing test suite on all platforms."
4

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.

5

"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.

phase A · faithful translation

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.rs side-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.
key rule: "translate, don't redesign"
phase B · compile + bench-until-green

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).
key rule: green-bar = pre-existing test suite passes on all platforms

§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.

// TODO(port): <reason>

Agent flagged something it can't translate confidently. Flagging is better than wrong code. Phase B humans escalate.

// PERF(port): <zig idiom>

Zig perf idiom (appendAssumeCapacity, arena bulk-free, comptime monomorphization) downgraded to idiomatic Rust. Phase B re-optimizes.

// SAFETY: <invariant>

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

ownership classification
Zig *TBox<T> / Rc<T> / *const T
if agent miscategorizes OWNED as SHARED, borrowck rejects subsequent double-mutation code
cleanup paths
Zig defer x.deinit()impl Drop
if agent forgets to free, code leaks visibly (caught several pre-existing leaks)
error paths
Zig errdefer? auto-drops
collapses an entire class of "did I add the right cleanup?" bugs
PR body, paraphrased: we now have compiler-assisted tools for catching & preventing memory bugs, which have cost the team an enormous amount of development & debugging time over the years.

This 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 .zig → .rs (no compile) parallel, per-file phase B — main cargo compile, crate-by-crate test suite is the green-bar CI (robobun, BuildKite) pre-existing test suite build #54202 = green claude/ci-auto-fix-53852 per-failing-run branch claude/flaky-stabilize-v2 timing/RSS artifacts claude/bench-until-green perf regressions hunt claude/code-dedup −2,638 LOC after Phase A red? merge fixes back into main · iterate · until CI green no human-named feature branches in the loop · agent is the named owner
claude/phase-a-port the per-file Zig→Rust draft branch
claude/ci-auto-fix-N one branch per failing CI run; agent fixes, merges
claude/bench-until-green perf regression hunting against a baseline
claude/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 STATUS trailers — 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, and rayon is 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-port merged 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.

The headline isn't "AI is ready." It's: if you've already invested in a thorough test suite, sync-only architecture, minimal dependency surface, and explicit ownership semantics, then AI is a force multiplier for bulk transliteration. Those are big ifs.
Sources consulted