Skip to content

Loop kernels: own-frame cell slots — captured loop bounds and accumulators - #101

Open
kvey wants to merge 4 commits into
mainfrom
claude/performance-wins-o4z6pf
Open

Loop kernels: own-frame cell slots — captured loop bounds and accumulators#101
kvey wants to merge 4 commits into
mainfrom
claude/performance-wins-o4z6pf

Conversation

@kvey

@kvey kvey commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

A binding captured by a nested closure stays a heap cell after
localization, and any loop touching one (the captured bound / accumulator
shape: 'let total = 0; rows.forEach(...); for (i = 0; i < total; i++)')
lost its kernel entirely. KSlot::Cell maps such cells into kernel
registers like locals: the entry guard requires Value::Number (TDZ
declines to the generic path), and every exit/bail/interrupt unwind
writes the register back through the RefCell.

Soundness matches the upvalue-snapshot argument: nothing inside a kernel
region can call the capturing closure, so no observer exists between
entry and write-back. The one exception — pinned-closure calls
(KOp::CallKernel), whose callees snapshot upvalues once per activation —
is excluded at translation: a region that WRITES any cell and calls a
pinned closure stays generic (the callee's snapshot could be the very
cell being written).

Corpus: captured bounds/accumulators, IncCellStmt, late entry, string
taint, mid-loop bound reassignment, TDZ reads, the write+callee
exclusion (both aliased and not), bail interleaving, -0. Structural
pins: cell-bound and cell-accumulator loops MUST kernelize; the
write+callee combination must NOT.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01UXTPdiyC5qPFF8Qd678KNj

claude added 4 commits July 10, 2026 06:36
…ators

A binding captured by a nested closure stays a heap cell after
localization, and any loop touching one (the captured bound / accumulator
shape: 'let total = 0; rows.forEach(...); for (i = 0; i < total; i++)')
lost its kernel entirely. KSlot::Cell maps such cells into kernel
registers like locals: the entry guard requires Value::Number (TDZ
declines to the generic path), and every exit/bail/interrupt unwind
writes the register back through the RefCell.

Soundness matches the upvalue-snapshot argument: nothing inside a kernel
region can call the capturing closure, so no observer exists between
entry and write-back. The one exception — pinned-closure calls
(KOp::CallKernel), whose callees snapshot upvalues once per activation —
is excluded at translation: a region that WRITES any cell and calls a
pinned closure stays generic (the callee's snapshot could be the very
cell being written).

Corpus: captured bounds/accumulators, IncCellStmt, late entry, string
taint, mid-loop bound reassignment, TDZ reads, the write+callee
exclusion (both aliased and not), bail interleaving, -0. Structural
pins: cell-bound and cell-accumulator loops MUST kernelize; the
write+callee combination must NOT.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXTPdiyC5qPFF8Qd678KNj
…reads

The canonical tokenizer scan ('for (i = 0; i < s.length; i++)
s.charCodeAt(i)') never kernelized: the receiver is a string, not an
array base, and every charCodeAt was a full generic method call. String
locals now pin into kernel STRING SLOTS, discovered exactly like array
bases (charCodeAt consumption is the string-specific evidence, and it
wins the ambiguous '.length' discovery — an entry-guard mismatch just
declines to the generic path).

Both accesses are BAIL-FREE, unlike array elements: the entry guard
requires the slot to hold a primitive string (immutable, and the local
is pinned) and identity-checks the canonical String.prototype.charCodeAt
(a primitive receiver's lookup goes through an own-index/length wrapper
straight to String.prototype, so nothing else can shadow it — pinned in
the realm at install, like Array.prototype.push). Every Number index
then has a defined result: ToIntegerOrInfinity + code unit in bounds,
NaN out — the builtin's exact computation through the same
JsString::code_unit_at (O(1) on ASCII via the cached unit count).

The activation-pinned string cache also sidesteps a pre-existing
pathology: the per-call receiver clone drops the per-instance Cell unit
count, so a join-built (non-rope) ASCII string paid an O(n) scan per
charCodeAt in the generic path — O(n^2) per loop. Measured (release,
idle): a 12.8 KB join-built scan x200 rounds 39.5 s -> 0.16 s; the
rope-built string_scan benchmark workload 56 -> 25 ms wall (~2.2x).
RESULT lines byte-identical.

Corpus: non-ASCII/astral/lone-surrogate units, OOB/fractional/negative/
NaN indices, empty string, monkeypatched charCodeAt (patch observed),
String-object receivers, two string bases in one region, charCodeAt
feeding Math and array writes, rope-built strings, charAt staying
generic. Structural pins: the scan loop MUST kernelize, charAt must not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXTPdiyC5qPFF8Qd678KNj
Function kernels rejected element access outright ('a bail needs a frame
to resume into'), so the commonest accessor callbacks stayed on the frame
path. Argument-typed READ-ONLY array bases now translate: a[i] element
reads (dense + numeric typed arrays) and dense a.length, with a new
answer to the bail problem — KOp::Abandon. A frameless kernel with array
bases is read-only pure (element stores reject at translation), so an
access missing its dense fast path simply DISCARDS the register-only
activation and the caller reruns the whole call generically, which
performs the exact spec semantics (holes, prototype reads, accessors,
OOB, BigInt elements, non-array receivers).

The compiler's parameter prologue copies LoadArg into locals, so the base
the body reads is a local ALIASING an argument: translation binds each
discovered obj-local to exactly ONE argument slot at its (init-dominated)
prologue store, and every access resolves through args[arg_objs[slot]] at
runtime — no register, no pin, no guard beyond args_used. Excluded by
construction everywhere an abandon has no caller to rerun from or the
argument window carries raw f64s: recursive kernels (rec + arg_objs
rejects), mutual-recursion family members, CallKernel callees, and the
all-f64 sort-comparator specialization all decline arg-objs kernels at
their guards. Typed-array .length abandons too (a prototype accessor no
frameless kernel can guard).

Measured (release, idle): a 4M-call accessor + dot-product workload
1.08 s -> 0.64 s (1.7x); string/cell workloads unchanged. RESULT lines
byte-identical.

Corpus: function+arrow accessors, dot-product loops inside kernelized
bodies, holes/OOB/negative/fractional indices, prototype reads, mixed
element types, string/number/object receivers, typed arrays incl. BigInt
kinds, element stores staying generic, missing/extra arguments, mid-run
mutation, recursion staying generic, reduce-callback usage, Math+array
mix. Structural pins updated: (a, i) => a[i] and (a) => a.length MUST
carry fn kernels; stores and recursive array consumers must not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXTPdiyC5qPFF8Qd678KNj
… args

Close out the §6.5.1 candidate list in docs/js-performance-roadmap.md
(new §6.10 with the design and measured numbers for the three tiers) and
give the two previously uncovered shapes benchmark coverage: a
cell_accumulate workload (captured bounds/accumulators) and a
fn_array_args workload ((a, i) => a[i] accessors + a dot product), in
both the cross-runtime harness and the criterion suite. Refresh the
stale typed-array workload comment (bases accepted since §6.8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXTPdiyC5qPFF8Qd678KNj
@cursor

cursor Bot commented Jul 10, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@github-actions

Copy link
Copy Markdown

chidori-js cross-runtime benchmarks

Same JS workloads run under chidori-js, Node.js, and Bun (5 timed run(s) + 1 warmup each, median reported). All workloads cross-checked to produce identical results.

Startup baselines: chidori 3.1ms · node 28.9ms · bun 13.8ms

Execution-only time (startup baseline subtracted)

workload chidori node bun fastest
arith_loop 33.1ms (8.8×) 3.8ms 4.8ms (1.3×) node
array_hof 42.1ms (3.1×) 25.7ms (1.9×) 13.5ms bun
array_push_sum 28.6ms (1.9×) 16.4ms (1.1×) 14.7ms bun
array_sum 164.4ms (3.9×) 41.9ms 64.1ms (1.5×) node
cell_accumulate 135.8ms (15.4×) 9.5ms (1.1×) 8.8ms bun
closures 25.6ms (12.4×) 2.1ms 3.6ms (1.8×) node
fib_recursive 60.5ms (7.7×) 9.7ms (1.2×) 7.9ms bun
fn_array_args 795.8ms (52.2×) 42.6ms (2.8×) 15.2ms bun
json_roundtrip 142.9ms (5.4×) 53.2ms (2.0×) 26.4ms bun
mutual_recursion 125.5ms (20.2×) 9.6ms (1.5×) 6.2ms bun
property_access 27.0ms (9.5×) 4.1ms (1.4×) 2.8ms bun
sort 290.4ms (4.1×) 111.4ms (1.6×) 71.1ms bun
string_build 22.4ms (7.9×) 6.1ms (2.2×) 2.8ms bun
string_scan 17.6ms (3.5×) 8.0ms (1.6×) 5.0ms bun
typed_array 43.9ms (4.1×) 24.1ms (2.2×) 10.8ms bun

Total time including startup (raw wall-clock)

workload chidori node bun
arith_loop 36.2ms 32.7ms 18.6ms
array_hof 45.2ms 54.6ms 27.3ms
array_push_sum 31.7ms 45.3ms 28.5ms
array_sum 167.5ms 70.8ms 77.9ms
cell_accumulate 138.9ms 38.4ms 22.6ms
closures 28.7ms 30.9ms 17.4ms
fib_recursive 63.5ms 38.6ms 21.7ms
fn_array_args 798.8ms 71.5ms 29.0ms
json_roundtrip 145.9ms 82.1ms 40.2ms
mutual_recursion 128.6ms 38.5ms 20.0ms
property_access 30.0ms 32.9ms 16.6ms
sort 293.4ms 140.3ms 84.9ms
string_build 25.5ms 35.0ms 16.6ms
string_scan 20.7ms 36.9ms 18.8ms
typed_array 47.0ms 53.0ms 24.6ms

Peak memory (subprocess max RSS, median of 3 dedicated run(s))

workload chidori node bun smallest
(startup) 7.2MiB 39.5MiB (5.5×) 31.3MiB (4.4×) chidori
arith_loop 7.2MiB 44.2MiB (6.2×) 40.0MiB (5.6×) chidori
array_hof 19.6MiB 59.4MiB (3.0×) 50.7MiB (2.6×) chidori
array_push_sum 18.6MiB 57.3MiB (3.1×) 55.2MiB (3.0×) chidori
array_sum 16.3MiB 57.9MiB (3.5×) 40.7MiB (2.5×) chidori
cell_accumulate 7.1MiB 43.3MiB (6.1×) 40.8MiB (5.7×) chidori
closures 7.1MiB 43.3MiB (6.1×) 40.2MiB (5.6×) chidori
fib_recursive 7.3MiB 43.3MiB (5.9×) 39.3MiB (5.4×) chidori
fn_array_args 7.4MiB 45.5MiB (6.1×) 42.8MiB (5.8×) chidori
json_roundtrip 8.1MiB 45.2MiB (5.5×) 46.5MiB (5.7×) chidori
mutual_recursion 7.1MiB 44.5MiB (6.2×) 41.7MiB (5.9×) chidori
property_access 7.3MiB 43.3MiB (5.9×) 40.3MiB (5.5×) chidori
sort 10.1MiB 57.4MiB (5.7×) 43.5MiB (4.3×) chidori
string_build 11.4MiB 47.1MiB (4.1×) 38.5MiB (3.4×) chidori
string_scan 8.4MiB 45.6MiB (5.4×) 36.6MiB (4.3×) chidori
typed_array 8.1MiB 46.5MiB (5.8×) 38.4MiB (4.8×) chidori

Numbers are machine- and load-dependent (shared CI runner) — read them as ratios, not absolutes. chidori-js is an interpreter, so it trails the V8/JSC JITs on compute but starts far faster and in far less memory. A ⚠️ marks a workload whose result disagreed across runtimes.

In-process heap utilization (exact bytes, tracking allocator)

chidori-js heap utilization (exact, via tracking global allocator)

== realm: Engine::new() footprint ==
  retained  668.5 KiB   construction peak  668.8 KiB   churn  686.1 KiB in 7959 allocs   825 live objects

== compile: bytecode footprint (parse + lower, no execution) ==
  workload           retained       peak      churn   allocs
  arith_loop          4.0 KiB   41.2 KiB   57.7 KiB      224
  fib_recursive       4.1 KiB   39.8 KiB   53.6 KiB      212
  property_access     5.0 KiB   42.5 KiB   71.1 KiB      295
  array_push_sum      5.1 KiB   42.2 KiB   74.4 KiB      327
  array_hof           7.8 KiB   45.3 KiB   72.1 KiB      398
  string_build        2.8 KiB   38.9 KiB   51.5 KiB      131
  closures            6.8 KiB   43.7 KiB   71.3 KiB      357
  string_scan         6.4 KiB   45.3 KiB   95.3 KiB      441
  typed_array         9.7 KiB   53.1 KiB  157.0 KiB      801
  mutual_recursion    9.0 KiB   49.0 KiB   91.4 KiB      530
  cell_accumulate     6.0 KiB   43.9 KiB   72.8 KiB      287
  fn_array_args      13.9 KiB   57.0 KiB  182.3 KiB     1200

== eval: full run on a fresh engine (peak/retained over the realm baseline) ==
  workload               peak      churn   allocs   retained   after gc
  arith_loop         41.2 KiB   61.5 KiB      256    7.8 KiB    1.6 KiB
  fib_recursive      39.8 KiB   61.8 KiB      282   11.7 KiB    4.5 KiB
  property_access    42.5 KiB   76.3 KiB      340    9.8 KiB    2.4 KiB
  array_push_sum    201.9 KiB  271.3 KiB      380   10.0 KiB    2.5 KiB
  array_hof         132.9 KiB  198.6 KiB      490   13.8 KiB    2.5 KiB
  string_build      356.2 KiB  687.9 KiB    24.2k    7.3 KiB    2.3 KiB
  closures           43.7 KiB   79.5 KiB      431   15.0 KiB    4.2 KiB
  string_scan        68.4 KiB  189.6 KiB     2541   11.3 KiB    2.8 KiB
  typed_array       140.4 KiB  287.9 KiB      855   15.4 KiB    2.6 KiB
  mutual_recursion   97.6 KiB  620.9 KiB    36.0k   97.1 KiB   83.7 KiB
  cell_accumulate    43.9 KiB   78.2 KiB      336   10.9 KiB    2.5 KiB
  fn_array_args      57.0 KiB  213.6 KiB     1272   21.6 KiB    3.5 KiB

== steady_state: 10 repeat runs on one engine (leak check; growth/run should be ~0) ==
  workload           growth/run     peak/run  churn/run
  arith_loop              749 B      2.5 KiB   11.1 KiB
  fib_recursive         1.1 KiB      4.4 KiB   13.0 KiB
  property_access         749 B      3.1 KiB   11.7 KiB
  array_push_sum          749 B    194.7 KiB  203.3 KiB
  array_hof               758 B    123.0 KiB  132.9 KiB
  string_build            749 B    351.4 KiB  643.1 KiB
  closures              1.5 KiB      5.0 KiB   15.4 KiB
  string_scan             758 B     59.8 KiB  100.4 KiB
  typed_array             749 B    128.3 KiB  137.1 KiB
  mutual_recursion      1.6 KiB      6.3 KiB  457.6 KiB
  cell_accumulate         749 B      3.2 KiB   11.8 KiB
  fn_array_args         1.5 KiB     28.9 KiB   39.2 KiB

@github-actions

Copy link
Copy Markdown

Test262 conformance coverage

45448 / 48070 executed pass (94.55%) · 2622 fail · 2540 skip · 50610 total

Area Pass Fail Skip Pass-rate
intl402 1725 1540 54 52.83%
built-ins 20939 933 1771 95.73%
language 22784 149 715 99.35%
Total 45448 2622 2540 94.55%
Per-subdirectory breakdown (66 areas with failures)
Area Pass Fail Skip Pass-rate
built-ins/Temporal 3886 717 0 84.42%
intl402/Temporal 1367 662 0 67.37%
intl402/DateTimeFormat 3 240 1 1.23%
intl402/NumberFormat 135 113 1 54.44%
intl402/DurationFormat 0 110 0 0.00%
intl402/ListFormat 1 79 1 1.25%
intl402/RelativeTimeFormat 0 79 1 0.00%
language/expressions 10508 76 454 99.28%
intl402/Segmenter 2 75 2 2.60%
intl402/Collator 1 63 1 1.56%
built-ins/RegExp 1466 60 353 96.07%
intl402/DisplayNames 4 52 1 7.14%
intl402/Intl 33 33 0 50.00%
language/statements 9278 26 33 99.72%
language/module-code 562 20 17 96.56%
built-ins/Array 2956 14 111 99.53%
built-ins/Object 3392 12 7 99.65%
built-ins/TypedArray 1427 11 8 99.24%
intl402/String 8 11 0 42.11%
built-ins/AsyncFromSyncIteratorPrototype 28 10 0 73.68%
built-ins/Function 466 9 34 98.11%
built-ins/Promise 630 9 64 98.59%
built-ins/TypedArrayConstructors 705 9 24 98.74%
language/global-code 33 9 0 78.57%
built-ins/Date 583 8 3 98.65%
language/eval-code 338 8 1 97.69%
built-ins/encodeURI 24 7 0 77.42%
built-ins/encodeURIComponent 24 7 0 77.42%
built-ins/NativeErrors 82 6 6 93.18%
built-ins/RegExpStringIteratorPrototype 11 6 0 64.71%
intl402/Locale 101 6 45 94.39%
intl402/BigInt 6 5 0 54.55%
built-ins/AsyncDisposableStack 99 4 1 96.12%
built-ins/DisposableStack 88 4 1 95.65%
built-ins/Number 335 4 1 98.82%
built-ins/Proxy 270 4 37 98.54%
intl402/PluralRules 48 4 1 92.31%
built-ins/AsyncGeneratorPrototype 45 3 0 93.75%
built-ins/DataView 545 3 13 99.45%
built-ins/String 1210 3 10 99.75%
built-ins/Symbol 76 3 19 96.20%
intl402/Date 9 3 0 75.00%
language/arguments-object 258 3 2 98.85%
built-ins/Error 53 2 38 96.36%
built-ins/Math 325 2 0 99.39%
built-ins/Reflect 151 2 0 98.69%
built-ins/Set 379 2 2 99.48%
built-ins/parseInt 53 2 0 96.36%
intl402/FallbackSymbol 0 2 0 0.00%
intl402/Number 5 2 0 71.43%
language/literals 449 2 83 99.56%
language/types 109 2 2 98.20%
built-ins/ArrayBuffer 190 1 30 99.48%
built-ins/BigInt 75 1 1 98.68%
built-ins/JSON 141 1 23 99.30%
built-ins/Map 168 1 35 99.41%
built-ins/SharedArrayBuffer 102 1 1 99.03%
built-ins/StringIteratorPrototype 6 1 0 85.71%
built-ins/WeakMap 100 1 40 99.01%
built-ins/WeakSet 83 1 1 98.81%
built-ins/decodeURI 54 1 0 98.18%
built-ins/decodeURIComponent 55 1 0 98.21%
intl402/Array 1 1 0 50.00%
language/destructuring 18 1 0 94.74%
language/function-code 216 1 0 99.54%
language/identifier-resolution 13 1 0 92.86%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants