2026-07-27 compiler root-cause dt_parse 206KB / 23ms JS2 wired into the daily driver
The mutable-DOM parser dt_parse hung on any element with an attribute (<a b> = a 10-byte reproducer).
Prior sessions diagnosed heap-layout-dependent memory corruption in the nx runtime. The truth, proven mechanically from the
emitted assembly: nx_cc left TWO terminators in any if-arm containing break/continue/return
(the parser appended its merge-branch after the statement's branch). Every CFG pass then read the dead branch as the block's
terminator, and the 07-16 emit-time branch-inlining made the dead branch reachable — control fell back into the loop body
with the index unchanged. An infinite spin that moved whenever you added a debug print — because prints change block
structure, not heap layout.
ir_bb_sealed in nx_ir.nx: a block already ending in an unconditional terminator refuses further
branches. Nothing after a terminator, ever. The self-hosted compiler was rebuilt through a 3-stage bootstrap to a
byte-identical fixpoint (542,070 B), and the fix removed dead branches from the compiler's own body (it shrank 3 KB).
| Check | Before | After |
|---|---|---|
| break-form probe corpus (P0–P5) | P4 spins forever | ALL-DONE rc=0 |
| Oracle agreement (C-bootstrap known-good) | disagrees | exact match (rc=3) |
| nx_domtree_gate | HANGS before first check | 11/11 GREEN |
| dt_parse, real Wikipedia (206,630 B) | infinite hang | 3,922 nodes in 23 ms |
| parse→serialize round-trip | "empties real pages" | 99% survival, marker kept, 22 ms |
| nx_browser_bg_gate | 16/0 GREEN | 16/0 GREEN (no regression) |
| nx_js_conformance | 92/92 | 92/92 (no regression) |
nx_cc_health (the standing compiler witness) grew a break-semantics tooth: it builds the probe corpus and runs it
under a child-side alarm, so a future regression hang becomes a clean RED. Negative control ran and passed: with the
pre-fix compiler swapped back in, the witness went RED (probe killed by SIGALRM at 8 s); with the fixed compiler, GREEN.
The per-loop iteration cap (10M) multiplies under nesting and never bounded recursion. A global execution fuel now counts
every js_eval dispatch; exhaustion unwinds as an ordinary error and the page renders script-less. Proven with a
nested-infinite-loop page: terminates in 221 ms, fuel_hit=1, static content survives. Conformance unchanged at 92/92.
Fuel is sized by memory, not time: eval temporaries currently cost one page per step (no arena yet) — the
arena is the next engine-hardening rung.
The fetcher's HTML path now runs inline scripts against the parsed DOM and emits the mutated serialization — fail-safe by construction (script-less pages take the untouched verbatim path; any suspicious shrink falls back to the raw body; fuel bounds hostile scripts). The exact wiring that had to be reverted on 07-23 (Wikipedia hung the fetcher):
| Live test (rebuilt nishi_fetch.exe) | Result |
|---|---|
| example.com (no scripts → verbatim path) | byte-perfect passthrough |
| Wikipedia, full JS2 path (fetch+parse+run scripts+serialize) | 205,796 B in 598 ms wall (was: infinite hang) |
| facebook shell | rendered, no hang, no fallback |
Every eval step allocated a 16-byte value box via its own mmap — 204 call sites — and the kernel rounds each to a
resident 4096-byte page. That 256× amplification is what OOM-killed the VM and forced the fuel down to 1M steps. Since the
engine never frees a value box, a bump arena is lifetime-identical by construction. Measured result:
| Fuel budget | Arena | Wall | Old scheme (page/cell) |
|---|---|---|---|
| 200,000 steps | 2,048 KiB | 6 ms | ~781 MiB, 221 ms |
| 1,000,000 steps | 12,288 KiB | 32 ms | ~3.8 GiB (OOM) |
| 5,000,000 steps | 62,464 KiB | 159 ms | ~19 GiB (OOM) |
Linear at 12.5 bytes/step, so the production fuel ceiling was raised 1M → 10M steps — a number derived from that table (~125 MiB, ~320 ms worst case), not chosen by taste. The gate asserts bytes-per-step ≤ 64: 5× headroom over measured, 64× under a page-per-cell regression, so it cannot pass by luck.
js_render_page_pending_begin documents "returns 1 on parse error" and leaves the global environment NULL. The
external-bundle consumer ignored that return and marched a NULL into the pending-fetch loop; the accessors guarded
genv[5]==0 — which dereferences genv to check it — faulting at address 0x28 (5×8). Wikipedia's
concatenated page scripts hit a parse error and the process dumped core. Fixed at both ends: null-guard before the dereference in
all four accessors (a browser must never crash on unparseable page JS) and an honest rc check at the caller that degrades to a
static render. Same page now yields 24,568 bytes of DOM text instead of a core dump.
The consumer only resolved root-relative script paths. Modern SPAs host bundles on CDNs, so Netflix referenced 2 bundles and fetched zero. Script URLs now resolve the three browser forms (absolute, protocol-relative, root-relative), https-only so a script we are about to execute can never be downgraded to cleartext, and bounded at 8 bundles per page against fetch storms. Netflix now delivers 2 of 2 bundles = 3.9 MB of JavaScript into the engine. Its hydrated DOM is still 3,376 bytes — bundle delivery is solved; hydration is not. That is the next rung, stated honestly rather than claimed.
_ops/browser_regress_all.sh runs the compiler witness, every DOM/render/JS gate, and the real-page timing and
round-trip benches: 7/7 GREEN. It caught its own harness bug first — gates fork sibling organs by relative path, and running
them from the wrong directory produced a false compiler-regression RED. A gate's working directory is part of its contract.
| Gate | Verdict |
|---|---|
| nx_cc_health (compiler + break semantics) | GREEN |
| nx_domtree_gate | 11/11 |
| nx_browser_bg_gate | 17/17 |
| nx_js_conformance | 92/92 |
| nx_jsfuel_probe (fuel + arena, 6 teeth) | 6/6 |
| dt_parse + round-trip on real Wikipedia | GREEN |
Daily driver rebuilt and re-verified end to end: example.com 563 B byte-perfect, Wikipedia 205,796 B in 525 ms, BBC 624,871 B in 701 ms — all through the live JS path, no hangs, no crashes.
With bundles finally delivered, Netflix and Wikipedia both failed at the same place: the JavaScript parser. Rather than guess, each failure was turned into a named construct with a controlled reproducer — a probe where ASCII/known-good rows must pass, so a failing row is a real gap and not a broken probe. Each fix moved the parse frontier measurably deeper into the bundle:
| # | Construct the bundle used | Why it appears everywhere | Parse frontier |
|---|---|---|---|
| 1 | {À:"A",Á:"A",à:"a"} — non-ASCII identifier keys | lodash deburr map, in nearly every bundle | — → 289,669 |
| 2 | [0,4,,5] — array elision (hole) | TypeScript's downlevel async helper trys.push([0,4,,5]) | 289,669 → 582,104 |
| 3 | x?.5:1 — ternary vs optional chaining | minifiers emit .5 for 0.5; ?. was taken greedily | 582,104 → 860,389 |
Fix 3 is the spec rule OptionalChainingPunctuator :: ?. [lookahead ∉ DecimalDigit] — one comparison in the lexer. Note that
.5 alone and a?.b alone both already worked; only the combination failed, which is exactly why a
controlled probe beats reading code. The regression sweep stayed 7/7 GREEN and conformance 92/92 after every fix.
The next blocker is now function*(e){…} — generator functions. That is a real language feature, not a lexer
one-liner, so the fix loop stops here rather than pretending otherwise.
The census's biggest rock is the SPA/JS platform gap — five daily-driver sites at 0‰ reading-recall. The language engine was already at 92/92; what was missing was a parser that survives real HTML, a memory model that survives real scripts, a budget, and non-destructive serialization. Those landed. What has not landed: hydration — running a 3.9 MB React bundle does not yet produce content, and Wikipedia's own page scripts do not parse in our engine. Those two gaps, named precisely, are the next rungs. No SPA site is claimed as fixed, because none is.
Method note: the diagnosis used strace as a layout-neutral breadcrumb trail (each tiny allocation is a visible
mmap), a free instruction-pointer sample from strace -i at timeout-SIGTERM, raw-bytes disassembly of the sovereign
ELF, a 6-variant probe corpus to pin the exact broken form, and the C-bootstrap compiler as a disagreeing oracle. No debug
prints in the guest — prints were what kept the bug invisible for two sessions.