Memoize slot-cache chain reachability for closure reads#2726
Merged
Conversation
Closure-variable reads at hops 1-3 re-validated their slot location on every access: CanReachAtOuterHop re-probed every intermediate declarative environment (linear slot-name scan + dictionary probe per hop) on each read, ~6% of dromaeo-string-base64-modern (3.5% CanReachAtOuterHop + 2.4% TryResolveNonLocal). A successful walk is now published as an immutable SlotChainMemo pinning the probed chain links by identity plus Engine._envBindingInjectionEpoch, mirroring the global read cache's NestedChainMemo. Steady-state closure reads then validate with a handful of reference compares instead of re-walking. Soundness: a declarative environment's name set is a deterministic function of its defining AST node/definition (every reuse channel re-initializes an instance with the identical name set), the only mutations that grow a pre-existing environment's name set (sloppy direct eval hoisting, AnnexB block-function var copies) bump the injection epoch, deletions only shrink it, and an environment's class is immutable - so pinned "intermediate does not own the name" probes stay false while identity and epoch hold. Validation follows the CURRENT _outerEnv pointers against the pinned links, so pooled environments re-attached under different outers and with-ObjectEnvironments (which break link identity by construction) fall through to the authoritative walk, which also stays in charge of every miss and re-publishes on success. ResolveAndPopulate publishes the memo directly when it lands at hops 1-3 since its traversal proves the same condition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lahma
enabled auto-merge (squash)
July 21, 2026 17:00
lahma
added a commit
that referenced
this pull request
Jul 21, 2026
Single-session re-measure of all lanes on main with #2725/#2726 merged. Extends the campaign notes with the round-2 wins (JSON scan fast paths, closure-read chain memoization) and records the measured-and-dropped direct-dispatch experiment: the builtin-call path is already at its cost floor, so the residual string-row gap is interpreter dispatch itself, not call ceremony. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 22, 2026
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.
Campaign-2 item (follow-up to #2716–#2723). Profiles show closure-variable reads re-validating their slot location on every access —
SlotLocationCache.CanReachAtOuterHop3.5% +TryResolveNonLocal2.4% ondromaeo-string-base64-modern, driven fromJintIdentifierExpression.GetValue(closure-captured lookup tables read in tight loops).Change
Plain per-node
(lastEnv, hop, slot)caching guarded by environment identity alone is unsound — environment shapes are not immutable (sloppy directevalinjects vars into pre-existing function environments; AnnexB block-function semantics create var-scope bindings mid-execution). Instead this extends the guard pattern the codebase already ships for exactly this hazard in the global-read path (NestedChainMemo+Engine._envBindingInjectionEpoch):SlotChainMemopins the probed intermediate chain links by identity plus the injection epoch after a successful hop-1..3 walk. Steady-state closure reads validate with ~5 reference compares + 1 int compare instead of per-hopHasBindingprobes.withinsertion breaks link identity by construction; pooled/reused environments re-initialize with per-definition-deterministic name sets (every reuse channel audited); all dynamic injections bump the epoch (everyCreateMutableBindingcaller audited); deletions only shrink name sets, so a cached "not here" verdict stays valid.Numbers (launchCount-5, both sides, default job otherwise)
Gates
Jint.Tests 3706/3643 (net10/net472) including 6 new soundness tests (alternating closure instances at one callsite, sloppy direct-eval shadowing mid-loop,
witharound closure reads with mid-loop injection, deep nesting past the memo depth, pooled loop environments re-attached across entries); PublicInterface 114/114; Test262 99,431 / 0 failed; CommonScripts 28/28.🤖 Generated with Claude Code