Reuse a fixed-slot per-iteration environment in for-of/for-in#2586
Merged
Conversation
for-of/for-in with a lexical head previously allocated per ITERATION: a DeclarativeEnvironment, a List<Key> + Key[] from GetBoundNames, dictionary- backed bindings (HybridDictionary + nodes), and a pooled-Reference round-trip from ResolveBinding + InitializeReferencedBinding. The spec creates a fresh binding per iteration with no copy step, so when nothing captures the environment the loop now reuses ONE fixed-slot environment: ResetSlots at iteration start re-establishes TDZ, the single identifier binding initializes straight into its slot, and destructuring initializes into slots by name. Escape analysis (EnvironmentEscapeAstVisitor over the body, and over destructuring default-value expressions) gates eligibility; using/await-using heads (per-iteration dispose resources) and suspendable contexts (generators/async — a pooled env must never cross a suspension) stay on the existing path. The env is pooled across loop entries on the handler with the Interlocked + engine-identity discipline of JintForStatement._cachedLoopEnv, and its stable identity keeps body slot caches hot. Also: HeadEvaluation no longer allocates the TDZ environment when there are no TDZ names (var/assignment forms). for-of over a 1000-element array (fresh engine, 300 loops): 148.9 -> 36.8 MB/iter (-75%), 52.9 -> 35.2 ms/iter (-33%); remaining allocations are iterator-protocol IteratorResult steps, not environments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lahma
enabled auto-merge (squash)
July 6, 2026 16:05
lahma
added a commit
that referenced
this pull request
Jul 6, 2026
#2594) Analysis of #2587 confirmed that everything a prepared script retains after engine disposal is engine-independent prepare-time metadata by design (constant-folded primitives, FDI State, block declaration caches, binding names) and that no Engine/Realm/Environment can be reached from the shared AST. The regression test guarding that invariant predates two environment cache shapes added since: the for-of/for-in per-iteration environment cache on the JintForInForOfStatement handler (#2586) and the Function-constructor definition-level environment parked on the realm-cached dynamic State (#2579). Cover both so a future refactor moving either onto shared AST state fails the test. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 13, 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.
for (const x of arr)/for (const k in obj)previously paid per iteration: a freshDeclarativeEnvironment, aList<Key>+Key[]fromGetBoundNamesinsideBindingInstantiation, dictionary-backed bindings (HybridDictionary+ nodes — the fresh environment has no slot layout), and a pooled-Referenceround-trip throughResolveBinding+InitializeReferencedBinding.The spec creates a fresh binding per iteration with no copy step (unlike
for (let ...)), so when nothing can observe environment identity the loop now reuses one fixed-slot environment:let/const, 1–16 bound names), andEnvironmentEscapeAstVisitorproves nothing in the body — or in a destructuring pattern's default-value expressions — captures or escapes (closures, direct eval).using/await usingheads stay on the existing path (they register per-iteration dispose resources).ResetSlotsre-establishes TDZ from the templates, the single identifier binding initializes straight into its slot, destructuring initializes into slots by name via the existingProcessPatternspath. The environment's stable identity also keeps the body's per-node slot caches hot.Interlocked+ engine-identity discipline ofJintForStatement._cachedLoopEnv; parked (and reset) in thefinallyso the cached env never roots the completed loop's values.HeadEvaluationno longer allocates its TDZ environment when there are no TDZ names (var/assignment forms).Measurements
BenchmarkDotNet default jobs, A/B vs main (with #2585) from separate worktrees:
Allocation census (fresh engine per iteration,
for (const x of arr)over 1000 elements × 300 loops): 148.9 → 36.8 MB/iter (−75%), 52.9 → 35.2 ms/iter (−33%). The environment types (DeclarativeEnvironment,Key[],DictionaryNode[Binding],HybridDictionary,Reference) disappear from the profile entirely; what remains is iterator-protocolIteratorResultsteps — a separate follow-up.Verification
Jint.Testsgreen (net10.0 + net472),Jint.Tests.PublicInterfacegreenusingheads, and labeled break/continue🤖 Generated with Claude Code