A codebase as a graph
The C++ is parsed into a typed semantic graph of ~15k nodes — one node per record, method, free function, macro or enum — with declares, calls, type-uses and inherits edges. The graph, not the file tree, is the unit of work.
A faithful, line-for-line translation of Luau — Roblox's typed Lua — from C++17 to Rust. Not bindings. Not a reimplementation. The actual compiler, virtual machine, and type checker, ported to safe-by-default Rust — and running right here in your browser.
Live in your browser
The compiler, VM and type checker are compiled to WebAssembly and run client-side — there is no server. Type-check runs the analyzer for precise, line-accurate diagnostics (it runs automatically as you type). Run executes the script on the Rust VM and captures its output.
Loading the Luau engine (WebAssembly)…
Type-check uses the path that is fully precise on this
wasm32-unknown-unknown build — it reports the exact line and message,
and re-runs automatically a moment after you stop typing (click a
Lnn chip to jump to that line). Run
executes on the VM; press Ctrl/⌘ + Enter to run.
The type-error example shows the checker catching a real mistake before you Run.
The method
Automated C++→Rust translation is an open problem. The published state of the art (RustMap, EvoC2Rust, DARPA TRACTOR) tops out around ~13k lines of C at ~87% equivalence with human patching. luaur is ~205k lines of production C++17 — lexer, parser, bytecode compiler, register VM, a full bidirectional type checker, native code generation, CLIs — translated as a convergent system.
The C++ is parsed into a typed semantic graph of ~15k nodes — one node per record, method, free function, macro or enum — with declares, calls, type-uses and inherits edges. The graph, not the file tree, is the unit of work.
Each node becomes its own Rust file with an explicit use
header (16,185 files in all). Nodes are topo-sorted and translated only
once their dependencies exist, so each prompt sees the real
translated types it depends on — not guesses.
A translated node must compile in-tree and pass a drift check — it may not
silently drop declarations, fake green by deleting mod entries,
or stub out logic — before it lands. Failures are reverted and re-queued.
Equivalence is proven twice: Luau's own doctest suite,
ported to 5,347 Rust #[test]s, plus a byte-exact
bytecode differential — programs compiled by C++ Luau, executed on
the Rust VM, producing identical results.
The bulk of independent nodes were translated by a round-robin roster of cheap models; the hard tail — mutually-recursive type clusters, the GC, the VM execution core, the type-inference engine — by stronger agents. Ranking those models reveals the central lesson of the project: a drift gate proves a translation builds and didn't fake green — it does not prove it's semantically right.
| model | landed | one-shot accept | survival |
|---|---|---|---|
| qwen3-coder-next | 3,602 | 76.9% | 24.6% |
| gemini-3-flash-preview | 2,774 | 81.2% | 61.8% |
| gpt-5.4-nano | 1,593 | 79.1% | 42.1% |
| gemini-3.1-flash-lite | 1,133 | 82.3% | 51.9% |
| gemini-3.5-flash | 288 | 68.6% | 73.3% |
| deepseek-v4-flash | 105 | 63.8% | 57.1% |
| mimo-v2.5 | 57 | 80.7% | 63.2% |
The volume leader (qwen, 77% one-shot) has the lowest survival — it produced the most plausible-but-wrong code, concentrated in the hardest subsystems. The real quality leader is gemini-3-flash-preview. The takeaway generalizes: rank translation models by survival against a real test oracle, never by “it compiled.”
! vs !. The bytecode differential
found 6 runtime bugs in a VM that already “passed.” The dominant class: C's
logical !x mis-ported as Rust's bitwise !x != 0
(which is always true). Invisible to a reviewer; obvious to a diff.
visit(ty)
where the C++ recursed via traverse(ty) — silent, because it
type-checks and “mostly” works.
unsigned tt:4; int next:28
into one word; translated naively, the node grew past its 32-byte budget and the
hash-table layout drifted. Fixed with an explicit packed accessor.
Published layers
luaur is published as independent crates so you can depend on exactly the layer you need.
luaur | Start here. Umbrella: the mlua-style API + compile/eval/check helpers, re-exporting every layer |
luaur-rt | The safe, ergonomic mlua-style API — Lua, create_function, UserData, FromLua/IntoLua |
luaur-common | Foundations: SmallVector, DenseHashMap, Variant, FastFlags |
luaur-ast | Lexer, parser, AST |
luaur-bytecode | Bytecode format + builder |
luaur-compiler | Luau source → bytecode compiler |
luaur-code-gen | Native code generation (A64 / X64) |
luaur-vm | The register VM + standard library |
luaur-analysis | Type checker / type inference |
luaur-config | .luaurc configuration |
luaur-require | Require-by-string module resolution |
luaur-web | wasm32 bindings — run / type-check Luau in the browser |
Plus the CLI tools: luaur-repl-cli (REPL), luaur-analyze-cli
(standalone type checker), and luaur-ast-cli /
luaur-compile-cli / luaur-bytecode-cli /
luaur-reduce-cli.
The honest version
Why this exists, why it isn't a fork of the C++, and where to send a contribution.
Two honest reasons. First, the translation itself was the interesting part — taking ~205k lines of production C++17 and porting it, line for line, as a convergent system that the upstream test suite actually validates is a hard, unsolved problem, and that process was the point (see How it was built).
Second, a practical need: I wanted a Luau that runs cleanly on
wasm32-unknown-unknown — the target the Rust ecosystem reaches
for most often — without dragging in C, emscripten, or a separate toolchain.
A native-Rust port gets you that for free.
Because for language contributions you should. Upstream Luau is the source of truth — it's where new features, fixes and the spec live. luaur is a translation of it, not a competing implementation: if luaur ever attracts real interest, it will most likely track Luau upstream rather than diverge from it.
So the recommendation is genuinely: if you want to improve Luau, contribute to the C++. Those improvements flow downstream here. luaur is the place to care about the Rust/wasm packaging and the translation machinery, not the place to land new language semantics.
The headline 1.96× body-to-body ratio compares like for like — function bodies against function bodies, not counting blank lines or comments — so it isn't a measurement artifact. The Rust really is about twice the lines, and that's expected for a faithful port:
match/Option
handling, bounds-checked accessors, packed-field accessors.
In other words, the extra lines buy memory safety and a structure that a test oracle can hold honest. They are not bloat — they are the cost of the same logic, written explicitly.
It passes Luau's own ported unit suite (5,347 tests), runs all 293 conformance scripts byte-identically, and matches the C++ VM on a byte-exact bytecode differential. That's a strong correctness bar — but luaur is a research-grade translation, best treated as such. For anything load-bearing, pin a version and run your own tests, exactly as you would adopting any new runtime.