v12
An embeddable JavaScript engine written in pure Rust from scratch. No V8, no QuickJS, no compromise on speed.
What it is
Most options for running JavaScript in Rust are bindings to massive C++ codebases like V8 or slow AST interpreters. I wanted to build a native-Rust engine that cares about execution speed from day one: register-based bytecode, NaN-boxed values, hidden classes, and a JIT compiler.
The goal is an embeddable engine that starts instantly, uses little memory, and runs real programs correctly without pulling in hundreds of megabytes of C++ toolchains.
Architecture & pipeline
The workspace separates parsing, bytecode execution, memory, and JIT compilation into clean crates:
v12-bytecode&v12-bccompiler: Compiles the oxc AST into a 32-bit register-based instruction set with constant pools and exception tables.v12-heap: NaN-boxedJsValuein 64-bit words, hidden classes with transition trees, and a handle-based arena allocator.v12-interp: Tier-0 iterative bytecode interpreter with generator suspension and tier-up profiling.v12-jit-baseline&v12-codegen: Tier-1 template JIT using Cranelift to compile hot bytecode into machine code.v12-api: Embedder facade with a single-threadedContextto run scripts, bind Rust closures, and call JS functions.
flowchart LR
JS["JavaScript Source"] --> PARSER["oxc Parser & AST"]
PARSER --> COMPILER["v12-bccompiler"]
COMPILER --> BC["v12-bytecode (Register ISA)"]
BC --> INTERP["Tier-0 Interpreter"]
BC --> JIT["Tier-1 Cranelift Template JIT"]
INTERP <--> HEAP["Handle-based Heap & Mark-Sweep GC"]
JIT <--> HEAP
HEAP --> SHAPES["Hidden Classes & Inline Caches"] Hidden classes and inline caches sound like black magic until you write the transition tree yourself.
Key decisions
NaN-boxed values. Every JS value (numbers, booleans, null, undefined, object handles) fits into a single 64-bit word. Primitives never hit the heap.
Hidden classes & inline caches. Tracking object shapes dynamically lets the interpreter cache property offsets, turning hash map lookups into single pointer additions.
Handle-based heap with mark-sweep GC. Objects live in arenas addressed by 32-bit handles. This avoids scanning the native Rust stack and prevents pointers from invalidating under running code.
The hard parts
Async/await and generators without stack unwinding. When JS awaits a Promise or yields, you have to freeze the entire register frame, save the scope chain, and resume later without blocking the thread.
The Test262 suite. JavaScript has endless corner cases. Getting 7,500+ official Test262 conformance tests to pass meant debugging type coercions, temporal dead zones, and prototype chain edge cases until 2am.
What I learned
Writing a JS engine connects compiler design, memory management, and spec debugging. Passing Test262 tests taught me that conformance and raw speed constantly fight each other in dynamic language runtimes.