watjs — fully WAT-native JavaScript interpreter

JS lexed, parsed & executed entirely inside a single hand-written WASM module. The host provides only a thin I/O surface.
status as of 2026-08-12 branch main @ f8e78be 1718 commits 0 traps / 0 hangs ↗ watjs.berrry.app
31.2k
lines of WATX · 11 modules
353 KB
watjs.wasm artifact
486
self-check JS tests
34.0k
test262 tests passing / 53k
90.0M
output tokens spent building

Daily activity per day · not cumulative

Commits & tokens per day

▮ commits / day   ● output tokens / day (M)   ◆ human intervention   12 = nudges that day
amber diamonds mark days a human steered the engine work (hover for the prompts); the number is repeated "keep going"-style nudges. Status-dashboard prompts are excluded.

Codebase growth cumulative · end-of-day

Source size & wasm size over time

● lines of WATX (k)   ● watjs.wasm (KB)  ·  rebuilt at each day's last commit

Architecture see DESIGN.md / CLAUDE.md

Phase progress

phasescopestate
0lex + parse + arithmeticdone
1functions / closures / control-flowdone
2objects / arrays / prototypes / builtinsdone
3BigInt / exceptions / wrappersdone
4threaded-code VM + GC + test262 breadthin progress

Test suites live run · 2026-08-12

suitefilespass rate
self-check test/486 / 486100%
test262/batch39 / 39100%
test262/broad35 / 3697%
test262/broad230 / 30100%
test262/broad324 / 24100%
test262/cases4 / 4100%
test262/harness30 / 3294%
test262 total (curated dirs)162 / 16598%

Full vendored test262 tree ~53k files · 2026-07-05

Whole vendored tc39 tree (/tmp/t262), run with a fresh wasm instance per test and a 1 s per-test timeout (timeouts counted as fail). skip = module/async/raw-flagged tests not yet run; intl402 is near-zero because Intl is unimplemented. Pass rate = pass / (pass+fail), excluding skips.
areapassfailskiptimeoutpass rate
annexB7273581267%
built-ins1570672676952268%
language1663114865594792%
harness96317097%
staging795648392655%
intl402 (no Intl)233318001%
TOTAL339781308063465772%

Performance watjs vs QuickJS — both in wasm

Benchmarked with node tools/bench.js — 3 Computer Language Benchmarks Game macros (n-body, spectral-norm, fasta) plus the 10-program Are We Fast Yet suite. Each is self-timed (Date.now, auto-calibrated) and its result is verified identical across every engine, so a faster run can't be a wrong run. watjs has no JIT — it's a handler-threaded interpreter, benchmarked honestly against other interpreters.

The fair peer is QuickJS compiled to WebAssembly: like watjs it runs inside the wasm sandbox with no native code, so the gap reflects interpreter design — not V8's JIT. Order-of-magnitude standing (approximate, machine-dependent):

enginekindspeed vs watjs
Node (V8)JIT-compiled to native~1000× faster
QuickJS nativebytecode interpreter in C~30–80× faster
QuickJS in wasm  fair peersame interpreter, inside WebAssembly~20–60× faster
watjshandler-threaded interpreter in WAT1× (baseline)
All 13 benchmarks produce byte-identical, verified output on watjs, Node, and QuickJS-in-wasm. QuickJS-wasm runs within ~1–2× of native QuickJS, so the wasm boundary itself costs little — the ~20–60× gap to watjs is interpreter design. watjs is weakest on polymorphic-dispatch-heavy code (e.g. richards) and best on tight arithmetic loops.

Parsing is a separate axis and watjs is competitive here

The table above is execution speed. Turning source into a running program is a different cost — measured on its own with node tools/bench-parse.js, which times new Function(src) on a large body that is built but never called (lex + parse + front-end compile, no execution). watjs has no GC, so it parses once per fresh wasm instance; the fast engines get distinct source variants to defeat V8's compilation cache.

engineparse rate vs watjs
Node (V8)~18× faster
QuickJS native~2× faster
QuickJS in wasm  fair peer~2× faster
watjs1× (baseline)
On the front-end watjs runs within ~2× of QuickJS (native and in-wasm) — its parser is competitive. The 20–60× gap is entirely the interpreter/execution side, not parsing. As with the compute suite, absolute throughput is machine-dependent; the stable number is the ratio to the qjs-wasm peer (~2×).

Recently landed newest first

commitchange
f8e78beclass static block: an arrow inherits the await/arguments reservation
ff7ba82arrow function: a single BindingIdentifier param may not be a reserved word
4ff3ebbasync arrow: async heading an arrow may not be followed by a LineTerminator
9b46af1object literal: a */async method modifier requires a method body
c9ba132object shorthand: await/yield reserved as an IdentifierReference by context
8bfa216destructuring assignment: a rest target may be a literal-base member expression
51f8566regex: lazy quantified groups (a)*? / (a|b)+? / (x){2,3}?

Coverage by area test262, approximate

areanotes
Proxyper-trap spec invariants; built-ins/Proxy ~220, construct/newTarget done
Objectnamed accessors, descriptors, for-in proto-chain enumeration
Date~580 passing (timeclip, toPrimitive hints, UTC/year offsets, setters)
CollectionsSet/Map proto, ES2024 set methods, WeakMap/WeakSet, groupBy, Symbol.species
TypedArrays%TypedArray% intrinsic, exotic numeric-index semantics, detached checks, species-create, Float16Array, Uint8Array base64/hex, resizable/length-tracking buffers, ArrayBuffer.prototype.transfer
Classes / subclassing newblock-scoped class name, static & instance fields, derived-ctor super() + return semantics, subclassing native exotics (Array/TypedArray/Map/Set/Promise/RegExp/Date/Boolean/Number/String)
Scoping newreal nested lexical envs — block scope, per-iteration for-let bindings, TDZ, multi-declarator for-init
Eval / global env newdirect vs indirect eval, sloppy-eval let/const in a fresh env, indirect eval in the global env, top-level let/const/class off globalThis with TDZ, global-lexical/var conflict & non-configurable shadowing
RegExp newduplicate named groups + per-position capture reset, scoped inline modifiers (?ims-ims:…), u-mode syntax strictness, lazy/greedy quantified groups with proper backtracking (CPS); legacy static accessors ($1$9)
Async / Promisesmicrotask job queue, true await suspension, async generators (+.return/.throw), for-await-of, Array.fromAsync
Destructuringcover-grammar bare {pat}=RHS (+133 assignment), for-of/for-in patterns
BigInt / Symbolexact within i64 (64-bit-bounded — not arbitrary-precision, see below), wrapper valueOf, @@toPrimitive

Not implemented missing / partial features — the honest gaps

BigInt is 64-bit-backed, not arbitrary-precision. Every BigInt is a single i64, so values past 2^63 (~9.2e18) overflow silently — 2n ** 128n and 30n! give wrong answers. It is exact beyond Number's 2^53 limit, but it is not a bignum.
No garbage collector — memory only grows within a run. watjs uses a bump allocator: heap_reset() frees everything between tests, but during a single program run nothing is ever reclaimed. So allocation-heavy or long-running code climbs monotonically toward the linear-memory ceiling and then traps (memory access out of bounds). Concretely, regex over a large input (>~50 KB) traps today, and any long-lived workload that churns objects/strings will too. A real mark-sweep GC is the planned fix; it is not implemented yet.

Notable ECMAScript features watjs does not implement yet (or only partially):

featurestatenotes
BigInt — arbitrary precisionnot implementedbacked by one i64; overflows past 2^63. Needs heap-allocated bignum limbs
ES modulesimport / export linkingnot implementedonly a dynamic import() parse + Promise stub; no module graph
Intl — internationalizationnot implementedintl402 ~1%; very large surface, deferred
Temporal — date/time APInot implementedlarge surface, deferred
Atomics / SharedArrayBuffer, cross-realmnot implementedheadless, single-realm host
Full UTF-16 stringspartialUTF-8 internally; lone surrogates aren't single code units → blocks isWellFormed/toWellFormed and some astral-plane edge cases
Mark-sweep garbage collectornot implementedbump allocator only; memory is never freed within a run (just heap_reset() between tests). Long-running / allocation-heavy programs grow until they trap
Large-input regex & big allocationspartialcorrect at small/medium scale; regex over >~50 KB of input runs out of the bump region and traps (a symptom of the missing GC above)

Stability

✓ Crash/hang surface clean. 2026-07-05 full sweep: 53,404 files, 0 traps / 0 hangs, and per-test timeouts collapsed 2,859 → 57 vs the June sweep — compute-heavy paths got much faster (whole tree now runs in 73 s). Remaining failures are correctness / architectural, not crashes.

Open issues & known gaps

✓ Recently resolved: real nested lexical environments — block scoping + per-iteration for-let bindings; multi-declarator for-init; class inner-name binding (named class exprs + static self-ref); subclassing native built-ins (super() into Array/TypedArray/ Map/Set/Promise/RegExp/Date/…); TypedArrays over resizable buffers (length-tracking); eval scoping (direct vs indirect, sloppy-eval fresh env) + a global lexical environment (top-level let/const/class off globalThis, with TDZ) — Aug 2026.
Silent SyntaxError false-pass. The engine still swallows compile-time SyntaxErrors (tag=0, no throw) → the harness false-passes any test/include that won't compile. Poisons A/B comparisons; e.g. regExpUtils.js → ~500 RegExp / property-escape tests false-pass. Highest-priority correctness trap.
bug / correctness gapimpactstate
Array/function can't be a [[Prototype]]Object.create(arr) doesn't inherit indices (proto stored as scope-parent)~12–20 tests; deep object-model fixopen
Reference Records — member-prefix ++/-- evaluation orderarchitectural; direct/indirect-eval scoping largely landedpartial

Next up