A JavaScript engine written in Rust
Zipp is a from-scratch ECMAScript engine: a hand-written parser, a register bytecode compiler, an explicit-frame virtual machine, native JIT tiers and a generational garbage collector, all in Rust. The same engine runs as a native CLI, as an embeddable Rust library, and as a WebAssembly module in the browser.
What Zipp is
ZIPP, the Zippy Interpreter for Polyglot Programming, is a portable polyglot runtime: one Rust register-bytecode VM that several languages compile to, so programs in different languages share one runtime, one sandbox and one set of embeddings, from the native CLI and a Rust library to WebAssembly in browsers, Node, Deno and Bun. JavaScript and Python are the languages it supports today. JavaScript is ES2025, passing 99.999% of the pinned Test262 corpus unmodified; Python is an experimental subset with a bundled torch subset. The design is for more languages: docs/LANGUAGES.md describes how one plugs in. This page is about the JavaScript engine.
Zipp began on 29 May 2026 as a small typed language and became a JavaScript engine the next day. Since then it has grown into roughly 424,000 lines of Rust in the zipp-vm crate, tests included, plus a CLI, a WebAssembly embedding, a hardened sandbox runner and an experimental Python frontend that compiles to the same bytecode.
It is not a binding to V8, SpiderMonkey or JavaScriptCore, and it does not wrap QuickJS. Every stage from source text to native machine code is Zipp's own, with one vendored exception: a fork of the regress regular-expression engine. The Python frontend's lexer and parser are Zipp's own as well.
- Test262 executions passing, unmodified upstream
- 96,229 / 96,230
- Zipp/Node wall time, all 30 native benchmark rows
- 0.703×
- Median native process launch
- 8.3 ms
- WebAssembly engine on the wire (Brotli)
- 1.20 MB
One documented expected failure. Corpus pinned to tc39/test262 d53ebc8.
Geometric mean, 15 October 2026 capture. Lower is faster.
Node 33.0 ms, Bun 50.8 ms, Deno 79.0 ms on the same machine.
JavaScript-only engine file of v0.0.29, bindings included, 4.8 MB raw. ZIPP Lite: 0.75 MB.
Language coverage
Zipp implements the ES2015 through ES2025 language and runtime, and passes all but one execution of the pinned Test262 corpus, including the staging directory. The feature list is the boring kind of complete:
- Classes with private elements, static blocks, computed keys and all eight decorator kinds.
- Destructuring, spread and rest, generators, async generators, promises, iterator helpers, and
using/await usingexplicit resource management. Map,Set, weak collections, weak references and finalization registries.- Twelve TypedArray kinds including
Float16Array,DataView, resizable and transferable buffers, shared memory andAtomicswith real blocking waits. - Arbitrary-precision
BigInt(ani128fast path backed bynum-bigint), symbols, proxies, and all thirteenReflecttraps with invariant checks. - Modern
RegExp: named groups, lookbehind, match indices (/d) and Unicode sets (/v), executed directly over UTF-16 code units. - ES modules with top-level await, dynamic
import(), import attributes, and deferred and source-phase imports. eval,Function,ShadowRealm, andTemporalwith fifteen calendars and the IANA time-zone database generated from pinned upstream data.
Strings preserve lone UTF-16 surrogates (stored as WTF-8 internally), + on strings builds ropes in O(1) and flattens lazily, and deep recursion throws a catchable RangeError instead of overflowing the native stack, because JavaScript frames never live on the Rust stack.
How the engine runs code
Source goes through a hand-written lexer and recursive-descent parser that owns its AST, so parsed programs can be cached and shared. The compiler emits a flat, three-address register bytecode per function. The virtual machine executes that bytecode over a flat register file with an explicit frame stack, then hands hot code to a JIT.
- Parse
Hand-written lexer and recursive-descent parser, built primarily for correct early errors. Every negative execution in the pinned Test262 corpus must fail with the right error type.
- Compile to register bytecode
Each function becomes a
Vec<Instr>over a fixed register file. Operands are register indices, small immediates or constant-pool entries. The instruction set is small and regular so the JIT can consume the same structured form. - Interpret
Values are NaN-boxed 64-bit words: doubles are themselves, and integers, booleans, null, undefined and heap references hide in the quiet-NaN space. Property access goes through shared shapes and per-site inline caches.
- Tier up
After eight calls or eight back-edges a function or loop region is offered to the x86-64 JIT, which keeps integers and doubles unboxed in machine registers. Guards bail to the exact bytecode instruction; nothing is ever truncated silently.
The architecture page goes deeper into the value model, the JIT tiers (SROA, INT, REGALLOC/DOUBLE and MEM), the non-moving generational nursery, and why the explicit frame stack is what makes deoptimization exact.
Three ways to run it
| Profile | Use it for | Boundary |
|---|---|---|
zipp js / zipp mjs native CLI | Trusted programs, scripts and benchmarks | Maximum throughput with the x86-64 or ARM64 JIT enabled |
zipp-wasm in a Web Worker | Arbitrary browser-hosted code | Interpreter-only safe-sandbox build with no unsafe code; the host terminates the Worker at its deadline |
zipp-sandbox native runner | Hardened native execution of untrusted scripts | No JIT, unsafe forbidden at compile time, instruction, heap, output, import and wall-time limits |
git clone https://github.com/f2i-com/zipp.org
cd zipp.org
cargo build --locked --release
./target/release/zipp js examples/hello.js
# ↳ hello, worldThe CLI also runs ES modules (zipp mjs), Python (zipp py), and picks a frontend from the file extension or shebang with zipp run. Bare source such as x = 1 never silently defaults to one language. Native builds are published for Windows and Linux x86-64 on the releases page.
Embedding Zipp in Rust
The zipp_vm::embed module keeps one compiled VM alive across host calls. A host compiles a script once, installs a host-call handler, runs the top level, and later evaluates expressions or calls functions in the same global context.
let mut st = zipp_vm::embed::compile_script(source)?;
st.set_host_call(Box::new(|kind, args| match kind {
"http.get" => Ok(blocking_get(&args[0])),
_ => Err(format!("TypeError: unknown host call {kind}")),
}));
st.run_init()?; // top-level execution
let html = st.eval_in_context("render()")?; // later, same globalsValues cross the boundary as owned HostValue trees, never as VM-internal handles. Arrays and plain objects marshal structurally; functions, classes, maps, dates, proxies and cycles cross as Opaque and are never overwritten by a structural write-back. The embedding guide covers slots, microtask draining, grammar goals and the browser Engine class.
JavaScript in WebAssembly
The zipp-wasm crate builds the engine for wasm32-unknown-unknown with wasm-bindgen, as a persistent VM a browser host can keep alive across re-entries. It is interpreter-only, grants no host capabilities by default, and exposes a small Engine class: initScript, evalInContext, callFunction, setInstructionBudget, takeOutput and friends.
Since v0.0.26 each engine is one .wasm file that carries its own bindings. In v0.0.29 the JavaScript-only engine is 4,838,214 bytes raw and 1,238,265 bytes after Brotli, and ZIPP Lite, the JavaScript core without Intl and Temporal built for size, 2,760,194 and 751,404; the engine that also includes Python with torch built in is 8,608,417 bytes raw and 2,005,913 on the wire, and Python without torch 1,630,183. The bindings account for 12,155 to 14,402 of those bytes after Brotli. The JavaScript-only engine is about three times the size of QuickJS-NG's WebAssembly reactor, a trade-off documented plainly on the WebAssembly page, which also explains the loader, the Worker-termination deadline model and the resource limits.
Conformance and performance
Zipp runs the pinned tc39/test262 corpus in both sloppy and strict mode: 96,230 executions, including staging, with zero skips. Against the unmodified corpus 96,229 pass. The one remaining execution is an Annex B test whose final assertion is ES2017 wording that ES2018 deleted, and it is the single entry in a checked-in expected-failure manifest; no test file is patched. The Test262 page documents that failure, the CI gate, and how the number climbed from a mis-scored 100% to an honest 96.97% and back up.
On the native benchmark suites, the 15 October 2026 capture puts Zipp at a geometric mean of 0.703× Node's wall time across thirty rows, with 23 rows faster than Node and seven slower, all of them in the hostile suite: the JavaScript bytecode VM at 3.13×, the warm router at 1.38× and allocation-survival at 1.37× lead the gaps. The benchmarks page has every row, the hardware, compiler flags, competitor versions, bootstrap intervals, links to the raw JSON, and the earlier captures. Measured for v0.0.24 against Node 24 alone, in an ordinary release build rather than the canonical capture, the ten headline rows run in 0.88× Node's time, and Zipp's median is below Node's on all 13 real rows.
Frequently asked questions
Is Zipp production-ready?
It is a 0.0.x project, first committed on 29 May 2026; v0.0.29 shipped on 23 October 2026. The JavaScript engine is conformance-complete and benchmarked, the browser sandbox has been through two external correctness audits, and the Python frontend is explicitly experimental. Read the release notes and decide per use case.
Does Zipp have a JIT?
Yes, on native x86-64: a tiered template JIT with hot-loop on-stack replacement, plus a guarded ARM64 baseline tier. The WebAssembly build is interpreter-only by design, so that the browser sandbox contains no runtime code generation.
How is this different from Boa or QuickJS?
Boa is also a Rust engine but interpreter-only; QuickJS is a small C interpreter. Zipp has native JITs and a Python frontend, at the cost of a larger binary. The comparisons page lays out where each is the better choice.
Can I run Node packages?
Zipp is an ECMAScript engine, not a Node runtime. There is no require, fs or http built in. The zipp js compat goal permits top-level return for CommonJS-shaped scripts, and hosts expose capabilities explicitly through host calls.
What license is it under?
Apache-2.0 for Zipp itself, including its Python parser. The vendored regex fork is MIT or Apache-2.0, and generated Unicode data (CLDR locale data, and the character names the Python parser knows, from the Unicode Character Database) carries the Unicode license.