# JS execution performance: the road toward JIT-class throughput

> **Status:** profiling review complete (2026-07-02); the optimization push it
> scoped has **landed on this branch** — deterministic fast hasher, quick-wins
> batch, key-verified inline caches, superinstruction round 2, and the
> call-path slimming (cell pool + `Rc`-shared `BytecodeFunction`). Measured
> results in **§6**; the remaining roadmap (register bytecode, full
> cells→locals, shapes) is unchanged below. This document is the successor to the
> per-phase plan in [`docs/interpreter-optimization.md`](./interpreter-optimization.md)
> (Phases 0–2 landed there) and the retired closure-threading experiment in
> [`docs/jit.md`](./jit.md). It re-scopes the goal from "make the interpreter
> less wasteful" to **"close as much of the gap to JIT runtimes as the
> engine's invariants allow"** — and it is grounded in a new callgrind
> (instruction-exact) profile of the cross-runtime benchmark suite rather
> than wall-clock on a noisy container.
>
> The three invariants are unchanged and non-negotiable: **zero `unsafe`**,
> **no new heavyweight dependencies**, **byte-identical deterministic replay**
> ([`docs/replay.md`](./replay.md)). Every item below states how it satisfies
> them.

---

## 1. Where we are (measured 2026-07-02)

### 1.1 Cross-runtime gap

`node crates/chidori-js/benchmarks/run.mjs` (5 runs, median, execution-only =
subprocess wall-clock minus per-runtime startup), on an otherwise-idle dev
container (4 cores):

| workload | chidori | node (V8) | bun (JSC) | gap vs fastest |
| --- | ---: | ---: | ---: | ---: |
| arith_loop | 342 ms | 2.2 ms | 4.0 ms | 155× |
| array_hof | 362 ms | 21 ms | 15 ms | 24× |
| array_push_sum | 590 ms | 14 ms | 18 ms | 41× |
| closures | 624 ms | 0.6 ms † | 4.0 ms | (†) |
| fib_recursive | 1.38 s | 7.0 ms | 6.9 ms | 199× |
| json_roundtrip | 223 ms | 38 ms | 26 ms | 8.5× |
| property_access | 904 ms | 0.6 ms † | 3.2 ms | (†) |
| sort | 1.73 s | 110 ms | 99 ms | 17× |
| string_build | 440 ms | 3.6 ms | 3.4 ms | 128× |

† A sub-millisecond "execution" time means V8's optimizer effectively
*deleted* the workload (loop-invariant hoisting / dead-store elimination on a
loop whose result folds), so those ratios measure the JIT's ability to not do
the work, not engine speed doing it. Cross-checked directly: node's *total*
process time for property_access (~37 ms) is below its own ~42 ms startup
baseline. Read those rows as "unboundedly behind on eliminable loops."

**Honest summary:** chidori-js executes these workloads **~10–40× slower**
than JIT runtimes where the work is irreducible (json, sort, array traversal)
and **~100–200×** slower on tight numeric/call loops, which is exactly where
speculative JITs shine. Startup is chidori's one clear win (~4 ms vs node's
~42 ms). For context: QuickJS — the reference "fast pure interpreter" — sits
roughly 10–30× behind V8 on these same shapes; the realistic ceiling for an
interpreter-only chidori-js is that band, and §4 covers what crossing it
would actually take.

(Wall-clock methodology note: an earlier run of this table taken while a
`cargo` build was saturating the container's cores inflated node/bun times
3–15× and understated every gap. On shared hardware, cross-runtime ratios
are only meaningful from an idle machine — one more reason the roadmap's
load-bearing numbers are callgrind instruction counts, which contention
cannot touch.)

### 1.2 Where the instructions actually go (callgrind)

Wall-clock on this container has a ~10–15% noise floor
(`interpreter-optimization.md` §7.6), so the load-bearing numbers here are
**callgrind instruction counts** — deterministic, environment-independent,
and reproducible to the instruction. Totals are for one full workload run.

**property_access (11.0 G instructions) — hashing IS the workload:**

| share | where | what |
| ---: | --- | --- |
| **48.7%** | SipHash (`sip::Hasher::write` 19.8% + `hash_one` 16.1%) + `IndexMap::get_index_of` (12.7%) | every `o.a` get/set SipHashes the key string and probes the property `IndexMap` |
| 17.7% | `step` | dispatch + op bodies |
| 6.0% | `run_frame` | loop bookkeeping |
| 4.9% | `set_prop_mode` | write-path walk |
| 4.2% | `Vec::push_mut` | operand-stack pushes |

**fib_recursive (12.8 G) — the price of a call:**

| share | where | what |
| ---: | --- | --- |
| 21.3% | `step` | dispatch + op bodies |
| **16.7%** | `malloc` + `_int_free` + `free` | one `Rc<RefCell<Value>>` heap allocation **per binding per call** (`make_frame`), plus frame vec churn |
| 8.6% | `run_frame` | loop bookkeeping |
| ~7.8% | SipHash + `IndexMap` probe | resolving the global `fib` by string hash **on every call** |
| 4.9% | `make_frame` | frame setup |
| 2.8% + 1.9% + 1.3% | `drop_in_place<Frame>`, `Rc::drop_slow`, `Value::clone` | refcount churn |
| 1.3% | `slice_contains` | `stable_cells` `Vec::contains` on every `InitCell` |
| 1.1% | `BytecodeFunction::clone` | per-call clone |

**arith_loop (3.2 G) — dispatch-bound, as Phase 0 predicted:**

| share | where | what |
| ---: | --- | --- |
| **52.3%** | `step` (37.7%) + `run_frame` (14.7%) | pure dispatch |
| 8.8% | `Vec::push_mut` | operand-stack traffic |
| 8.2% | `bin_arith` | the actual arithmetic |
| 5.9% | libm `fmod` | the workload's `%` operator |
| 5.4% | `drop_in_place<Value>` | stack-slot drops |

Three structural taxes explain nearly all of the gap:

1. **Hash-table property access** — no shapes, no caches, and (until this
   change) a DoS-hardened hasher: ~49% of property-heavy execution.
2. **Every binding is a heap cell** — `compiler.rs` documents this as the v1
   trade ("every source-level binding is a heap **cell** … an allocation per
   binding"): ~25% of call-heavy execution is allocator + refcount traffic.
   `frame.locals` and `LoadLocal`/`StoreLocal` exist in the VM but the
   compiler never emits them (`num_locals: 0`).
3. **Stack-machine switch dispatch** — >50% of tight-loop execution, the
   known ceiling from `interpreter-optimization.md` §4.

---

## 2. Landed with this review: deterministic fast hasher (`src/fxhash.rs`)

`IndexMap`'s default hasher is `RandomState`/SipHash — a *keyed, DoS-resistant*
hash that is exactly wrong for an engine-internal property table. Replaced
with an in-tree implementation of the Fx hash function (rustc's own table
hasher; one rotate/xor/multiply per 8-byte chunk, ~30 lines, zero new
dependencies) for: object property maps (`ObjectData::props`), `Map`/`Set`/
`WeakMap`/`WeakSet` backing stores.

**Measured (callgrind, deterministic):**

| workload | before | after | Δ instructions |
| --- | ---: | ---: | ---: |
| property_access | 10.98 G | 7.75 G | **−29.4%** |
| fib_recursive | 12.83 G | 12.24 G | −4.6% |

Wall-clock moved less than the instruction count on this container
(property_access roughly −10%, sort and string_build directionally similar) —
the remaining probe is more memory-bound, and the shared container's noise
floor blurs the rest. The instruction count is the honest, reproducible
metric here.

**Why this is safe:**

- *Determinism:* hashes decide only bucket placement inside `IndexMap`;
  iteration order is insertion order and lookups are settled by `Eq`. The
  hash function is unobservable to JS and to the replay journal. Unlike
  `RandomState` (per-process random seed!), Fx is fully deterministic —
  bucket layouts are now *identical across runs by construction*, which is
  strictly more deterministic than before.
- *Security:* SipHash's seed defends tables whose keys an adversary chooses
  against collision-flooding. Property keys come from the agent program and
  its data; the engine already bounds runaway execution with the uncatchable
  op budget and the host interrupt/timeout. A flooding attack degrades a run
  the same way `while(1)` does, and the same defenses answer it. V8, JSC,
  and SpiderMonkey all make this same trade.
- *Gates:* full crate suite (incl. `tests/replay.rs` record→replay
  byte-identity) and the Test262 gate, green.

---

## 3. The interpreter roadmap, re-ranked by measured share

Ordered by (measured cost × implementation risk). Each item is a pure
performance side effect: same results, same errors, same enumeration order,
same journal — gated by Test262, the replay byte-identity suite, and the
callgrind instruction-count proxy.

### 3.1 Property-key atoms with precomputed hashes (attacks the remaining ~24% of property-heavy)

After the hasher swap, `get_index_of` still re-hashes the key string and
byte-compares it on every access. Property names are known at compile time
(they sit in `FuncProto.consts`); the fix is a compile-time **atom table**:

- Intern property-name strings once per engine; a `PropertyKey` then carries
  `(Rc<str>, precomputed u64 hash)`; equality tries pointer-equality first.
- `GetProp`/`SetProp` reference the atom directly from the const table —
  no `JsString` clone, no `PropertyKey` construction, no re-hash per access.
- Determinism: an atom table is a cache keyed by string *content*; identical
  content → identical atom. Nothing address-dependent leaks: hashes are
  content-derived (Fx, unseeded).

Expected: kills most of the remaining 24% `get_index_of` + the per-access key
materialization visible in `step`. Low risk — no semantic surface at all.

### 3.2 Cells → locals: stop heap-allocating every binding (attacks ~25% of call-heavy)

The compiler's own header calls this the deferred v1 trade. The VM already
has pooled `frame.locals: Vec<Value>` and `LoadLocal`/`StoreLocal` ops —
unused. The work is compiler-side:

- Pre-pass per function body: collect names referenced by nested closures
  (the `FnCtx` comment already reserves the concept). Bindings **not**
  captured — the overwhelming majority — lower to `locals` slots; captured
  ones stay cells. Frames containing direct `eval` (or `with`) keep
  everything in cells, as today.
- Params stay cells only when a mapped `arguments` object aliases them or a
  closure captures them.
- Per-iteration `let` semantics: a fresh *cell* per iteration is only needed
  when the loop body captures the binding; otherwise a local slot is reused —
  same observable behavior, zero allocations.

Expected: removes the malloc/free + `Rc`/`RefCell` traffic that is ~25% of
fib-style execution and much of the `closures` workload's 49× gap; every
`LoadCell` (16.9% of all executed ops in the Phase-0 survey) that becomes
`LoadLocal` drops a pointer-chase + borrow-check + refcount-safe clone to an
indexed `Vec` read. This is the single biggest interpreter win available.
Medium risk (compiler rewrite of binding resolution), fully covered by
Test262's closure/TDZ/arguments suites + the differential harness.

### 3.3 Global inline cache (attacks fib's ~8% + every cross-function call)

`LoadGlobal` resolves `fib` by string hash on **every recursive call**. The
global object is one known object; give each `LoadGlobal` site a slot cache:

- Cache `(slot_index)` validated by a **global-object mutation counter**
  (bump on any insert/delete/reconfigure of globals — not on value writes,
  which go through the slot). Hit → direct indexed read from the `IndexMap`;
  miss → today's path, then refill.
- Determinism: deterministic by construction — the counter is engine state
  driven only by program behavior, identical across record/replay. (No
  pointer identity involved; if per-object identity is ever needed for
  object-property ICs, mint **allocation-order object ids** — also
  deterministic — rather than addresses.)
- Never serialized; rebuilt per `Vm` like the existing caches.

Expected: turns every global read (function references above all) from
hash+probe into `counter check + Vec index`. Low risk, small surface.

### 3.4 Superinstruction continuation + per-op precomputation (cheap, incremental)

The Phase-2 fusion infrastructure is landed and cheap to extend:

- `GetPropConst`/`SetPropConst` ops that carry the resolved atom (with §3.1)
  — removes the const-table indirection in the hottest property idiom.
- `stable_cells` membership: resolve at compile time into a per-cell flag on
  the op (`InitCell` vs `InitCellStable`) instead of `Vec::contains` per
  execution (1.3% of fib for a linear scan!).
- Integer-`%` fast path in `arith`: both operands integral f64 in safe range
  → compute directly instead of libm `fmod` (5.9% of arith_loop).
- Loop-idiom fusions from the Phase-0 pair table still unmined:
  `LoadCell;LoadCell;<binop>`, increment patterns (`i = i + 1` as a single
  `IncCell`-class op).

### 3.5 Register bytecode (Phase 4 — now with a justified trigger)

Unchanged assessment from `interpreter-optimization.md`: the biggest
dispatch-side win (arith_loop is 52% dispatch + 8.8% stack pushes + 5.4%
stack drops), and the biggest rewrite. Two changes to its standing since:

- §3.2 (cells→locals) is a **prerequisite done right**: once bindings live in
  indexed frame slots, the distance to "ops address slots directly" (that is
  what a register VM is) shrinks substantially — much of the compiler-side
  analysis is shared.
- The decision input is now instruction-exact: re-run the callgrind proxy
  after 3.1–3.4; if dispatch+stack-shuffle still dominates compute-heavy
  workloads by >40%, the rewrite pays.

### 3.6 Frame diet (small, riskless)

`Frame` carries rarely-used fields (`dispose_scopes`, `enumerators`,
`with_scope`, `eval_vars`, completion machinery) inline; boxing the rare ones
(`Option<Box<RareFrameState>>`) shrinks per-call initialization and the
`drop_in_place<Frame>` cost (2.8% of fib). Likewise `BytecodeFunction::clone`
per call (1.1%) can become an `Rc` bump.

### 3.7 Out of scope here, tracked elsewhere

- **Warm-realm reuse** — `engine_new` ≈ 3.6 ms dominates short scripts;
  flagged INVESTIGATE in `interpreter-optimization.md` §11.4 and partly
  addressed by the resume caches ([`docs/resume-performance.md`](./resume-performance.md)).
- **Object-shape (hidden-class) layer + property ICs** — the full V8-style
  answer to property access. Deliberately *sequenced after* §3.1/§3.3: atoms
  + global ICs capture most of the measured cost at a fraction of the risk
  (shape transitions, `delete`, proto mutation, dictionary-mode fallback are
  where engines grow their worst bugs). Revisit with fresh callgrind data;
  if `get_index_of` still dominates property-heavy code, shapes are the next
  step and the determinism story is the same as ICs (shapes are
  content/insertion-order-derived, never serialized).

---

## 4. The JIT question, answered honestly

The ask: *"if we could add a JIT without `unsafe`, it would be reasonable."*

**A native-code JIT without `unsafe` does not exist.** Emitting machine code
at runtime requires mapping writable-then-executable memory and calling
through a synthesized function pointer — both outside safe Rust's semantics
by definition, no matter who writes the code. The options, ranked by how
close they get:

| option | unsafe? | deps | verdict |
| --- | --- | --- | --- |
| **Cranelift baseline JIT** (`cranelift-jit`) | none in *our* crate (`forbid(unsafe_code)` stays); the `unsafe` lives in the dependency | pure Rust, no C — but a compiler backend (~large, slow to build, its own CVE surface) | The only real JIT path. See below. |
| **JS → wasm → wasmtime/pulley** | same delegation, more of it | wasmtime is a much larger dependency than Cranelift alone | Strictly worse than direct Cranelift for this use. |
| **Closure-threading** (safe Rust) | zero | zero | **Built, measured 1.01–1.11×, retired** ([`docs/jit.md`](./jit.md)). Dispatch was never the whole problem. |
| **Basic-block closure compilation** (safe Rust) | zero | zero | The unexplored safe headroom: compile straight-line blocks to single closures keeping intermediates in a scratch buffer, `Ctl` only at block boundaries. Est. 1.3–2× on dispatch-bound code, delicate mid-block throw semantics. Only worth revisiting *after* §3.2/§3.5, which shrink the same costs with less machinery. |

**On determinism:** the old blanket objection ("a JIT fights replay") is
narrower than `interpreter-optimization.md` §2 states, and the retired
experiment proved the pattern: a **non-speculative** baseline tier (no type
guards, no deopt) that compiles a function on its **Nth activation** (an op-
count trigger — deterministic engine state, not wall-clock) and reuses the
interpreter's own helpers computes byte-identical results with a byte-
identical journal, record and replay alike. Determinism is an engineering
constraint on JIT design, not a disqualifier.

**The honest cost-benefit:** a Cranelift tier is the "no heavyweight deps"
invariant traded away, plus the largest correctness surface this engine
would ever take on (frame reconstruction at OSR points, unwinding into JS
try/finally, the op-budget contract inside native code) — for a payoff that
lands almost entirely on compute-heavy replay/test throughput, because JS
is <1% of live agent wall-clock (`interpreter-optimization.md` §11.5).

**Recommendation:**

1. Land §3.1–3.4 (atoms, cells→locals, global ICs, fusion batch). These are
   safe-Rust, dep-free, and attack the *measured* 25–50% shares. Re-profile.
   Expected composite: **3–6×** on the benchmark suite, taking the gap on
   irreducible workloads from ~10–40× to roughly **3–10×** and tight loops
   from ~100–200× to ~30–60× — the QuickJS band, i.e. parity with the best
   pure interpreters, with startup (~4 ms vs node's ~42 ms) already won.
2. Decide register bytecode (§3.5) on the post-3.x callgrind numbers.
3. Treat a Cranelift tier as a **product decision, not an engineering
   default**: it becomes rational only if a workload emerges where JS compute
   dominates wall-clock at scale (mass replay fleets, compute-heavy agent
   steps) *and* the 5–10× interpreter band is demonstrably not enough. If
   that day comes, the design constraints are already established: baseline-
   only, non-speculative, deterministic activation-count tier-up, interpreter
   helpers for every slow path, `unsafe` confined to the dependency, and the
   whole thing behind the same toggle-equivalence gate the closure-threading
   experiment used.

---

## 5. Gates (unchanged, per item)

Every roadmap item ships only when all four hold:

1. **Test262 gate** (`scripts/test262.sh --gate`): zero regressions.
2. **Replay byte-identity** (`tests/replay.rs`): identical journal + output,
   caches cold vs. warm, optimization on vs. off.
3. **Differential harness** (`tests/fusion.rs` pattern): optimized vs.
   fallback path, byte-identical output and errors over the corpus.
4. **Callgrind instruction-count proxy**: the claimed share actually drops;
   wall-clock is reported but never load-bearing on shared hardware.

---

## 6. Results of the 2026-07-02 optimization push (landed)

Five commits on this branch implement §2, §3.1 (the cheap half), §3.3, §3.4,
§3.6, and an allocation-free approximation of §3.2. All of it is safe Rust,
zero new dependencies, and gated green on both crates' full suites (98 + 603
tests, including record→replay byte-identity, the fusion differential corpus,
and the new `tests/ic.rs` stale-hint corpus cross-checked against Node).

1. **Deterministic Fx hasher** (§2) for property maps and Map/Set stores.
2. **Quick wins:** O(1) `stable_cells` flags; integer fast path for `%`
   (libm `fmod` was 5.9% of arith_loop); `Rc` pointer-equality fast path in
   `JsString::eq`.
3. **Key-verified inline caches** on `GetProp`/`SetProp`/`LoadGlobal`
   (`FuncProto::ic`): a slot-index hint per op site, verified against the key
   stored at that slot on every use — a stale hint is a miss, never a wrong
   answer, so no invalidation protocol exists. Own-data-property reads/writes
   and global reads skip hashing entirely on hits.
4. **Superinstruction round 2:** the fusion pass matches variable-length
   windows and runs to a fixed point. A whole `i < N` loop test
   (`CmpCellConstBranchFalse`), a statement-position `i++` (`IncCellStmt`,
   the 6-op window), `cell <op> const` operands (`AddCellConst`/
   `ArithCellConst`), and the per-iteration `let` copy (`LoadCellInit`) are
   each ONE dispatch. The canonical counting loop: 21 → 12 dispatches per
   iteration.
5. **Call-path slimming:** binding cells are pooled (recycled only at
   `Rc::strong_count == 1` — provably unreachable, so reuse is unobservable);
   `FunctionInner::Bytecode` holds `Rc<BytecodeFunction>`, making the
   per-call clone a refcount bump instead of one or two Vec allocations.
6. **Cells→locals localization (§3.2, landed)** — `localize.rs` rewrites
   provably-uncaptured bindings to flat `frame.locals` slots at compile
   finish. Captured/stable/aliased cells keep their ORIGINAL indices (child
   protos' upvalue references stay valid, no patching); dynamic-resolution
   functions (direct `eval`, `with`, materialized `arguments`) bail whole.
   Every superinstruction gained a local mirror, so localized loops keep
   their one-dispatch tests/updates. En route, `mapped_param_cells` — which
   pinned every sloppy function's parameters as stable heap cells — is now
   built only when the function can actually materialize `arguments`;
   `fib`-style leaf calls now allocate **zero** cells. Gated by a
   capture-focused differential corpus (`tests/localize.rs`) across all four
   {fuse}×{localize} combinations.

Round 3 (same session) added four more commits:

7. **Rope strings** — `s += chunk` was O(total²): string_build's profile was
   96.5% memcpy. `JsString` gained a rope arm: each `+` over well-formed
   operands is one O(1) node; bytes are copied exactly once on first
   observation. `.length` and the size guard stay O(1) via stored totals;
   flatten and drop are both iterative. **string_build: 5.00 G → 0.18 G
   instructions (27×)** — and prompt-building via `+=` is the canonical
   agent pattern.
8. **Dense-array element fast paths** — every `a[i]` converted the Number
   key to a heap-allocated string (grisu formatting!) and reparsed it.
   `GetPropDynamic`/`SetPropDynamic` and the array iteration builtins
   (map/filter/forEach/reduce, the array iterator) now access unshadowed
   in-bounds dense elements directly. array_push_sum −26%, array_hof −17%.
9. **Prototype-level inline caches** — `IcEntry` carries an independent
   `proto_slot` + holder: a data property on the receiver's DIRECT
   prototype (the `arr.push` / class-method pattern) is cached, verified by
   holder pointer identity (which doubles as realm isolation) and
   no-own-props shadowing. The own-hit path never touches the holder.
10. **Owned-args calls** — the interpreter's call ops move their pooled
    argument buffer into the callee frame instead of re-copying.

### 6.1 Instruction counts (callgrind — deterministic, the load-bearing metric)

Whole-workload totals, branch start → after rounds 1–3:

| workload | before | after | Δ |
| --- | ---: | ---: | ---: |
| string_build | 5.00 G | 0.18 G | **−96%** |
| property_access | 10.98 G | 3.95 G | **−64%** |
| arith_loop | 3.17 G | 2.28 G | **−28%** |
| fib_recursive | 12.83 G | 8.59 G | **−33%** |
| array_push_sum | 5.04 G¹ | 3.73 G | −26%¹ |
| array_hof | 3.03 G¹ | 2.52 G | −17%¹ |

¹ vs. the post-round-2 measurement (no branch-start baseline was taken).
The zero-host agent-replay example is instruction-identical before/after
round 3 (11.05 G) — its cost is host-effect glue, not the paths these
rounds touched.

malloc/free (16.7% of fib at branch start) and SipHash (48.7% of
property_access) have left the profiles' top ranks entirely.

### 6.2 Wall-clock (idle container, 5-run median, execution-only)

Against the same-day pre-push idle-machine run (§1.1 methodology):

| workload | before | after | speedup |
| --- | ---: | ---: | ---: |
| property_access | 904 ms | 310 ms | **2.9×** |
| arith_loop | 342 ms | 199 ms | 1.7× |
| fib_recursive | 1.38 s | 900 ms | 1.5× |
| closures | 624 ms | 451 ms | 1.4× |
| array_push_sum | 590 ms | 436 ms | 1.4× |
| array_hof | 362 ms | 276 ms | 1.3× |
| sort | 1.73 s | 1.38 s | 1.3× |
| json_roundtrip | 223 ms | 183 ms | 1.2× |
| string_build | 440 ms | 382 ms | 1.2× |

Geometric mean ≈ **1.5×** across the suite (2.9× where property access
dominates), on top of the hasher win the same day. The zero-host agent-replay
path (`examples/agent_replay`) went 21.0 → 18.5 ms (−12%) — it is
host-effect glue rather than tight loops, as §11.5 of the interpreter doc
predicts.

### 6.3 What's next (assessment after round 3 — now addressed by §6.4)

§3.2 (cells→locals) and the property/array cache work are **landed**. Hash
probing (`get_index_of`) no longer appears in any workload's top profile
ranks — the shapes question (§3.7) is answered for now. What the profiles
show after round 3:

- **sort (14.1 G, the biggest remaining)**: ~25% pure call ceremony for the
  tiny comparator — `Frame` is 408 bytes and its construction, pool
  round-trips, and drop dominate each comparison. The lever is a **Frame
  diet** (box the rarely-used fields) and/or a leaf-call fast path that
  skips unused frame machinery. *(→ landed as the frame POOL + direct-call
  path, §6.4.)*
- **register bytecode** (§3.5) remains the dispatch-side end-game for the
  arith/fib class (step + run_frame are >50% there).
- A discovered pre-existing conformance gap: sealed-array appends are not
  rejected (`Object.seal(a); a[len] = v` writes). Tracked for a separate
  fix validated by Test262.

## 6.4 Round 4 (2026-07-02, follow-up session): the call-ceremony round

Callgrind confirmed §6.3's read: after round 3, ~30–40% of sort/fib-class
execution was **per-call ceremony** — frame construction (`make_frame_owned`
6.3%), frame drop (3.9%), buffer-pool round-trips (`recycle_frame` +
`recycle_value_vec` 7%), ~400-byte `Frame` moves (memcpy 2.8%), and the
layered call dispatch (~6%). This round attacks exactly that; all safe Rust,
zero new dependencies, byte-identical benchmark checksums.

1. **Frame pool** — frames are now recycled WHOLE as `Box<Frame>`
   (`Vm::frame_pool`): a synchronously-finished frame is scrubbed (every
   value-bearing field cleared, so the pool never extends a value's lifetime
   — the cell pool's discipline) and parked; the next call re-initializes
   fields in place. The operand-stack/locals/cells/args buffer *capacities*
   stay attached to the frame across calls (the per-call
   `take_value_vec`/`recycle_value_vec` round-trips for three buffers are
   gone), `run_frame` takes the box (a pointer move, not a ~400-byte
   memcpy), and a suspension keeps its box (no `Box::new` per await/yield).
   A pooled frame's `func` slot holds a shared placeholder
   (`Vm::dummy_bf`) so no `BytecodeFunction` outlives its frame.

2. **Direct call path** (`Op::Call`/`Op::CallMethodless` →
   `Vm::call_direct`): the callee is peeked on the operand stack; a plain
   (sync, non-generator, non-class-ctor) bytecode function skips the
   generic dispatch layers and its arguments MOVE straight from the
   caller's operand stack into the pooled callee frame — no intermediate
   pooled `Vec`, no second copy, and the popped function value is reused as
   `func_obj` without refcount traffic. Everything else (native, bound,
   proxy, async, generator, not-callable) takes the generic path unchanged.
   The generic `call_valuevec` itself was collapsed to a single object
   borrow (callable check + dispatch extraction were two).

3. **Interpreter-loop slimming** — the per-op budget/interrupt checks
   hoisted behind one `counting` bool sampled at frame entry (both are only
   ever installed before execution starts); the common case pays one
   predicted branch instead of two `Option` loads through `self` per op.

4. **`merge_sort` scratch buffer** — the recursion allocated TWO fresh
   `Vec` clones per node (O(n log n) allocations + refcount churn). Now one
   scratch buffer for the whole sort; the left run MOVES into it and merges
   back in place (zero `Value` clones). Identical recursion structure and
   stable-merge order, so the comparator sees the exact same call sequence.

5. **Sort loop fast paths** — `Array.prototype.sort`'s snapshot loop now
   uses the existing `has_get_elem` dense fast path (round 3) instead of
   per-index `HasProperty`/`Get` with a heap-allocated string key, and
   write-back overwrites existing dense elements in place under the same
   gate as `Op::SetPropDynamic` (`props.is_empty()` + in-bounds non-hole).
   The undefined-partition pass moves values instead of cloning.

6. **`func_obj` gating** — the per-call `Rc` round-trip is skipped unless
   `proto.uses_arguments` (its only consumer is `arguments.callee`).

### 6.4.1 Instruction counts (callgrind, whole workload, deterministic)

| workload | round-3 baseline | after | Δ |
| --- | ---: | ---: | ---: |
| sort | 14.09 G | 11.58 G | **−17.8%** |
| fib_recursive | 8.59 G | 7.45 G | **−13.3%** |
| closures | 4.47 G | 4.00 G | **−10.4%** |
| array_hof | 2.52 G | 2.38 G | −5.4% |
| property_access | 3.95 G | 3.85 G | −2.6% |
| arith_loop | 2.28 G | 2.23 G | −2.5% |
| array_push_sum | 3.73 G | 3.67 G | −1.7% |
| string_build | 0.18 G | 0.18 G | 0% |

Benchmark RESULT checksums are byte-identical before/after every change.
Wall-clock (5-run median, same shared container, indicative only per §1.1):
sort 1.09 s → 0.93 s, fib 696 → 572 ms, closures 350 → 311 ms.
An incidental robustness gain: heap-boxed frames raised the native-stack
recursion ceiling from ~1160 to ~1460 JS frames (see the known issue below).

### 6.4.2 Robustness follow-ups (landed with this round)

Both known issues flagged during this round's review are **fixed**:

- **Deep recursion now throws instead of aborting the process.** The
  pre-existing failure: `max_call_depth` (2000) exceeded what the default
  8 MB native stack supports, so `rec(1500)`-style recursion killed the
  process with an uncatchable native stack overflow (main aborted at ~1160
  frames; the frame pool had already raised that to ~1460). Root cause:
  `step`'s single ~190-arm match carried a **4 KB stack frame** (LLVM's
  imperfect stack coloring unions the arms' locals), so every JS call cost
  ~5.5 KB of native stack. Fix, two-sided:
  - `step` is split: hot ops stay inline (~2.7 KB frame), everything else
    delegates to an `#[inline(never)] step_cold`. Each op has exactly ONE
    implementation. A plain JS→JS call's native footprint is now ~3 KB, so
    the 2000-frame guard fires (catchable RangeError) comfortably inside
    8 MB. Probe-verified: `rec(1900)` returns, unbounded recursion throws
    `RangeError`, spread-call recursion included. Callgrind cost of the
    split: ≈0.5% geomean (fib +1.1% worst) — the price of the abort→throw
    conversion.
  - The chidori server/CLI ran agent JS on `tokio::spawn_blocking` threads
    with tokio's **2 MiB** default stacks (abort at ~350 frames!). All
    tokio runtimes are now built via `scheduler::new_tokio_runtime()` with
    16 MiB threads (`JS_THREAD_STACK_BYTES`, matching the branch worker
    threads' existing choice).
  - Remaining sharp edge (accepted): recursion THROUGH `direct eval`
    (`perform_direct_eval` holds its own 4 KB frame) stacks ~10 KB/frame
    and can still hit the native limit before the depth guard on small
    stacks. Pathological; unchanged from before.
- **Sealed-array appends are rejected.** `Object.seal(a); a[a.len] = v`
  (and `push`/`unshift`, in-bounds hole writes, `preventExtensions`-only
  receivers) wrote through the dense-array Set path, which never consulted
  `extensible` when CREATING an element. `ordinary_define_own` now rejects
  creation on a non-extensible dense array — silently in sloppy mode,
  TypeError in strict — matching Node/spec exactly. One Test262 test flips
  to passing (baseline refreshed).

## 6.5 Typed loop kernels (landed): unboxed register execution for numeric loops

The answer to "can we selectively fast-path compute-heavy loops in a
limited context" — a deterministic, safe-Rust, zero-dependency middle tier
between the interpreter and a real JIT. Design informed directly by the
retired closure-threading experiment (docs/jit.md): removing *dispatch*
alone bought 1.01–1.11×, so this tier removes the **boxing** instead — the
`Value` clone/match/drop and operand-stack traffic around every add and
compare.

**How it works** (`kernel.rs`, `Op::LoopKernel`, `Vm::run_kernel_op`):

- At compile finish (after localize + fuse), back-edges identify loop
  regions. A region qualifies only if every op is on a numeric allowlist —
  loads/stores of localized `frame.locals` slots, `Number` constants,
  arithmetic/comparisons/branches and their fused forms; anything else
  (calls, property access, cells, TDZ init, try handlers, suspension)
  disqualifies it entirely. The region's stack bytecode is translated by
  abstract interpretation into a flat register program over unboxed `f64`s
  (canonical stack slots become registers; compare ops must feed branches —
  a materialized boolean disqualifies). The loop-header op is REPLACED by
  `Op::LoopKernel` (indices/jump targets everywhere are untouched); the
  original header op is preserved as the kernel's `fallback`.
- At runtime the kernel enters only when (a) no op budget is installed
  (per-op accounting stays exact — the conformance runner's budgeted runs
  execute fully generically) and (b) every mapped local currently holds a
  `Number`. JS numeric ops are CLOSED over numbers, so after the guard no
  non-number can appear mid-kernel — there is no deopt map because there is
  nothing to deopt. A failed guard executes the fallback op and the generic
  interpreter takes that iteration; the kernel retries at the next
  back-edge arrival (so a `let x;` warming to a number on iteration 1
  enters the kernel from iteration 2). Loop exits write the registers back
  and resume the interpreter at the exit ip. The cooperative interrupt flag
  is polled on kernel back-edges at the interpreter's cadence.
- Arithmetic calls the SAME `number_arith_raw`/`js_mod`/`to_int32` helpers
  as the interpreter's Number×Number fast paths — results are bit-identical
  by construction (NaN, -0, shift masking, `%` sign, `>>>`).

**Kernel v2 (same branch): dense-array access + nested loops.** The
translator's virtual stack became TYPED — number registers vs. array-base
entries (a two-phase walk first discovers which locals are used as bases) —
which generalizes the tier to the `s += a[i]` class:

- `a[i]` reads, `a[i] = v` in-place writes, and `a.length` translate to
  `LoadElem`/`StoreElem`/`LoadLen` kernel ops. Each access RE-CHECKS its
  full fast-path condition at runtime (unshadowed dense array, integral
  in-bounds index, non-hole `Number` element — the same conditions as
  `Op::SetPropDynamic`'s existing fast path) and otherwise **bails
  precisely**: registers write back and the generic interpreter resumes AT
  the access op, its operand stack reconstructed from a per-kernel shape
  table (numbers from registers, bases from the pinned object slots). A
  bail is a slow iteration, never a wrong answer — holes that read through
  the prototype, accessor elements, frozen/sealed arrays, string elements,
  float/negative/OOB indices, and mid-loop growth all take the exact spec
  path (differentially pinned in the corpus and cross-checked against
  Node).
- Base locals are pinned at entry (stores to them reject at translation),
  aliased bases work (per-access borrows), and `a[b[i]]` nests.
- An inner loop's `Op::LoopKernel` header translates as its preserved
  fallback op, so nested numeric loops collapse into ONE outer kernel
  (the per-iteration `let j` reset — a dead `undefined` store — is elided
  when provably re-stored before any read within the block).

**Measured (callgrind, whole workload, deterministic):**

| workload | before | after | Δ |
| --- | ---: | ---: | ---: |
| arith_loop | 2.23 G | 0.48 G | **−78% (4.6×)** |
| array_sum (new workload) | 10.67 G | 3.76 G | **−65% (2.8×)** |
| array_push_sum | 3.67 G | 2.72 G | **−26%** (its sum loop kernels) |
| sort / fib / closures / property / string / json | — | — | unchanged |

Wall-clock arith_loop ~245 ms → ~44 ms (5.6×); a mixed array/nested
workload (dot products + 2D walk) 686 ms → 193 ms (3.6×). The gap to V8 on
pure numeric loops drops from ~100× to ~20×. `fib` (calls), `sort`
(comparator calls), and property/string workloads are untouched by design —
kernels only fire where the loop body is local numerics and dense-array
element access.

**Gates:** the differential corpus (70+ programs) + 300-case deterministic
fuzz (`tests/kernels.rs`) require byte-identical behavior kernels-on vs
kernels-off across break/continue/labels, NaN/-0/precision edges, guard
bails, late entry, nested loops, array holes/accessors/freeze/aliasing/
growth/reassignment, and op-budget interaction; structural tests pin the
canonical, array, and nested loops to actually kernelize; full suites +
Test262 gate green (kernels are additionally OFF under the runner's op
budget, so conformance runs the generic path — the corpus carries
kernel-specific coverage). The pass is disabled under the `op-histogram`
feature (it would hide per-op counts).

**Kernel v3 (same branch): `Math.*` intrinsics + in-body `const`/`let`.**

- The compiler's Math method-call pattern (`LoadGlobal("Math"); Dup;
  GetProp(name); Swap; args…; Call(n)`) translates to direct kernel ops for
  `abs floor ceil round trunc sign sqrt fround` (unary) and
  `min max pow imul` (binary, exact-arity only). Every kind calls the SAME
  core function its builtin uses (`builtins::numbers`), so results are
  bit-identical — including `Math.round`'s half-up negatives, `min/max`
  NaN-poisoning and ±0 ordering, and `imul`'s int32 wrap. The **entry
  guard** identity-checks the global `Math` binding (a plain data property
  holding the canonical object — accessors/replacements decline) and each
  used method (methods are writable; a monkeypatched `Math.max` makes the
  kernel decline and the patch runs generically, observably). `Math.PI`-
  class value constants are non-writable AND non-configurable on the
  canonical object, so with the object identity guarded they fold to
  literal constants at translation. Unsupported methods/arities
  (`hypot`, `log`, variadic `max`) reject the region as before.
- In-body `const x = …` / `let y = …` emit a TDZ-init op
  (`InitLocalTdz`) that previously rejected the region — the single
  biggest eligibility hole in practice. It is now ELIDED under the same
  proof as the dead `undefined` store: the local must be re-stored before
  any read, branch, or branch target; a genuine conditional-TDZ-read
  region stays generic so the ReferenceError comes from the spec path
  (pinned in the corpus).

Measured on a Math-heavy loop (clamp + `imul` hash over 2M iterations, the
DSP/aggregation shape): **2058 ms → 199 ms (10.3×)**, node at 57 ms — the
gap on this class drops from ~36× to ~3.5×. Benchmark-suite counts are
otherwise unchanged (checksums identical); the corpus grew Math edge cases
(NaN/±0 ordering, half-up rounding, monkeypatching, wholesale `Math`
replacement, accessor-on-globalThis, patch-between-activations) plus
in-body-declaration cases, and the fuzz generator now routes values
through Math intrinsics.

**Kernel v4 (same branch): dense appends, materialized booleans, and
captured loop bounds.**

- **Appends & hole fills**: `StoreElem` now performs an exact
  one-past-the-end append (`a[a.length] = v`, `arr.push`-free building)
  and in-bounds hole fills — both CREATE a property, so they additionally
  require the array extensible and under the dense-storage bound;
  otherwise they bail to the generic path, which owns the sloppy-silent /
  strict-TypeError / RangeError semantics. This unlocks the fill-by-append
  and `new Array(n)` fill idioms.
- **Booleans as first-class kernel values**: the virtual stack and the
  local map are statically TYPED. A stored comparison (`const hi = x > 5`),
  `!x`, `true`/`false` literals, and loop-carried flags become Bool
  registers holding exactly 0.0/1.0; the guard requires `Value::Bool` and
  write-back restores it — `typeof ok` never sees a number. Coercing
  consumers (arithmetic, conditions, Math args) read the raw register —
  identical to `ToNumber`/`ToBoolean` on a boolean — while array
  indices/elements REFUSE bools (`a[true]` is the property `"true"`), and
  strict (in)equality between statically mixed bool/number operands folds
  to its constant (the generic `strict_equals` never compares across
  types). Local types are discovered to a FIXPOINT (a boolean store types
  the local; the next translation run reloads it as Bool); genuinely
  mixed-type locals keep the loop generic.
- **Captured loop bounds**: a read-only-in-region UPVALUE (`const N`
  captured from the enclosing scope — the classic module-level bound)
  snapshots into a register at entry, guarded `Number`. Sound because
  kernel regions contain no calls: nothing can write the cell
  mid-activation. In-region upvalue writes still reject.

Measured: array_sum drops further, 3.74 G → 2.03 G instructions (−81%
total from its 10.67 G pre-kernel baseline — the `new Array(N)` fill loop
was bailing per-iteration on holes). A run-scanning workload (append-build
+ boolean-flag scan, 500k elements ×4) goes **2065 ms → 204 ms (10.1×)**.
Other suite counts unchanged (±layout noise), checksums byte-identical.

**Kernel v5 (same branch): FUNCTION kernels — frameless tiny callees.**

The sort/HOF profile is ~55% comparator-call ceremony (frame init/recycle,
operand-stack moves, `Value` clones/drops) around an 8-op body. A function
whose ENTIRE body is on the kernel allowlist now also compiles to a register
program (`FuncProto::fn_kernel`), and the call paths (`call_direct`,
`call_bytecode_vec`, the slice-based `call_bytecode`) execute it FRAMELESS:
no frame, no operand stack, no pool traffic — arguments load straight into
registers and `Return` yields the result value (`KOp::Ret`, typed
Number/Bool).

- **Entry guard, per call**: every consumed argument present and a `Number`
  (a missing/extra/string argument declines THAT call, generically);
  captured upvalues hold `Number`s (read-only snapshots — no calls can run
  inside a kernelized body, so nothing writes a cell mid-execution); no op
  budget; no trace sink (it must see an enter/exit per call); `Math`
  canonicals verified as in v3. Callers apply the depth guard before the
  hook, so the max-call-depth RangeError fires identically on both paths.
- **fn-mode translation rules** on top of the loop allowlist: `LoadArg`
  reads become guarded argument registers; locals are pure register scratch
  — there is no guard to type them, so every read must be DOMINATED by a
  real store, tracked as a sorted `init` set merged under the same
  must-match rule as the virtual-stack shape (a genuine TDZ read or
  use-before-init rejects; the generic path owns the error). Element access
  and `a.length` reject outright (a bail needs a frame to resume into). The
  declared-function prologue (`this`/`new.target` materialized into locals)
  translates via an OPAQUE stack entry whose only legal consumer is a store
  to a never-read local — arrows and declared functions both kernelize.
  Bodies with loops work (the interior `Op::LoopKernel` translates as its
  fallback, and kernel back-edges poll the interrupt flag as usual).

Measured (callgrind, RESULT checksums byte-identical across the suite):
**sort 11.56 G → 6.21 G (−46%)**, **closures 4.03 G → 2.31 G (−43%)** (its
callbacks capture numeric upvalues — the guarded-snapshot rule covers them),
**array_hof 2.38 G → 1.72 G (−28%)**, arith_loop/array_sum −3.4% each;
fib_recursive +0.07% (the `fn_kernel.is_some()` probe on a never-eligible
callee — ~1 instruction per call), property_access unchanged. Wall-clock
sort ~1.4 s → ~0.6 s on the dev box.

**Gates:** corpus grew ~20 function-kernel programs (comparators incl.
boolean-returning and ternary, map/filter/reduce/every/findIndex callbacks,
upvalue capture with number and string cells, missing/extra/non-number
arguments, monkeypatched Math, −0/NaN pins, `.call`/`.apply` entry,
recursion staying generic, loops inside kernelized bodies); structural pins
require the canonical tiny functions to carry `fn_kernel` and
frame-dependent bodies (`arguments`, property access, calls, allocation,
implicit-undefined return) to NEVER carry one; the op-budget test now also
covers a call-heavy program (function kernels are OFF under a budget, like
loop kernels).

**Kernel v6 (same branch): SELF-RECURSIVE function kernels.**

fib-class functions are pure scalar bodies whose only off-allowlist ops are
the recursive call sites. `LoadGlobal` of the function's OWN name now
translates (fn mode) to a speculative "self" entry, and the plain-call
pattern over it fuses to `KOp::SelfCall`: the executor
(`Vm::run_fn_kernel_rec`) runs the whole recursion as stacked REGISTER
WINDOWS over one grown `Vec<f64>` with an explicit (return-pc, dst,
window) stack — zero frames, zero `Value`s, zero operand stacks for the
entire call tree.

- **Guard** (on top of v5's): the global the callee resolves through must
  be a plain data property holding the VERY closure being invoked (pointer
  identity) — a rebound/shadowed/accessor'd name declines and the generic
  `LoadGlobal` observably resolves whatever the program set up. Checked
  once per top-level entry: nothing inside a kernel can write globals.
- **Depth fidelity**: self-calls track depth against the interpreter's
  limit (`call_depth + window count`); an overflow ABANDONS the activation
  and returns "guard declined" — sound because function kernels are pure
  (registers only) — so the caller's generic rerun recurses to the same
  depth and raises the exact spec RangeError from the exact frame.
  Interrupts poll on self-calls and back-edges as usual.
- **Static safety rails**: every self-call must supply every argument
  index the body consumes (a short call would need the generic `undefined`
  parameter), and a recursive kernel must return NUMBERS only (a boolean
  would land in a caller register statically typed Num, diverging under
  `typeof`/strict-eq). Argument expressions must be statically-Number
  registers. Mutual recursion and named-expression self-reference (a
  lexical binding, not a global) stay generic.

Measured: **fib_recursive 7.51 G → 0.81 G instructions (−89%, 9.3×)**;
wall-clock fib(30) ~78 ms vs node 22's ~110 ms total on the same box — the
first workload where chidori beats node outright. Every other suite count
is at layout-noise level (±0.15%), checksums byte-identical. Corpus grew
fib/gcd/Ackermann (nested self-call arguments), rebinding-mid-program,
boolean-return and mutual-recursion negatives, per-call declines, and a
dedicated depth-overflow differential (`max_call_depth = 64` on a big-stack
thread) pinning the abandon-and-rerun RangeError path.

**Kernel v7 (same branch): named-property access on pinned objects.**

The property_access shape — a monomorphic `o.a = i; o.b = o.a + 1; …`
get/set loop over a plain object — was pure interpreter tax (44% `step`,
27% `Value` clone/drop/push). `Op::GetProp`/`Op::SetProp` over an
oslot-pinned base now translate to `KOp::LoadProp`/`KOp::StoreProp`, with
a resolution model STRONGER than an inline cache: each (base, key) class
resolves ONCE at kernel entry to a raw property-map slot index, and then
runs with **zero per-access checks and no bail path**. That is sound
because nothing inside a kernel region can restructure a property map —
no calls, no property creation or deletion (`delete` rejects the region;
a creating store never translates) — and the only in-kernel property
writes are `StoreProp`'s in-place `Number` overwrites, so both the slot
index and the loaded-value Number-ness are activation invariants.

- **Entry conditions** per class (mirroring `Op::SetProp`'s interpreter
  fast path): the base holds an `Internal::Ordinary` object (exotic
  receivers — Proxy, module namespace, typed arrays, `Date`&c. — decline),
  the property exists as an OWN data property, holds a `Number` where the
  region loads it, and is writable where the region stores it. Any miss
  declines the ACTIVATION into the generic fallback iteration (accessors
  fire, frozen objects fail silently/throw, prototype reads walk the
  chain — all observably, on the spec path), and the kernel re-tries at
  the next back-edge (late entry covers the create-then-loop idiom).
- Aliased bases stay coherent (every access reads/writes the object's
  real storage); slots re-resolve on every activation, so shape changes
  BETWEEN activations are fine. `o.length` keeps the array `LoadLen`
  path.

Measured: **property_access 3.84 G → 0.69 G instructions (−82%, 5.5×)**,
checksums byte-identical across the suite. The fatter kernel dispatch
loop costs arith_loop/array_sum ~+2.5% (register pressure), dwarfed by
the win. Corpus grew getter/setter observation counts, frozen and
non-writable stores (sloppy + strict), prototype reads, aliasing,
create-in-loop late entry, `delete`-in-loop rejection, between-activation
shape changes, non-Ordinary receivers, and −0/NaN pins.

**Kernel v8 (same branch): pinned-closure calls inside loop kernels.**

The closures shape — `for (…) s = f(s) - 4` over a tiny capturing callback
— spent ~2300 instructions per iteration on generic dispatch and call
ceremony around ~15 instructions of work (the callee itself already ran
frameless via its v5 function kernel). A call of an OBJECT-TYPED LOCAL
under a plain `undefined` this now translates to `KOp::CallKernel`: the
callee local is pinned exactly like an array base (in-region stores to it
reject), and the callee's function-kernel register program runs INLINE on
a dedicated window above the caller's registers.

- **One guard per activation** covers everything `run_fn_kernel` checks
  per call: plain bytecode function, has a (non-recursive,
  Number-returning) fn kernel, every consumed argument index below the
  smallest argc any site supplies, canonical Math, Number upvalues — the
  upvalue snapshot loads into the window ONCE (callee code never writes
  upvalue registers, and no calls can run between iterations). Loop calls
  happen at a constant depth, so one depth check stands in for the
  per-call guard; an active trace sink declines (it must see an
  enter/exit per call). Argument registers must be statically NUMBER
  (they copy raw into the callee's guarded-Number arg registers).
- **Per call**: copy argc registers, run the callee window (its
  back-edges poll the interrupt through the caller's counter), copy the
  `Ret` register out. No frames, no `Value`s, no operand stacks.
- **Codegen isolation**: the register loop is MONOMORPHIZED on a const
  `CALLEES` flag — kernels without closure calls compile the arm and its
  state out entirely. (The naive single loop cost arith_loop/array_sum/
  property_access 7–10% in register spills; the split restored all three
  to their prior counts.)

Measured: **closures 2.32 G → 0.57 G instructions (−76%; −86% from the
4.03 G session start)**, other suite counts within ±1%, checksums
byte-identical. Corpus grew multi-callee regions, non-number upvalues,
mid-loop and between-activation callee reassignment, boolean-returning /
kernel-less / native / recursive callees (all decline observably),
short/extra argument counts, monkeypatched Math, −0 pins, and callee
results flowing into property/element stores.

### 6.5.1 What's next (new baseline)

- Remaining kernel candidates: `String.prototype.charCodeAt`-class reads,
  loop bounds via own-frame CELLS (captured accumulators), typed-array
  element access (a natural fit — elements are statically numeric), and
  argument-typed ARRAY parameters for function kernels (`(a, i) => a[i]`
  needs arg object slots + a bail-free access story).
- Kernel-tier extensions with clear shapes: MUTUAL recursion (guard a
  small set of global bindings instead of one), self-calls through local/
  captured bindings (`const f = n => … f(…)`), and boolean-returning
  recursion (type the result register Bool).
- **step dispatch remains the wall for non-kernel code**: `step` +
  `run_frame` are 25–43% of call-heavy workloads. Register bytecode (§3.5)
  is the remaining structural lever, and kernels shrink its risk: the
  translator's typed-stack machinery is exactly the analysis a register
  allocator needs.

Re-run the callgrind sweep before choosing; the noise-floor and idle-machine
caveats in §1.1 stand.

## 6.6 JSON round-trip (landed): single-buffer stringify + parser fast paths

json_roundtrip's profile was ~30% raw allocator traffic and ~12% Rust
`format!` machinery: the serializer built a fresh `String` per LEAF, a
`Vec<String>` + `join` + `format!` wrap per tree LEVEL, and allocated an
`Rc<str>` property key per member [[Get]]; the parser built every string
char-by-char and the number formatter ran grisu for every integer.

Changes (`builtins/numbers.rs`, `vm.rs`) — no spec-visible effect moves:
the [[Get]] order, toJSON/replacer calls, and proxy traps are untouched,
and a 25-case differential battery (escapes, indent modes, replacer
allowlists/omission, boxed primitives, toJSON, surrogate escapes,
control-char rejection, circular detection, −0/1e21/2^53-class numbers)
is byte-identical to node 22:

- **One output buffer for the whole tree**: `json_stringify` appends and
  returns emitted/omitted; an omitted object member TRUNCATES its written
  `"key":` prefix back off (its side effects already ran, exactly as the
  spec orders). Separators are direct pushes; the pretty-print strings
  are all empty in compact mode.
- **Run-based escaping** (`json_quote_into`): only `"`, `\` and control
  bytes escape — all single ASCII bytes — so maximal clean runs copy as
  slices, multi-byte UTF-8 included wholesale.
- **`JsString` keys end-to-end**: member keys stay `Rc` (clone = refcount
  bump) through the key list, the member [[Get]], toJSON/replacer
  arguments, and quoting — no per-member allocation.
- **Small-integer number formatting** (`push_number_string`): integral
  |n| ≤ 2^53 — exact, and its plain decimal digits ARE the shortest
  round-trip form — formats straight into the buffer; larger/fractional
  values keep the spec grisu path (`String(2**60)`-class values differ!).
- **Parser no-escape fast path**: a string body without escapes/control
  bytes is ONE slice copy; the escape-aware loop only runs from the first
  backslash.

Measured: **json_roundtrip 2.07 G → 1.20 G instructions (−42%)**, RESULT
checksums byte-identical across the suite. Remaining costs are parse-side
object building (property-map hashing) and the interpreter reads around
the loop — shape-cache territory, out of scope here.

## 6.7 String code-unit reads (landed 2026-07-10): cached length + O(1) ASCII indexing

A gap found while adding bench coverage, not by callgrind — no workload
exercised it (the new `string_scan` closes that): the `charCodeAt`-class
accessors (`charCodeAt`/`charAt`/`codePointAt`/`at`) materialized the ENTIRE
string as a fresh `Vec<u16>` per call (`units_this` → `to_utf16_vec`), and
`.length` on a plain `Utf8` string re-scanned it per read — so the canonical
tokenizer loop `for (i = 0; i < s.length; i++) s.charCodeAt(i)` was O(n²)
with n full-string allocations.

Two changes, no new dependencies, no semantic surface:

- `Repr::Utf8` carries a lazily-cached UTF-16 unit count (`Cell<u32>`,
  computed on first `len_utf16`; `from_code_units` records it for free).
  `.length` is O(1) after first read, and — since unit count == byte count
  iff the string is pure ASCII — `code_unit_at` indexes bytes directly on
  ASCII strings (the `Rope` arm already stores totals, so an ASCII rope
  gets the same O(1) path over its flattened bytes). Construction stays
  O(1); the bytecode-constant load path (`from_rc_str`) pays nothing.
- The four accessor builtins read through `code_unit_at` instead of
  materializing code units.

Measured: `string_scan` (8 KB ASCII string, 12 scan rounds + a charAt pass)
**1.50 s → 50 ms wall (30×)**; heap churn for the criterion variant
**20.16 MiB / 166 k allocs → 167 KiB / 2.4 k allocs**. RESULT byte-identical
to Node. Non-ASCII strings keep the O(i) iterator path per read — a rope
index or WTF-8 offset table remains available headroom if a workload ever
needs it, and kernel candidate (a) (§6.5.1) now has its O(1) accessor
prerequisite.

## 6.8 Typed-array kernel bases (landed 2026-07-10): §6.5.1 candidate (c)

The loop-kernel translator always accepted `t[i]` / `t[i] = v` / `t.length`
over a pinned object base — the RUNTIME arms just only recognized dense
`Internal::Array`, so a typed-array loop kernelized and then bailed on every
access. The arms now also take numeric typed arrays:

- `LoadElem`/`StoreElem`: a valid-index integer-indexed [[Get]]/[[Set]]
  reads/writes element storage directly — own props and the prototype chain
  are never consulted, so no props/proto re-check is needed (unlike dense
  arrays). The register already holds the ToNumber'd store value, and
  `typed_array::encode`/`decode` are the SAME per-kind conversions the
  builtin path uses (f32 rounding, ToInt32-class wrapping, u8 clamping) —
  bit-identical by construction. OOB (incl. detached / shrunk views) bails:
  the generic path owns undefined-absorption and silent-store semantics.
  BigInt-element kinds always bail (elements aren't Numbers).
- `LoadLen`: typed-array `.length` resolves through a prototype ACCESSOR, so
  the activation entry guard (`kernel_ta_len_ok`, gated by the new
  `Kernel::loads_len` flag) identity-checks that any typed-array LoadLen
  base still resolves to the pinned canonical `%TypedArray%.prototype`
  getter (`Realm::ta_length_getter`) with no own shadow. Sound for the whole
  activation: nothing inside a kernel can add props, change protos, or
  resize/detach a buffer (all require calls).

Measured: the `typed_array` workload (Float64Array sum/dot/transform +
Int32Array bit-mix) **637 ms → 37 ms wall (17×)** — from ~45× behind Node to
roughly parity. Gates: the kernels differential corpus grew 17 typed-array
programs (per-kind store semantics incl. clamping/wrapping/f32 rounding,
OOB reads and writes, aliased same-buffer and cross-kind views, offset
subarrays, own-`length` shadow, patched prototype getter, null proto,
BigInt kinds, resizable-buffer length tracking, mixed dense+typed regions);
full suites + Test262 gate green.

## 6.9 Kernel v9 (landed 2026-07-10): the recursion shapes — mutual,
boolean-returning, and captured-binding self-reference (§6.5.1 e/f/g)

The three shapes v6's self-recursion tier declined, all landed in one
generalization. `KOp::SelfCall` gained a `callee` selector, `Kernel::
self_global` became a `KernelRec` descriptor (`self_refs` + partner
`globals`), and the windowed executor became MULTI-FUNCTION:

- **Boolean returns (g)**: translation runs under an assumed static return
  type (Number first, then Boolean) — `SelfCall` dst registers are typed by
  the assumption and every `Ret` must agree, so `isEven`-class predicates
  kernelize while mixed-type recursions stay generic. A top-level boolean
  result materializes `Value::Bool` (typeof/strict-eq exact).
- **Captured-binding self-reference (f)**: `LoadUpvalue` in callee position
  (the compiler's `LoadUpvalue; LoadUndefined(this); args…; Call` pattern)
  speculates SELF; the entry guard requires the cell to hold the very
  closure being invoked. `const gcd = (a, b) => … gcd(…)` and named
  function expressions kernelize; a rebound `let` or a helper closure in
  the cell declines observably. Mis-speculation only costs translation —
  the shapes it could hit were never kernelizable.
- **Mutual recursion (e)**: `LoadGlobal` of a non-self name in fn mode
  becomes a speculative partner reference. The entry guard resolves the
  whole call FAMILY once per activation (transitively, bounded at 8):
  every name a plain data global holding a plain sync bytecode closure
  with a function kernel, every member's self-references intact, every
  member's `Ret` type matching the entry kernel's, every call site
  supplying the RESOLVED callee's consumed arguments, canonical Math and
  Number upvalues per member. Execution stacks per-member register
  windows over one buffer; the current member's tables are hoisted so
  same-kernel recursion (fib-class) pays nothing new — fib's count is
  unchanged. Nothing inside a kernel writes globals or cells, so entry
  resolution holds for the activation; rebinding a partner BETWEEN calls
  declines and the patched binding is observed generically.

Measured (idle machine, interleaved A/B medians): **mutual_recursion
(isEven/isOdd over 20k outer calls + const-arrow gcd) 1.106 s → 0.160 s
(6.9×)**; fib_recursive / closures / sort unchanged. RESULT byte-identical.
Remaining headroom: the per-activation family resolution allocates its
tables per OUTER call (~40k entries here) — an entry cache or Vm-pooled
scratch would shave the shallow-recursion case further. *(→ landed as the
family cache + grow-only register buffer, §6.11.)*

Gates: the kernels differential corpus grew 16 recursion programs
(boolean/mutual/const-binding families, typeof/strict-eq observation,
partner and self rebinding declines, non-kernelizable family members,
short-arg call sites at translation and at entry, −0 pins, Math inside
recursion, captured numeric upvalues) and the depth-overflow differential
now covers the mutual path; structural pins updated (boolean/mutual/
captured-binding recursion MUST kernelize, parameter-recursion must not);
full suites + Test262 gate green.

## 6.10 Register bytecode (landed 2026-07-10): §3.5, scoped as a tier

The remaining structural lever from §3.5, built the way this engine builds
tiers rather than as the big-bang rewrite the original phase plan imagined:
a compile-finish pass (`reg.rs`) translates a WHOLE eligible function body
from the final stack bytecode into a register program
(`FuncProto::reg: Option<Rc<RegProto>>`), and `Vm::run_frame` — the single
funnel every call, construct, accessor, and module/script evaluation goes
through — executes it via `run_reg_frame` instead of the stack loop. No
operand stack, no per-value push/pop traffic, and one dispatch where the
stack machine pays two or three: `LoadLocal a; LoadLocal b; Add; StoreLocal c`
is a single `Add { dst: c, a, b }`.

**Translation model** (mirrors the kernel tiers' discipline, over boxed
values):

- **Same helpers, same semantics.** Every register op calls the SAME `Vm`
  helper as its stack twin. The inline-cache fast paths (`GetProp`/`SetProp`/
  `LoadGlobal`) and the dense-array element fast paths were extracted into
  shared `Vm` methods (`ic_get_prop`, `ic_set_prop`, `ic_load_global`,
  `elem_get`, `elem_set`, …) that BOTH interpreters call, so each op keeps
  exactly one implementation and the tiers cannot drift. Register programs
  carry their own `IcEntry` table with the identical key-verified protocol.
- **Registers are `frame.locals`.** Slots `0..num_locals` are the localized
  bindings (same TDZ marker, same semantics); slots above are the canonical
  homes of the stack machine's operand depths (`canon(d) = num_locals + d`).
  A register frame is an ordinary pooled `Frame` — GC tracing, `arguments`,
  cells, upvalues, and the frame pool all work unchanged.
- **Lazy operands.** A virtual stack of entries abstract-interprets the
  stack code at compile time. `LoadLocal`/`LoadConst`/`LoadThis`&c. emit
  NOTHING — consuming ops read the local register or a `konst` operand
  directly; everything is flushed to canonical form at control-flow edges,
  and any op that writes a local first flushes live lazy references to it
  (locals are provably unaliasable — that is what localization established).
  A forward dataflow (intersection at joins, to a fixpoint) proves most
  local reads can never see the TDZ marker, so they skip the check the
  stack interpreter pays on every `LoadLocal`; unprovable reads emit an
  explicit `TdzCheck`. The `Dup; GetProp(name); Swap` method-call prologue
  fuses to one property op (plus at most one move). Fused stack
  superinstructions map 1:1 onto fused register ops (`CmpBrK`,
  `IncLocal`, `AddCellK`, …), so non-kernel loops keep their one-dispatch
  tests and updates.
- **Whole-function eligibility, tier ordering.** Any op outside the
  translated subset declines the function: `with`/direct-eval scope ops,
  `super`/private class elements, dispose scopes (`using`), suspension
  ops, and `Op::LoopKernel` — a loop-kernelized function keeps the stack
  tier because its unboxed kernels are far faster than boxed register ops.
  The call paths still try `fn_kernel` first, so the tier order per
  activation is: function kernel (unboxed, frameless) → register program
  (boxed, framed) → stack loop. Await-free `async` bodies translate (they
  contain no suspension ops and run to completion in one shot); a real
  `await` declines the body.
- **Try/catch/finally translate** (round 2, same day) — and with them
  for-of loops and array destructuring, whose IteratorClose-on-abrupt-exit
  protocol rides the handler machinery. `ROp::PushTryHandler` records the
  CANONICAL operand depth after a full flush (so the surviving entries —
  a for-of loop's iterator — sit where every unwind expects them); the
  register-mode completion walk (`reg_do_completion`) mirrors
  `do_completion` exactly, with "truncate the operand stack" becoming
  "clear the canonical registers above the handler's depth" (value-
  lifetime parity) and the exception landing in `canon(depth)` — the
  register the catch label's seeded state reads it from. Catch/finally
  labels are seeded in the pre-emission dataflow (catch at `depth + 1`)
  with a conservative all-maybe TDZ set, since a throw can occur anywhere
  in the guarded region. `Op::CompletionJump`/`Op::EndFinally` (break/
  continue/return crossing finallys) translate 1:1; the delegation arms
  cannot occur (generators never translate). A jump target reachable ONLY
  through parked completions (a `while(true)` whose sole exits break
  through a `finally`) has no seeded state and declines the function.
- **Determinism.** The register program is a pure function of the source,
  compiled at compile finish. It polls the cooperative interrupt flag at
  the stack loop's cadence. ~~Like the kernel tiers, the register tier is
  OFF whenever an op budget is installed~~ — superseded by §6.11: the tier
  now carries EXACT per-op stack-unit costs and stays on under a budget,
  so the production server's default `CHIDORI_JS_OP_BUDGET=5e9` runs get
  the register tier too. The kernel tiers still decline under a budget.
- **Native stack discipline.** `run_reg_frame` keeps the hot ops inline and
  delegates everything rare to an `#[inline(never)]` `rstep_cold`, the same
  split (and reason) as `step`/`step_cold`; the depth-overflow differential
  pins that deep recursion through register frames raises the same
  catchable RangeError at the same depth.

**Gates:** a 110-program differential corpus + 300-case deterministic fuzz
(`tests/reg.rs`) require byte-identical output and errors reg-on vs reg-off
across the virtual-stack shuffles (ternaries, logical operators, optional
chains, method calls, compound assignment), TDZ edges, IC hit/miss/exotic
paths, dense-element semantics, every call shape, closures/per-iteration
capture, `arguments` aliasing, mixed-tier throws, the full handler-
machinery surface (rethrow, nested finallys, return-in-finally overriding,
break/continue crossing finally regions, iterator close on break/throw
with throwing/suppressed `return()`), and the decline boundaries; structural pins require the canonical shapes to translate and
the excluded shapes to decline; the op-budget test additionally asserts the
budget drains to the exact same count on both compiles. Full suites for
both crates (118 + 761 tests, incl. record→replay byte-identity) and the
Test262 gate: green. The pass is disabled under the `op-histogram` feature
(register execution would hide per-op counts).

### 6.10.1 Measured (callgrind instruction counts, whole workload)

Reg-on vs reg-off, same commit, release build; RESULT checksums
byte-identical on every workload:

| workload | reg off | reg on | Δ instructions |
| --- | ---: | ---: | ---: |
| string_scan | 521.4 M | 368.3 M | **−29.4%** |
| sort | 2.64 G | 2.07 G | **−21.5%** |
| mixed_helpers (new) | 5.98 G | 5.18 G | **−13.5%** |
| string_build | 183.3 M | 160.1 M | **−12.7%** |
| json_roundtrip | 1.25 G | 1.20 G | −4.3% |
| arith / fib / closures / property / array_hof / array_push_sum | — | — | ±0.003% |

The unchanged rows are exactly the kernel-owned workloads: their hot
functions decline register translation by design (the `+0.003%`-class
deltas are the one-branch `proto.reg` probe per call, the same cost class
as the `fn_kernel` probe). The wins land where the boxed interpreter
itself carries the run — top-level glue around native/kernel calls (sort's
build-and-invoke loop), string-scan/build loops, and `mixed_helpers`,
which is all glue by construction. Geomean over the five
interpreter-carried workloads: **−17%** instructions.

Wall-clock (idle container, 5-run median, execution-only):

| workload | reg off | reg on | speedup |
| --- | ---: | ---: | ---: |
| string_scan | 58 ms | 42 ms | 1.38× |
| sort | 295 ms | 243 ms | 1.21× |
| mixed_helpers | 724 ms | 620 ms | 1.17× |
| string_build | 28 ms | 26 ms | 1.08× |
| json_roundtrip | 154 ms | 147 ms | 1.05× |
| kernel-owned workloads | — | — | 1.0× (noise) |

Handler-machinery spot check (round 2): an ad-hoc for-of/try/destructuring
glue script (`Object.entries` iteration, destructuring in a helper called
per entry, try/catch around a throwing path, try/finally in the loop body —
all shapes round 1 declined) measures **518.9 M vs 646.6 M instructions
(−19.8%)** and 84 ms vs 103 ms wall (1.23×) reg-on vs reg-off, RESULT
byte-identical. The committed suite's rows are unchanged (no committed
workload uses try/for-of in hot code).

`mixed_helpers` is a new workload added with this round: small helper
functions over objects and strings (property traffic across calls, string
building, for-in, ternary classification) — the shape of real agent glue
where every function declines the kernel tiers and the interpreter itself
carries the run. It is the register tier's home turf; the kernel-owned
numeric workloads are untouched by design (their functions decline
translation), and the suite's RESULT checksums are byte-identical reg-on
vs reg-off across the board.

### 6.10.2 What's next (post-register baseline)

- ~~**Try/finally support**~~ — landed same day (see above): for-of,
  array destructuring, and try-wrapped code now translate.
- **Register-mode loop-kernel embedding** — let a function carry BOTH
  kernels and a register program by teaching kernel bail-out to resume at a
  register pc instead of a stack ip.
- ~~**Budget-compatible fast tiers**~~ — the register half landed (§6.11):
  budgeted runs keep the register tier with EXACT drain. The kernel tiers
  (whose fused unboxed regions have no 1:1 stack-op correspondence at
  bail boundaries) still decline under a budget; mapping them stays open.
- Re-run the callgrind sweep before choosing; §1.1's caveats stand.

## 6.11 Recursion-entry + for-in fast paths; the register tier joins budgeted runs (landed 2026-07-11)

Four items from a fresh callgrind review of the two workloads the previous
rounds left richest — mutual_recursion (1.43 G, ~22% per-activation
overhead) and mixed_helpers (4.16 G, ~14% for-in machinery) — plus the
op-budget follow-up §6.10.2 tracked. All safe Rust, zero new dependencies.

1. **Grow-only kernel register buffer.** `Vm::kernel_regs` was cleared and
   re-zeroed per activation, so a 300-deep `isEven` recursion re-zero-filled
   every window on each of 40k outer calls — `Vec::resize` + memset were
   11% of mutual_recursion. The buffer now keeps its high-water length
   across activations and is never re-zeroed: stale `f64`s are unreadable
   by construction (every register is loaded at entry or store-before-read
   by translation proof — the same proofs the kernels already rely on).
2. **Recursion-family cache** (`exec::RecFamily`, `Vm::rec_families`). v9's
   per-activation family resolution allocated ~8 table Vecs and re-scanned
   every member's kernel code (Ret-type + argc cross-checks) per OUTER
   call. The resolved family is now cached keyed by the entry closure's
   identity; a hit re-verifies only the dynamic facts — each recorded
   global SLOT still holds its member (key-verified indexed read, no
   hashing — insertion slots are stable under value writes and appends),
   upvalue self-cells intact, `Math` canonical — and re-snapshots upvalue
   values. Any failure drops the entry and re-resolves from scratch
   (exactly the uncached path), so hit vs miss is unobservable. The
   windowed executor's (return-pc, dst, window) stack is Vm-pooled, and
   the family's tables are read through the cache during execution.
   Bounded (8 entries, MRU); holding member closures across activations is
   the same retention class as the prototype-holder ICs.
3. **for-in fast path** (`Vm::for_in_keys_fast`). Every `for (k in o)`
   built a shadowing `HashSet` and per-chain-level `own_keys` key-clone
   Vecs — so a 5-key plain object also inserted Object.prototype's ~12
   non-enumerable keys into a set that then rehashed and dropped, 60k
   times (~14% of mixed_helpers + a large share of its allocator
   traffic). Now: an ORDINARY receiver's own enumerable string keys come
   straight off its property map (index-likes sorted first, own keys
   unique by construction — no dedup needed for one level), and the
   prototype chain is walked with an allocation-free "contributes any
   enumerable string key?" scan. If nothing contributes (every standard
   prototype), shadowing is irrelevant and the walk is done; a proxy, an
   exotic index source, or any enumerable proto key falls back to the
   generic path unchanged. Gated by an 18-case differential vs Node
   (index ordering, shadowing incl. non-enumerable shadows, null proto,
   mid-program `Object.prototype` injection, arrays/holes/strings,
   enumerable accessors, frozen objects, array-valued protos).
4. **The register tier joins budgeted runs** (`RegProto::costs`). The tier
   previously declined whenever an op budget was installed — so the
   production server's `CHIDORI_JS_OP_BUDGET=5e9` kept every agent run on
   the stack tier, and §6.10's wins never reached production. Each
   register op now carries the EXACT number of final stack-bytecode ops it
   stands for, attributed during translation: elided pure loads accumulate
   into the next ANCHOR op on their path (between a pure stack op and the
   next anchor nothing observable runs, so charging there is exact); a
   fall-through edge into a join sinks stranded units into a pure
   `ROp::Charge`; the fused method-call window anchors at its `GetProp`
   (unit 2 of 3) so a getter still runs — with the same remaining budget —
   when the stack tier would have died exactly at the trailing `Swap`.
   The charge is hard-checked before each op: the budget-exceeded throw
   fires exactly where the stack loop's per-op check would have, no op
   the stack tier would not have reached can run, and every control op
   costs ≥ 1 so termination stays guaranteed. Unbudgeted runs pay one
   predicted branch per op (the stack loop's own `counting` protocol).

   En route this EXPOSED a pre-existing register-tier crash that budgeted
   runs had masked: a zero-arg call/construct whose argument window starts
   one past every touched register (`new`/method-call at virtual-stack
   top, the Test262 `S13.2.2_A10` shape) panicked slicing
   `locals[at..at]` out of range — the process-killing kind of bug the
   conformance gate exists to catch, and it only surfaced because the gate
   (which installs a 50M budget) now exercises the register tier at all.
   Fixed with explicit empty-window guards; the shape is pinned in the reg
   corpus.

### 6.11.1 Measured (callgrind, whole workload, deterministic)

Baseline = the branch point, same toolchain; RESULT checksums
byte-identical across the suite and cross-checked against node/bun by the
benchmark harness:

| workload | before | after | Δ |
| --- | ---: | ---: | ---: |
| mixed_helpers | 4.157 G | 2.953 G | **−29.0%** |
| mutual_recursion | 1.434 G | 1.097 G | **−23.5%** |
| json_roundtrip | 1.193 G | 1.191 G | −0.2% |
| sort | 2.045 G | 2.051 G | +0.3% ¹ |
| string_scan | 353.8 M | 354.9 M | +0.3% ¹ |
| fib_recursive | 673.8 M | 676.5 M | +0.4% ² |
| closures / arith_loop / property_access / array_sum / typed_array / array_hof / array_push_sum / string_build | — | — | unchanged (±0.1%) |

¹ The register loop's `counting` branch — the price of budget
eligibility, paid only by reg-carried workloads.
² One instruction per self-call reading window sizes through the cached
family instead of hoisted locals.

Wall-clock (idle container, indicative per §1.1): mutual_recursion
150 → 113 ms, mixed_helpers 587 → 427 ms.

**Gates:** full suites for both crates green; kernels + reg differential
corpora green (reg corpus grew the zero-arg-window pin); the op-budget
test now genuinely exercises the register tier, joined by TWO new budget
differentials — a full 0..=total budget sweep on a side-effecting program
(every budget value must produce identical console output, completion,
and remaining budget on both compiles) and boundary probes across the
whole reg corpus; the 18-case for-in differential vs Node; Test262 gate
green — now running the register tier under the runner's budget for the
first time, i.e. 40k+ conformance tests newly executing the tier.

## 6.12 Masked kernel windows, kernel const-prop + fusion round 2, and
object-literal templates (landed 2026-07-11)

A fresh callgrind review of the post-§6.11 profiles found three structural
costs, attacked in one round. All safe Rust, zero new dependencies, RESULT
checksums byte-identical across the suite (cross-checked against node/bun by
the benchmark harness).

1. **Masked fixed register windows (`bytecode::KWIN = 32`).** Every kernel
   executor indexed its register file through `Vec<f64>` slice indexing —
   and the resulting bounds checks were **12–17% of every kernel-owned
   workload** (they never predict away: the panic branch sits in the hot
   dispatch loop). Registers now live in fixed `[f64; KWIN]` WINDOWS indexed
   as `regs[(r as usize) & KWIN_MASK]` — translation caps every kernel at
   KWIN registers (observed max: 10) so the mask is an identity on valid
   indices that PROVES the access in-bounds; the compiler drops the check
   and the panic branch entirely, zero `unsafe`. Loop kernels run one window
   (pinned-callee windows at KWIN strides above it, re-split per
   `CallKernel`); plain function kernels run one stack-allocated window (no
   pool traffic at all); the recursive executor strides windows at KWIN with
   a member-level/window-level loop split so same-kernel calls re-derive
   only the window view, never the member tables.
2. **Kernel const-propagation + fusion round 2** (`const_prop_kops`, three
   new superinstructions). The loop translator materialized constants that
   fed two-register ops (`(i*7919) % 10007` ran `Const` + `Arith` where
   `ArithK` exists), so cleanup gained a forward const-prop pass (same
   block discipline as `copy_prop_kops`): `Add`/`Arith`/`BrCmp` operands
   that are known constants rewrite to their `*K` immediate forms
   (mirroring the comparison when the constant is on the left; only
   commutative kinds swap `Arith` operands), `Mov`-of-constant becomes
   `Const`, and dead `Const` defs die in DCE (extended past `Mov`s). Fusion
   gained the shapes the array_sum dump showed dominating: `LenBrCmp` (the
   `i < a.length` header test — LoadLen + BrCmp in one dispatch, with the
   LoadLen entry-guard scan taught to see the fused form), `LoadElemAdd`
   (`s += a[i]`), `LoadElemArith` (the dot-product second load feeding its
   multiply), and `ArithKAdd` (`s += a <op> K` after const-prop). The
   canonical `s += a[i]` loop: 8 dispatches per iteration → 5. En route,
   the `i.fract() == 0.0` integrality probe in every element fast path
   (kernel and interpreter) became a cast round-trip (`dense_index`) —
   `fract` lowers to a libm `trunc` CALL on baseline x86-64, ~3% of
   array-heavy workloads.
3. **Object-literal templates** (`Op::NewObjectTpl`,
   `FuncProto::obj_tpls`). mixed_helpers spent ~12% (inclusive) in
   `define_own_property` + `IndexMap::insert_full`: every `{ id: …, name:
   …, … }` evaluation re-hashed and re-inserted every key through the full
   spec descriptor path — on a fresh ordinary extensible object, where
   none of that machinery can observe anything or fail. The compiler now
   detects all-static-data-key literals (≥2 distinct plain keys; computed
   keys, accessors, methods, spread, `__proto__`, and duplicates keep the
   generic path), pre-builds the property map ONCE at compile time, and
   instantiation CLONES it (bucket layout included — no hashing, no
   growth-rehash, no descriptor validation) and writes the evaluated
   values into slots `0..N`. Value expressions still evaluate in exact
   source order (static keys have no evaluation step, and the object
   cannot be observed mid-literal — no binding exists and every
   template-eligible property is plain data). Both tiers execute it
   (`ROp::NewObjectTpl` mirrors `NewArray`'s canonical window; budget cost
   is its exact 1 stack unit); kernel regions decline it as before. A
   side benefit: all objects from one site share one insertion layout, so
   the key-verified property ICs hit consistently.
4. **Smaller wins along the way**: `op_add` gained a String⊕Number fast
   path (both operands already primitive: format the number's digits
   straight into the once-allocated result buffer via
   `push_number_string` — the exact digits `to_js_string` produces —
   skipping two intermediate allocations per `+`; restricted below the
   rope threshold so big accumulators keep the O(1) rope path);
   `JSON.parse` interns object keys across parses (Vm-level bounded
   cache — repeated same-shaped documents reuse one `Rc<str>` per key)
   and pre-sizes each object's property map (one table allocation instead
   of the 0→3→7 growth); `js_mod`'s integer fast path also dropped its
   libm `trunc` calls.

### 6.12.1 Measured (callgrind, whole workload, deterministic)

Baseline = the branch point, same toolchain (rustc 1.97), same container;
RESULT checksums byte-identical on every workload:

| workload | before | after | Δ |
| --- | ---: | ---: | ---: |
| array_sum | 1.948 G | 1.522 G | **−21.8%** |
| mixed_helpers | 2.953 G | 2.324 G | **−21.3%** |
| string_build | 118.1 M | 94.0 M | **−20.4%** |
| typed_array | 521.0 M | 429.4 M | **−17.6%** |
| property_access | 247.4 M | 204.4 M | **−17.4%** |
| arith_loop | 382.3 M | 322.3 M | **−15.7%** |
| array_push_sum | 282.0 M | 240.0 M | −14.9% |
| closures | 378.4 M | 326.4 M | −13.7% |
| mutual_recursion | 1.097 G | 958.9 M | −12.6% |
| array_hof | 340.2 M | 327.4 M | −3.8% |
| sort | 2.051 G | 1.992 G | −2.9% |
| string_scan | 355.3 M | 351.4 M | −1.1% |
| json_roundtrip | 1.191 G | 1.186 G | −0.4% |
| fib_recursive | 676.5 M | 695.3 M | +2.8% ¹ |

Geomean across the suite: **−12%** instructions; −15 to −22% on the
kernel-owned and object/string-glue workloads the round targeted.

¹ fib runs the recursive windowed executor, where the window-loop
restructure costs a few instructions per call/return crossing against the
masking savings inside the (tiny, ~10-op) activations — the member/window
loop split recovered most of an initial +19%; the residual is the price
the other recursion shapes' wins ride on (mutual_recursion −12.6%).

Wall-clock under the RECOMMENDED (PGO) build — interleaved best-of-7 A/B,
both sides PGO'd on the same corpus, same idle container (plain-release
wall on two workloads swung the OTHER way purely from fat-LTO code layout;
cachegrind showed the new binary better on every simulated metric while
plain-release wall read slower — exactly the layout sensitivity that made
PGO the recommended benchmark build, and why callgrind stays the
load-bearing gate):

| workload | PGO before | PGO after | Δ |
| --- | ---: | ---: | ---: |
| array_sum | 201 ms | 138 ms | **−31%** |
| property_access | 29 ms | 20 ms | **−31%** |
| typed_array | 56 ms | 42 ms | **−25%** |
| arith_loop | 38 ms | 29 ms | **−24%** |
| mixed_helpers | 307 ms | 240 ms | **−22%** |
| mutual_recursion | 93 ms | 80 ms | −14% |
| array_push_sum / closures | 38 / 32 ms | 33 / 28 ms | −13% |
| string_build | 18 ms | 16 ms | −11% |
| string_scan / array_hof / sort / json | — | — | 0 to −5% |
| fib_recursive | 58 ms | 62 ms | +7% |

### 6.12.2 What the profiles say now

- **json_roundtrip barely moved (−0.4%)**: its cost is diffuse allocator
  traffic (~20% malloc/free) across parse-side ObjectData/array/string
  construction and teardown — per-key and per-table tweaks are rounding
  errors against one `Rc<RefCell<ObjectData>>` + map + value allocations
  per node. The structural answer is an ObjectData pool or arena (needs a
  recycle point for Rc-dropped objects) — tracked as the next
  allocation-side lever.
- **mixed_helpers (the "real agent glue" proxy)** is now dominated by the
  register tier's own dispatch (~15%), `Value` drop glue (~8%), and the
  remaining allocator share (~10%) — the object-literal define path and
  the string `+` machinery have left the top ranks. Further movement needs
  either reg-tier superinstructions or a smaller `Value`.
- **sort / array_hof** remain comparator-call-ceremony bound around the
  prepared-kernel paths; **string_scan** is `charCodeAt` loop glue the
  register tier already carries.

**Gates:** full suites for both crates green (chidori-js 129 incl. the new
`obj_tpl.rs` template structural+behavior tests and kernels structural pins
for `LenBrCmp`/`LoadElemAdd`/K-form const-prop; record→replay byte-identity
unchanged); kernels + reg differential corpora and 300-case fuzzes green —
the kernels corpus caught a real bug during development (the fused
`LenBrCmp` escaped the typed-array `length`-shadow entry guard scan, fixed
by teaching the guard the fused form); Test262 gate green (0 regressions,
99.11%); clippy clean across the workspace.

## 6.13 Pinned-string kernels (charCodeAt/length) + allocation diet
(landed 2026-07-11, follow-up round)

Round 2 on the §6.12 baseline: the biggest remaining per-workload gap with a
clear shape was string_scan (~6× behind bun — the tokenizer idiom
`for (i = 0; i < s.length; i++) s.charCodeAt(i)`, §6.5.1 candidate (a),
unlocked by §6.7's O(1) ASCII accessors), plus a first pass at the
allocation costs that dominate json_roundtrip.

1. **Pinned STRING bases in loop kernels** (`Kernel::sslots`,
   `KOp::StrLen`, `KOp::CharCodeAt`). A local used as `s.charCodeAt(i)` /
   `s.length` inside a kernel region is discovered (fixpoint, like array
   bases; string discovery WINS over a same-local array-base guess) and
   PINNED: the entry guard requires a FLAT ASCII string — flattening a
   rope-built accumulator once, cached in the rope node — and, for
   `charCodeAt`, the canonical `String.prototype.charCodeAt` resolution
   (pinned at install like `Array.prototype.push`; a monkeypatched method
   runs generically, observably). Both ops are then TOTAL — no bail path
   exists: strings are immutable and the local is pinned, so `StrLen` is an
   activation constant, and `CharCodeAt` computes ToIntegerOrInfinity
   (NaN→0, truncate-toward-zero via saturating casts — no libm) and the
   out-of-range NaN exactly as the builtin does. Non-ASCII/WTF-8 receivers
   decline the activation into the generic loop.
2. **Allocation diet**: `Internal::Promise`/`Internal::ModuleNamespace`
   carried the enum's largest payloads inline (104/72 bytes) — boxing the
   two cold variants shrinks `Internal` 104→64 and **every `ObjectData`
   184→144 bytes** (~20% less memory traffic per object allocation,
   json_roundtrip's dominant cost). JSON string values now parse straight
   from the source slice into the `Rc` backing (one allocation instead of
   the String round-trip's two).
3. Not taken: a `Vec<[f64; KWIN]>` window pool for the recursive executor
   measured WORSE on instructions than §6.12's slice+`try_into` windows
   (fib 695→713 M) and was reverted — fib's residual +2.8% stands as the
   accepted price of the recursion tier's masked windows. An ObjectData
   pool/arena (the real json lever) needs a recycle point for Rc-dropped
   objects — a GC-design change, still tracked.

### 6.13.1 Measured (callgrind, whole workload; wall best-of-5 plain release)

| workload | §6.12 baseline | after | Δ instructions | wall |
| --- | ---: | ---: | ---: | ---: |
| string_scan | 351.4 M | 145.2 M | **−58.7%** | 46 → 21 ms |
| json_roundtrip | 1.186 G | 1.176 G | −0.8% | 165 → 144 ms |
| mixed_helpers | 2.324 G | 2.318 G | −0.3% | 315 → 273 ms |
| fib_recursive | 695.3 M | 683.2 M | −1.7% | — |
| string_build / others | — | — | unchanged | — |

The wall movement beyond the instruction deltas on json/mixed is the
allocation diet (smaller objects, one less copy per parsed string) — cache
effects callgrind cannot see; plain-release layout noise caveats (§6.12)
apply, but the direction is consistent across repeated interleaved runs.

**Gates:** kernels differential corpus grew 9 pinned-string programs
(rope-built receivers, ToIntegerOrInfinity edges incl. NaN/fractional/
negative/OOB indices, non-ASCII and astral receivers declining to generic,
monkeypatched `charCodeAt` observed, mid-program reassignment re-guarded,
results feeding arithmetic/booleans) plus a structural pin
(`char_code_loops_get_kernels`); a 7-case differential vs Node 22
byte-identical; full suites green for both crates; Test262 gate green
(0 regressions); clippy clean.

## 6.14 Cell-writing function kernels (landed 2026-07-11): the PRNG idiom

Motivated by the CPython benchmark column: on the `sort` workload chidori
trailed every other runtime, and phase isolation showed the sort itself was
already at Bun parity (§6.5's prepared f64 comparator) — the deficit was the
LCG **fill loop**, 300k calls to
`function rnd() { seed = (seed * 1103515245 + 12345) >>> 0; return seed; }`
at ~370 ns each. `rnd` declined the function-kernel tier for exactly one op:
`StoreUpvalueChecked` (function kernels were pure — "nothing inside a kernel
can write globals or cells"). A function that mutates captured state through
a cell is the PRNG/accumulator/counter idiom, common in exactly the glue
code the fn-kernel tier targets.

1. **Translation** (`kernel.rs`): fn mode now accepts
   `StoreUpvalue`/`StoreUpvalueChecked` of a NUMBER — the value moves into
   the upvalue's register (later loads read the same register), and the
   kernel records the write in `Kernel::uv_writes` (`(register, upvalue)`
   pairs). Written registers join the always-live set so the completion
   flush's source survives DCE. The Checked form's TDZ test is statically
   satisfied: the entry guard already requires the cell to hold a Number
   (a TDZ'd cell declines to the generic path, which raises the spec
   ReferenceError). Non-Number stores (boolean/undefined — the flush would
   change the cell's observable type) and loop-mode upvalue stores keep
   declining.
2. **Execution** (`run_fn_kernel`): written cells buffer in registers for
   the whole frameless call — nothing else can run mid-kernel — and flush
   back as `Value::Number` on BOTH completions (return and the interrupt
   unwind, mirroring the loop kernels' local write-back), so the cell holds
   exactly the generic write-through path's state; a conditionally-skipped
   store flushes the entry snapshot back (same value, unobservable). A
   belt-and-braces entry check declines if a written cell aliases another
   captured cell. Both helpers live out of line so write-free kernels' hot
   paths are untouched.
3. **Tier boundaries**: non-empty `uv_writes` declines the recursion tier
   (its family resolution and cache treat cells as activation constants —
   checked at translation for self-recursion and at `build_rec_family` for
   non-recursive helpers pulled into a family), the prepared-callback paths
   (sort/HOF: they snapshot upvalues once per run), and the loop-kernel
   pinned-callee guard (one snapshot per activation). Those shapes run
   generically, exactly as before.

### 6.14.1 Measured (callgrind, whole workload; wall interleaved 6-run median)

| workload | before | after | Δ instructions | wall |
| --- | ---: | ---: | ---: | ---: |
| sort | 1.992 G | 1.554 G | **−22.0%** | 232 → 179 ms |
| sort fill phase only | 1.107 G | 657 M | −40.6% | 166 → 85 ms |
| 300k bare `rnd()` calls | 976 M | 526 M | −46.1% | 118 → 69 ms |
| fib_recursive | 683.2 M | 694.0 M | +1.6% | 65 → 64 ms |
| mutual_recursion | 940.2 M | 958.8 M | +2.0% | 79 → 81 ms |
| array_hof / others | — | — | ±0.1% | — |

Per-call cost of the PRNG shape drops ~370 → ~210 ns (the remaining cost is
the generic caller loop: `LoadGlobal; Call` dispatch around the frameless
body). On the cross-runtime suite the sort row goes from slowest-of-four to
ahead of Node and CPython (~1.5× Bun, from 2.0×). The fib/mutual instruction
upticks are the §6.13-class fat-LTO layout shift (their wall is flat, and
neither workload executes any new code on its hot path); the residual
stands as accepted noise, same as §6.13's window-pool revert.

**What's next here:** the fill loop itself still runs generic bytecode — a
loop kernel can't contain a call to a GLOBAL callee (pinned callees are
local-held only, and cell-writing callees decline the pinned path). Extending
`KOp::CallKernel` to entry-guarded global callees with per-call flush would
kernelize the entire fill loop (the inlined-LCG equivalent measured 26 ms
total, another ~3× on this phase). **Landed as §6.15.**

**Gates:** kernels differential corpus grew 16 cell-writing programs (PRNG,
shared cells across two kernels, conditional/write-only/self-assign stores,
-0/NaN round-trips, boolean/undefined stores declining, call-time TDZ
throwing generically, cell-writing comparator/HOF-callback/self-recursion
declining their tiers, the fill idiom) plus a structural pin
(`cell_writing_functions_get_fn_kernels`); benchmark suite cross-checks
green (all four runtimes agree on every workload); full suites green;
clippy clean.

## 6.15 Global pinned callees, cell-writing callee flush, and the
default-sort key precompute (landed 2026-07-11, follow-up round)

The two follow-ups §6.14 queued: kernelize the sort workload's fill loop
END TO END, and fix the latent no-comparator `Array.prototype.sort` cliff
(868 ms vs Bun's 129 ms on a 6×50k numeric sort — per-comparison ToString).

1. **Pinned GLOBAL callees in loop kernels** ([`KCalleeSrc`]). `LoadGlobal`
   of a non-`Math` name in a loop region now speculates a pinned callee
   ([`VE::GlobalFn`]) — its only legal consumer is a `Call` under an
   `undefined` this, fusing to the existing `KOp::CallKernel`, so a
   mis-speculated non-callee global just fails translation (exactly
   today's decline). The activation entry guard resolves the name the way
   `LoadGlobal`'s fast path does — an own DATA property of the global
   object holding a kernel-carrying closure — and declines
   accessor/proto-inherited/missing bindings to the generic loop (which
   then runs the getter per iteration / throws the ReferenceError
   exactly per spec). The binding is an activation constant: nothing on
   the kernel allowlist can rebind a global, and a `KProp` store aimed at
   the same global-object property can't pass its holds-a-Number entry
   check while this guard sees a closure.
2. **Cell-writing callees in the pinned path.** §6.14's `uv_writes`
   kernels are now admitted by the `CallKernel` guard: the callee window
   still snapshots upvalues once per activation, the written register IS
   the cell's live value across calls (nothing else can touch it
   mid-loop), and the window flushes written cells back after EVERY call
   — so a mid-loop bail (a store target going string-tainted) resumes the
   generic loop against current cell state, and the interrupt unwind
   keeps the generic-prefix contract. Cross-window aliasing (a written
   cell captured by another callee window or snapshot by the caller
   kernel's own upvalue registers) declines the activation — the
   differential corpus pins the caller-reads-the-cell shape staying
   generic.
3. **Default-sort ToString precompute** (`sort_default_primitive`,
   independent of the kernel tiers). The spec's default SortCompare
   stringifies both operands per comparison — ~2·n·log n allocating
   conversions. ToString of a Number/String/Bool/Null/BigInt is pure, so
   an all-primitive snapshot now precomputes the n keys once and sorts an
   index permutation with the IDENTICAL stable take-left merge — same
   output element-for-element, no observable difference (objects keep the
   exact per-comparison path: their toString/valueOf call order and count
   are user-visible; a Symbol element still throws from the comparison).
   All-ASCII keys (number-to-string output always is) compare as bytes
   instead of UTF-16 code units.

### 6.15.1 Measured (callgrind, whole workload; wall interleaved 7-run median)

| workload | §6.14 baseline | after | Δ instructions | wall |
| --- | ---: | ---: | ---: | ---: |
| sort (benchmark workload) | 1.554 G | 1.144 G | **−26.3%** | 175 → 125 ms |
| sort fill phase only | 669.1 M | 259.8 M | **−61.2%** | 85 → 33 ms |
| default-sort probe (6×50k, no comparator) | 9.900 G | 1.618 G | **−83.7%** | 838 → 184 ms |
| fib_recursive | 694.0 M | 694.0 M | ±0.0% | 64 → 66 ms |

Cumulative on the sort benchmark row since §6.13: 1.992 G → 1.144 G
instructions (−42.6%), 232 → 125 ms — on the cross-runtime suite chidori
now posts the FASTEST execution-only sort of all four runtimes (Node
~1.6×, Bun ~1.5×, CPython ~1.0–1.3× behind on the same box). The
no-comparator cliff drops from 6.7× behind Bun to ~1.4×. fib is
bit-identical this round (the §6.14 layout shift did not compound).

**Gates:** kernels differential corpus grew 12 global-callee programs
(args/two-callees/two-sites-min-argc, under-supplied args declining,
rebinding between activations, rebound-to-non-function throwing, accessor
global counting its per-iteration getter calls, cell-writing callee with
post-loop reads, mid-loop bail with a live cell, caller-reads-cell alias
decline) plus a structural pin (`global_callee_loops_get_kernels`); the
default sort gained a smoke pin (`default_sort_precomputed_keys`:
string-order numerics, mixed primitives, -0/NaN, stability, object and
Symbol elements, UTF-16 astral order, toSorted) all verified against Node
per case; benchmark suite cross-checks green on all four runtimes; full
suites green; clippy and rustfmt clean.

## 6.16 Glue-code round: typeof-dispatch fusion, compile-scoped string
interning, String+String add, for-in buffer pool (landed 2026-07-12)

mixed_helpers ran ~2.3× slower than CPython (~255 vs ~112 ms execution) —
by design the workload where every function declines the kernel tiers and
the register interpreter carries everything. Phase isolation split its
~255 ms into normalize/for-in ~120 ms, render/string-build ~66 ms, object
allocation ~41 ms, buckets/classify ~38 ms; the profile showed no cliff,
just the aggregate per-iteration tax (~38.6 k instructions each): dispatch
~30%, `Value` drops + malloc ~13%, string equality ~4.5%, map hashing ~5%.
This round took every bounded lever; the rest is the tracked
arena/GC-design work.

1. **`ROp::TypeofBr`** — the dispatch idiom `typeof v === "number"` fuses
   at register-translation time into a direct type-tag test: no typeof
   string materialized, no `Value` clone/drop, no string compare, no const
   load. Build-time conditions: an equality compare (`===`/`!==`/`==`/
   `!=`) K-folded against a string literal that IS one of the eight typeof
   names, whose operand is the Temp defined by the immediately preceding
   `Unary{Typeof}` (`code.last()` — any interleaving materialization would
   have emitted a later op). The fused-away Unary becomes a `Charge`
   carrying its already-assigned units, so budgeted runs drain at exactly
   the stack tier's points — the first cut deferred those units across the
   branch edge and the reg budget-sweep differential caught it precisely
   (per-path charge divergence). Literals outside the eight names
   (`typeof x === "Number"`) keep the generic compare.
2. **Compile-scoped string interning** (`Compiler::str_intern`): one
   `JsString` allocation per distinct string across a whole compilation —
   const pools AND object-template keys — so a for-in key cloned out of an
   instantiated template pointer-matches `k === "id"` literals and
   property-name consts in any function of the program. The eight typeof
   names route to the process-wide `names.rs` table instead, giving
   unfused/stack-tier `typeof` compares the pointer fast path too.
   Compile-scoped, so nothing is retained across programs.
3. **`op_add` String+String fast path**: both operands already primitive —
   ToPrimitive and ToString are identities — so concatenate directly under
   the same length cap; skipped are two identity-clone ToPrimitive calls
   and two ToString round-trips per join (`label + ":" + v` pays it
   twice).
4. **Pooled for-in key buffers** (`Vm::forin_pool`): a glue loop
   for-inning a fresh object per iteration reused one buffer allocation
   instead of a malloc/free per loop entry; parked cleared at `ForInPop`
   (unwound frames just miss the pool).

Evaluated and REJECTED: a for-in slot-carry for `rec[k]` re-hashing —
the measured cost is ~1.7% of the row and soundness needs a generation
counter on every `ObjectData`, undoing §6.13's size diet; the arena/GC
recycle point remains the real allocation lever (drops + malloc still
~12% after this round).

### 6.16.1 Measured (callgrind, whole workload; wall interleaved medians)

| workload | before | after | Δ instructions | wall |
| --- | ---: | ---: | ---: | ---: |
| mixed_helpers | 2.319 G | 1.996 G | **−13.9%** | 260 → 226 ms |
| normalize/for-in phase probe | 1.504 G | ~1.26 G | −16% | 166 → 142 ms |
| fib_recursive / sort / others | — | — | ±0.001% | unchanged |

The row stays ~2× CPython (~227 vs ~116 ms execution on the same box) —
CPython's dict iteration, type dispatch, and string concat are its own
C-tuned native gait, and parity for chidori on this shape needs the
ObjectData arena or a baseline-compile step, both tracked.

**Gates:** reg differential corpus grew 7 typeof-dispatch programs (every
tag × every value kind, negated/loose/literal-on-the-left forms, a
non-typeof-name literal, non-branch consumers, the LoadGlobalTypeof path)
plus a structural pin (`typeof_branches_fuse`, positive and negative);
the reg budget sweep runs over the new corpus entries (and caught the
first cost-model cut); full suites green; benchmark cross-checks green on
all four runtimes; clippy and rustfmt clean.

## 7. References

- [`docs/interpreter-optimization.md`](./interpreter-optimization.md) —
  Phases 0–2 (measurement, hot-loop cleanup, fusion), the noise-floor
  protocol, and the agent-replay "<1% of live wall-clock" result.
- [`docs/jit.md`](./jit.md) — the retired closure-threading experiment.
- [`docs/resume-performance.md`](./resume-performance.md) — the resume-cost
  caches (transpile/proto/regexp) that motivated "measure, then cache".
- [`docs/replay.md`](./replay.md) — the determinism contract every item here
  is gated against.
- `crates/chidori-js/benchmarks/` — the cross-runtime harness used for §1.1.
