Rust · WebAssembly · runs entirely in your browser

Luau,
translated to Rust.

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.

5,347
ported unit tests pass
293/293
conformance scripts, byte-identical
205k 420k
lines C++17 → Rust
1.96×*
body-to-body size ratio

Live in your browser

Playground

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.

source.luau
diagnostics & output loading wasm…
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.

Embed it

Use it from Rust

On top of the faithful engine sits luaur-rt — a safe, ergonomic API whose interface deliberately mirrors mlua, so embedders are immediately at home. Expose Rust functions and types to Luau; a returned Err — or even a panic! — surfaces as a catchable Lua error, not a crash. It is pure Rust: no C toolchain, no FFI, runs anywhere Rust does — including wasm32-unknown-unknown.

main.rs
// Expose a Rust closure to Luau, then run a script that calls it.
use luaur::{Lua, UserData, UserDataMethods};

let lua = Lua::new();
lua.globals().set("add",
    lua.create_function(|_, (a, b): (i64, i64)| Ok(a + b))?)?;
let sum: i64 = lua.load("return add(2, 3)").eval()?;   // 5

// Expose a Rust type — with methods and metamethods:
struct Vec2 { x: f64, y: f64 }
impl UserData for Vec2 {
    fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
        m.add_method("magnitude", |_, v, ()| {
            Ok((v.x * v.x + v.y * v.y).sqrt())
        });
    }
}
lua.globals().set("v", lua.create_userdata(Vec2 { x: 3.0, y: 4.0 })?)?;
let m: f64 = lua.load("return v:magnitude()").eval()?;   // 5.0

The method

How it was built

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.

01

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.

02

One item per file, bottom-up

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.

03

Gate every landing

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.

04

Two oracles, not spot checks

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.

“Compiling is not correct.”

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.

modellandedone-shot acceptsurvival
qwen3-coder-next3,60276.9%24.6%
gemini-3-flash-preview2,77481.2%61.8%
gpt-5.4-nano1,59379.1%42.1%
gemini-3.1-flash-lite1,13382.3%51.9%
gemini-3.5-flash28868.6%73.3%
deepseek-v4-flash10563.8%57.1%
mimo-v2.55780.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.”

The bugs that make the point

  • ! 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.
  • traverse-vs-visit. Calling single-node visit(ty) where the C++ recursed via traverse(ty) — silent, because it type-checks and “mostly” works.
  • NP-hard subtyping false-positive. A graph-coloring subtype query spuriously tripped the type-inference iteration limit. Two strong models stalled; instrumenting our code found a unifier counter not reset per top-level unification. Rigorous measurement beat reasoning.
  • Bitfield packing. Luau packs 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

The crates

luaur is published as independent crates so you can depend on exactly the layer you need.

luaurStart here. Umbrella: the mlua-style API + compile/eval/check helpers, re-exporting every layer
luaur-rtThe safe, ergonomic mlua-style APILua, create_function, UserData, FromLua/IntoLua
luaur-commonFoundations: SmallVector, DenseHashMap, Variant, FastFlags
luaur-astLexer, parser, AST
luaur-bytecodeBytecode format + builder
luaur-compilerLuau source → bytecode compiler
luaur-code-genNative code generation (A64 / X64)
luaur-vmThe register VM + standard library
luaur-analysisType checker / type inference
luaur-config.luaurc configuration
luaur-requireRequire-by-string module resolution
luaur-webwasm32 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

FAQ

Why this exists, why it isn't a fork of the C++, and where to send a contribution.

Why was luaur built?

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.

Why not just contribute to the C++ version?

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.

Why is the Rust ~2× the size of the C++? *

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:

  • C++ leans on terse idioms (dense one-liners, macros, implicit conversions, pointer arithmetic) that expand into several explicit, safe-by-default Rust lines — pattern matches, explicit match/Option handling, bounds-checked accessors, packed-field accessors.
  • Constructs the C++ packs into one statement (initializer lists, ternary chains, RAII tricks) often become a few readable lines in Rust rather than one clever one.
  • Faithfulness was the goal, not golfing: the port mirrors the original's structure 1:1 so it can be validated against Luau's own tests — it was never optimized for line count.

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.

Is it production-ready?

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.