Loop kernels: own-frame cell slots — captured loop bounds and accumulators - #101
Open
kvey wants to merge 4 commits into
Open
Loop kernels: own-frame cell slots — captured loop bounds and accumulators#101kvey wants to merge 4 commits into
kvey wants to merge 4 commits into
Conversation
…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
|
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. |
chidori-js cross-runtime benchmarksSame 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)
Total time including startup (raw wall-clock)
Peak memory (subprocess max RSS, median of 3 dedicated run(s))
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 In-process heap utilization (exact bytes, tracking allocator) |
Test262 conformance coverage45448 / 48070 executed pass (94.55%) · 2622 fail · 2540 skip · 50610 total
Per-subdirectory breakdown (66 areas with failures)
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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