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-12branch main@ f8e78be1718 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 intervention12 = 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
No runtime WASM emission. "Compiled" code = data interpreted by pre-compiled WAT handlers. JIT-to-wasm is out of scope.
Execution: Phase 1 tree-walk of the AST + Phase 2 bytecode VM (vm.watx) — same parser front-end feeds both.
Values: NaN-boxing in i64 (quiet-NaN prefix + 3-bit tag + 48-bit payload), confined to value.watx.
Memory: bump allocator (with-region/region.alloc); heap_reset() isolates tests. Mark-sweep GC deferred until test262 demands it.
Build: vendored WATX compiler (parser → include → macro-expand → typecheck → lower → emit), zero implicit coercion.
Phase progress
phase
scope
state
0
lex + parse + arithmetic
done
1
functions / closures / control-flow
done
2
objects / arrays / prototypes / builtins
done
3
BigInt / exceptions / wrappers
done
4
threaded-code VM + GC + test262 breadth
in progress
Test suites live run · 2026-08-12
suite
files
pass rate
self-check test/
486 / 486
100%
test262/batch
39 / 39
100%
test262/broad
35 / 36
97%
test262/broad2
30 / 30
100%
test262/broad3
24 / 24
100%
test262/cases
4 / 4
100%
test262/harness
30 / 32
94%
test262 total (curated dirs)
162 / 165
98%
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.
area
pass
fail
skip
timeout
pass rate
annexB
727
358
1
2
67%
built-ins
15706
7267
695
22
68%
language
16631
1486
5594
7
92%
harness
96
3
17
0
97%
staging
795
648
39
26
55%
intl402(no Intl)
23
3318
0
0
1%
TOTAL
33978
13080
6346
57
72%
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):
engine
kind
speed vs watjs
Node (V8)
JIT-compiled to native
~1000× faster
QuickJS native
bytecode interpreter in C
~30–80× faster
QuickJS in wasmfair peer
same interpreter, inside WebAssembly
~20–60× faster
watjs
handler-threaded interpreter in WAT
1× (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.
engine
parse rate vs watjs
Node (V8)
~18× faster
QuickJS native
~2× faster
QuickJS in wasmfair peer
~2× faster
watjs
1× (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
commit
change
f8e78be
class static block: an arrow inherits the await/arguments reservation
ff7ba82
arrow function: a single BindingIdentifier param may not be a reserved word
4ff3ebb
async arrow: async heading an arrow may not be followed by a LineTerminator
9b46af1
object literal: a */async method modifier requires a method body
c9ba132
object shorthand: await/yield reserved as an IdentifierReference by context
8bfa216
destructuring assignment: a rest target may be a literal-base member expression
51f8566
regex: lazy quantified groups (a)*? / (a|b)+? / (x){2,3}?
direct 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 new
duplicate 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)
cover-grammar bare {pat}=RHS (+133 assignment), for-of/for-in patterns
BigInt / Symbol
exact 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 beyondNumber'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):
feature
state
notes
BigInt — arbitrary precision
not implemented
backed by one i64; overflows past 2^63. Needs heap-allocated bignum limbs
ES modules — import / export linking
not implemented
only a dynamic import() parse + Promise stub; no module graph
Intl — internationalization
not implemented
intl402 ~1%; very large surface, deferred
Temporal — date/time API
not implemented
large surface, deferred
Atomics / SharedArrayBuffer, cross-realm
not implemented
headless, single-realm host
Full UTF-16 strings
partial
UTF-8 internally; lone surrogates aren't single code units → blocks isWellFormed/toWellFormed and some astral-plane edge cases
Mark-sweep garbage collector
not implemented
bump 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 allocations
partial
correct 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 gap
impact
state
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 fix
open
Reference Records — member-prefix ++/-- evaluation order