A from-scratch JavaScript engine in Rust — std only, zero dependencies — and a
runtime being built on top of it, the way Node/Deno/Bun wrap a JS engine with an event
loop and host APIs. Every crate in the workspace is std-only: no tokio, mio, libc,
rustyline, serde, or any other third-party dependency, anywhere.
A lexer, parser, and three execution tiers:
- a tree-walking interpreter — the reference oracle: the spec semantics live here, and
every other tier must match it observably (a differential fuzzer,
lumen-difftest, holds them to that); - an opt-in bytecode VM — functions compile whole (or not at all — no deoptimization) to a stack machine with slot-homed locals, per-site inline caches for property and free-name access backed by object shapes (hidden classes), and dense-array element fast paths;
- a native template JIT — bytecode lowers to real ARM64 machine code on macOS, Linux,
and Windows, with a correctness-first x86-64 backend on Intel macOS, Linux, and Windows:
per-op templates with the interpreter as the shared slow path, inline-cache reads baked
into the instruction stream, fused compare-and-branch, exact-
ToInt32bitops, and numeric register chains that keep runs of arithmetic entirely in FP registers. The full set of inline fast paths currently lives in the ARM64 backend; x86-64 starts with native control flow and checked per-op helpers. Other architectures degrade to the bytecode VM.
Tier selection: --tier=interp|bytecode|jit (jit is the default; where the JIT is
unavailable it degrades to the bytecode VM). Functions tier up after a call-count threshold —
immediately if the body contains a loop. Force the reference tree-walker with --tier=interp
(or LUMEN_TIER=interp).
The language surface: generators and async/await running on stackful coroutines (async
bodies suspend on the bytecode VM itself), full RegExp (including \p{…} and inline
modifiers), typed arrays, Proxy/Reflect, ES modules (top-level await, import defer,
source phase), Intl, and Temporal.
Intl (ECMA-402) and its CLDR data tables are behind the default-on intl cargo feature —
the largest single contributor to binary size (~3 MB of the release binary). Build with
--no-default-features for a small engine: the Intl global is absent and the toLocale*
methods degrade to their locale-independent forms, the way engines built without i18n do.
On dependencies and unsafe: the workspace stays std-only — the JIT maps executable memory
through raw platform declarations (mmap/mprotect, macOS MAP_JIT, or Windows
VirtualAlloc/VirtualProtect) rather than libc. Pages are writable only while code is copied,
then executable/read-only, and instruction caches are synchronized where required. The
interpreter and bytecode VM are safe Rust; unsafe is concentrated where machine
code meets the object graph (the JIT's executable pages and its templates' raw reads — every
baked offset is measured at runtime against the live types and fails closed to the checked
helper if anything doesn't hold) and in the N-API addon loader's dlopen bridge.
Passes 100% of tc39/test262: 53,400/53,400 (including
annexB, intl402, and staging) — on the default JIT tier and under LUMEN_TIER=interp.
Extracted from — and used by — the lucid-softworks/browser
engine as its JS backend (backend-lumen), with full git history.
A curated embed API on the engine exposes just enough — native-function registration, a
typed host-state slot, and event-loop hooks — for a runtime layer to be assembled from
independent op crates, without leaking the interpreter's internals into the published API.
On top of that:
-
Event loop (
lumen-runtime) — a single loop thread owns the (!Send) engine; blocking work runs on a std thread pool and completes back overmpsc. No epoll/kqueue reactor (that would need raw syscalls); the thread-pool-plus-completion model is libuv's own fs strategy. Each turn drains microtasks, queued callbacks, due timers, and I/O completions, then blocks until the next event. -
Timers (
lumen-timers) —setTimeout/setInterval/clearTimeout/clearInterval/setImmediate, plusqueueMicrotask. -
consoleandprocess— streamingconsole.*;process.argv/env/platform/cwd()/exit()/nextTick(). -
Filesystem (
lumen-fs) — synchronous ops (readFileSync,writeFileSync,existsSync,mkdirSync,readdirSync, …), file handles via a resource table (openSync/readSync/writeSync/closeSync), and asyncfs.promises.readFile/writeFileon the thread pool. -
Web platform (
lumen-web) — a growing slice of the WinterTC Minimum Common API:Event/EventTarget/CustomEvent/AbortController/AbortSignal/DOMException,TextEncoder/TextDecoder,atob/btoa,structuredClone,URL/URLSearchParams,performance.now(),crypto.getRandomValues/randomUUID/subtle.digest(SHA-256), andfetch/Headers/Request/Response. See the checklist at the top ofcrates/lumen-web/src/lib.rsfor what's implemented vs. deferred (streams,Blob/FormData,URLPattern, …).fetchspeaks HTTP/1.1 overstd::net.https:is not supported: TLS cannot be implemented on std alone and no third-party crate is permitted, sohttpsURLs reject with a clear error; plainhttpworks.Lumen.serve((request) => Response)is the matching HTTP/1.1 server — not a WinterTC API, but the cross-runtimeserve(handler)convention (Deno/Bun/Workers), so a Hono app runs withLumen.serve(app.fetch). v1 is single-accept,Connection: close, buffered bodies, http only (seecrates/lumen-web/src/server.rs). Cold-start and usage:examples/hono-app. -
Modules — both CommonJS and ESM.
lumen-clipicks the module kind the way Node does:.mjsis ESM,.cjsis CommonJS,.jsfollows the nearestpackage.json"type". ES modules run through the engine's real module graph (linking, top-levelawait);importspecifiers resolve against disk andnode_modules,node:builtins are importable (named imports included), and CommonJS packages interop by default export. CommonJS files run as the program entry withrequire.main === module. -
node:compatibility (lumen-node) — a CommonJSrequirewithnode_modulesresolution and the module wrapper,package.jsonmain/exports, thenode:path/node:os/node:fsbuiltins, andBuffer, so packages written against thenode:surface run. See the checklist at the top ofcrates/lumen-node/src/lib.rsfor the deferred pieces (subpath-pattern exports, the full N-API surface).Native addons load too:
require('./addon.node')dlopens the compiled library and runs its N-API registration, resolving the addon'snapi_*symbols against the lumen executable — the same mechanism thenodebinary uses. The N-API surface is implemented from scratch (values, properties, functions, callbacks, errors, references, object wrap, classes, promises, buffers, typed arrays, async work); the loader reachesdlopen/dlsymthrough rawextern "C"declarations, so no third-party crate is added. Seeexamples/native-addon.vite buildruns on lumen (examples/vite-app): a full Vite production build, bundling through Rollup's native N-API addon, transforming with esbuild's service subprocess, over ESM↔CommonJS interop and thenode:surface — buildingdist/and exiting cleanly. -
REPL + CLI (
lumen-repl,lumen-cli) — an interactive shell with a persistent realm, parser-driven incomplete-input detection (multi-line continuation), top-levelawait, and loop-to-quiescence so timers and awaited promises settle before the next prompt. Line editing is line-buffered (raw-mode/history would needtermios); userlwrapfor arrows and history.
lumen engine (std-only, zero-dep; `embed` feature gates the runtime API)
lumen-host substrate: OpState, ResourceTable, Extension, the thread-pool/callback primitives
lumen-timers setTimeout/setInterval/queueMicrotask/setImmediate
lumen-fs filesystem (sync + async)
lumen-web WinterTC Minimum Common API (Event, URL, crypto, fetch, …)
lumen-node node: compatibility (require, node:path/os/fs, Buffer)
lumen-runtime the event loop; assembles the op crates; console + process
lumen-repl interactive shell
lumen-cli node/deno-style entrypoint
test262-runner conformance harness (parallel workers over ./test262)
lumen-difftest differential fuzzer across the three execution tiers
lumen-wasm wasm build of the engine
The dependency graph is a strict DAG — lumen ← lumen-host ← {op crates} ← lumen-runtime ← lumen-repl ← lumen-cli — so each op crate can be worked on in isolation.
Grab a nightly prebuilt runtime on macOS arm64/x86_64 or Linux x86_64/arm64 (tagged releases also publish Windows x86_64 binaries):
curl -fsSL https://raw.githubusercontent.com/lucid-softworks/lumen/main/scripts/install.sh | bashIt installs the lumen CLI to ~/.lumen/bin from the rolling nightly release
(LUMEN_INSTALL and LUMEN_RELEASE override the location and tag). Other platforms build from
source — see below.
Run scripts / open a REPL through the runtime:
cargo build --release -p lumen-cli
./target/release/lumen-cli # REPL (or: lumen-cli repl)
./target/release/lumen-cli file.js [args] # run a script to loop quiescence
./target/release/lumen-cli -e 'code' # evaluate a stringThe engine also ships a minimal standalone shell (the test262 host, no runtime/host APIs):
cargo build --release -p lumen --bin lumen
./target/release/lumen file.js [more.js ...]scripts/test262-clone.sh # one-time: clone the suite into ./test262
scripts/run-test262.sh # run it (see crates/test262-runner for env knobs)
LUMEN_TIER=jit scripts/run-test262.sh # same suite against the compiled tiersThe execution tiers are also held together by a differential fuzzer: every generated program runs in all three tiers, which must agree on the completion value, thrown errors, the observable side-effect trace, and final global state. Divergences are delta-minimized into a regression corpus that replays on every run.
cargo run --release -p lumen-difftest -- --count 2000For warm Map/Set build and lookup scaling at 100, 1,000, and 10,000 keys, run
cargo bench -p lumen --bench collections. Functions are defined once and reused
across samples; the small invocation expression still passes through Engine::eval.
scripts/run-v8bench.sh # classic V8 suite (v8-v7) on lumen; downloads on first run
scripts/bench-compare.sh # same suite on node + bun + lumen, as a markdown table
git clone https://github.com/chromium/octane.git ../octane # one-time: Octane checkout
scripts/run-octane.sh # full Octane suite
scripts/run-octane.sh richards crypto # selected benchmarks
git clone https://github.com/v8/web-tooling-benchmark ../web-tooling-benchmark # one-time: checkout
(cd ../web-tooling-benchmark && npm install) # one-time: build dist/cli.js
scripts/run-web-tooling.sh # full suite (babel, terser, acorn, etc.)
scripts/run-web-tooling.sh --only babel # rebuild dist/cli.js for one selected benchmark
WEB_TOOLING_BENCHMARK_DIR=/path/to/web-tooling-benchmark scripts/run-web-tooling.sh --only terserOctane is expected at ../octane by default; set OCTANE=/path/to/octane to override.
Web Tooling Benchmark is expected at ../web-tooling-benchmark by default;
set WEB_TOOLING_BENCHMARK_DIR=/path/to/web-tooling-benchmark to override. The
upstream CLI bundle does not support runtime benchmark selection; --only <name>
rebuilds dist/cli.js in that checkout with webpack's build-time selector
(npx webpack --env.only=<name>) before running lumen. The full suite is a
many-hours run on current lumen builds; prefer --only <name> while iterating.
# one-time: provide an ARES-6 checkout outside this repo
# the default lookup is the sibling ../ARES-6; ARES6=... overrides it
scripts/run-ares6.sh # full ARES-6 suite
scripts/run-ares6.sh air basic # selected workloads: air, basic, babylon, ml
ARES6=/path/to/ARES-6 scripts/run-ares6.sh babylon mlARES-6 sources are not vendored here. The runner expects a checkout at ../ARES-6 by default; set ARES6=/path/to/ARES-6 to point at another checkout.
The reported summary: is the ARES-6 geomean in milliseconds, so lower is better. A selected run reports a partial geomean over only the selected workloads, which is useful for local iteration but is not the official full-suite ARES-6 score. If a workload fails, or if the expected metric and completion lines are missing, scripts/run-ares6.sh exits nonzero instead of hiding the failure.
Expect the full suite to take a long time on the current tree-walking engine; use selected workloads for quicker local checks.