Ladybird Adopts Rust, with Help from AI
Ladybird's decision to port LibJS to Rust using AI-assisted translation produced 25,000 lines of correct, intentionally non-idiomatic Rust in two weeks with zero test regressions across 65,000 tests.

Ladybird spent over a year evaluating memory-safe alternatives to C++ and rejected both Swift and Rust before adopting Rust anyway. The rejections were not irrational — the web platform’s object model is a genuinely bad fit for Rust’s ownership system, and that problem has not gone away. The post actually answers a harder question: what do you do when the alternatives are worse than the fit is bad?
Source
Ladybird adopts Rust, with help from AI
By: Andreas Kling, Ladybird
Published: Feb 23 2026
Type: Engineering article
Saved because this is one of the more honest “we changed our minds” posts in systems programming — it names exactly why Rust was rejected in 2024, names what changed (nothing technical, just the cost of standing still), and describes an AI-assisted porting methodology that is worth understanding independently of the language decision.
Choosing the language that is least wrong
The web platform’s object model is not a clean fit for any memory-safe language. It was designed in the 1990s around garbage collection, deep inheritance hierarchies, and mutable shared state. The JavaScript specification alone describes hundreds of abstract operations that assume garbage-collected objects with reference cycles and prototype chains that can change at runtime. Rust’s ownership model assumes the opposite: every value has a single owner, lifetimes are tracked at compile time, and reference cycles require explicit Rc<RefCell<>> scaffolding that becomes unwieldy at browser-engine scale. When Ladybird first evaluated Rust in 2024, this mismatch was the deciding factor — not ideology, but the concrete engineering cost of forcing a 1990s OOP object graph through a 2015 ownership system. That cost is real and has not changed.
Swift was the first alternative evaluated, and it failed on different grounds. Swift’s C++ interop had not reached the maturity needed for a codebase with the depth of Ladybird’s, and outside the Apple ecosystem, Swift’s platform support remained thin enough to make it effectively a single-vendor bet. A browser engine that achieves memory safety only on macOS has not solved the problem; it has relocated it.
What changed between 2024 and 2026 was not the quality of Rust’s OOP story. What changed was the cost of standing still. C++ is fast and expressive, and it does not prevent use-after-free, buffer overflows, or data races. Firefox has been integrating Rust into its codebase for years. Chromium has begun doing the same. The ecosystem of systems programmers who know Rust has grown to the point where Ladybird’s contributors were already familiar with it. Refusing Rust at this point was no longer a technical judgment — it was an organizational choice to narrow the contributor pool and defer the memory safety problem indefinitely. The pragmatic case finally outweighed the fit problem.
The porting strategy resolves the technical objection without pretending the mismatch doesn’t exist. The first target was LibJS — specifically the JavaScript engine’s lexer, parser, AST, and bytecode generator. These components are relatively self-contained: text goes in, data structures come out. They do not participate deeply in the garbage-collected heap or the DOM’s inheritance hierarchy. They have comprehensive test coverage through test262, the specification conformance suite covering 52,898 tests. That combination of bounded scope, clean interfaces, and exhaustive tests made LibJS the right entry point without forcing Rust’s ownership model onto the parts of the engine where it would genuinely fight back. The garbage-collected object model remains in C++ for now. The parts that can be safe get safe; the parts that cannot wait.
The AI-assisted translation is worth understanding on its own terms. Andreas Kling describes it as human-directed, not autonomous: hundreds of small prompts, explicit decisions about what to port and in what order, and multiple adversarial review passes using different models to surface mistakes. The result was roughly 25,000 lines of Rust in about two weeks — work Kling estimates would have taken multiple months by hand. The Rust output is intentionally non-idiomatic. It mimics C++ register allocation patterns because the correctness requirement from the start was byte-for-byte identical output from both the C++ and Rust pipelines running simultaneously in lockstep. Idiomatic style was explicitly deferred. The plan is to clean up the Rust code after the C++ pipeline is retired, not before. Correctness first, cleanliness later — and the test suite is what makes that sequencing safe rather than aspirational.
The zero-regression result across all 52,898 test262 tests and all 12,461 Ladybird regression tests is the headline. But the methodology that produced it is the harder achievement. Kling ran both pipelines simultaneously on real web traffic and verified that every AST produced by the Rust parser was identical to the C++ one, and every bytecode generated by the Rust compiler matched the C++ compiler’s output. The correctness bar was not “passes the same tests in isolation” — it was “produces identical output on the same inputs as the thing it is replacing.” That is a stricter standard, and it is only possible because both implementations were running at the same time on the same inputs. The moment the C++ pipeline is retired, that level of verification becomes unavailable.
My take
The AI-assisted translation will get most of the attention, but the more durable insight is the sequencing decision. Kling chose LibJS because it had clean interfaces and exhaustive test coverage, not because it was the simplest component. Exhaustive tests made lockstep verification possible, and lockstep verification made the “translated from C++” aesthetic a feature rather than a shortcut. Non-idiomatic Rust that provably produces identical output is more trustworthy than idiomatic Rust that passes the same tests separately — you can always improve the style later, but you can only establish byte-for-byte equivalence while both systems are still running.
Where this is useful
- Choosing a porting entry point in a large C++ codebase: prioritize subsystems with clean interfaces and strong test coverage over ones that seem isolated but lack verification
- Designing AI-assisted translation workflows: human direction and multiple adversarial review passes are what separate a correct translation from a plausible-looking one
- Justifying non-idiomatic output in a first-pass port: “identical behavior verified in lockstep” is stronger evidence of correctness than “idiomatic style that happens to pass the same tests”
- Not applicable when the target subsystem is deeply entangled with a garbage-collected object graph or deep inheritance hierarchies — port the leaf nodes with clean interfaces first
Apply this: Before porting any subsystem, write down the exact correctness bar you will use to verify the port is complete — if you cannot state it precisely, the test coverage is not sufficient and the port will rely on confidence rather than evidence.
What I want to remember
Remember: Non-idiomatic code that provably produces identical output in lockstep is more trustworthy than idiomatic code that passes the same tests in isolation — clean style can be added later, but byte-for-byte equivalence can only be established while both pipelines are still running.