Flatten eligible loop-body lexicals into the pooled loop environment#2610
Merged
Merged
Conversation
for (let ...) { const z = ...; ... } paid the block-environment
ceremony on every iteration: unpark/attach the cached env, two
UpdateLexicalEnvironment swaps, park + ResetSlots. When the body
block's bindings are slot-eligible (BlockState.SlotNames non-null
already implies all-lexical declarations, <= 16 bindings and no
escaping closures), contain no using declarations and don't shadow the
loop header's names, the body's slot layout is now concatenated onto
the pooled loop environment. Each iteration re-establishes the body
range's TDZ with a handful of Binding struct copies and runs the block
contents in place (JintBlockStatement.ExecuteFlattenedContents - the
statement list or single statement with the usual error protocol, no
environment work).
Under a debugger the normal block path runs instead; its fresh
environment shadows the unused flattened slots, so the fallback is
semantically inert. Suspendable frames never take the pooled path to
begin with.
A/B (default job, adjacent runs):
| row | before | after | |
|------------------------------|-----------|-----------|--------|
| stopwatch-modern driver | 185.3 ms | 175.2 ms | -5.4% |
| StopwatchBenchmark modern Execute | 175.0 ms | 165.4 ms | -5.5% |
| StopwatchBenchmark modern Prepared | 168.5 ms | 159.0 ms | -5.6% |
| StopwatchBenchmark classic (both) | 152.2/154.5 | 152.4/150.0 | flat |
| stopwatch classic driver | 168.3 ms | 168.5 ms | flat |
ForBencmark guard rows resolved flat via --launchCount 5
(ReenteredInnerLetLoop 5-launch mean 48.16 ms == base 48.09; its body
has no lexicals, so flattening never arms there). Allocations
byte-identical everywhere; modern rows drop a Gen0 bucket.
Tests pin the stopwatch-modern shape, per-iteration const freshness,
TDZ re-established across continue-skipped declarations, header-name
shadowing (declined), capturing bodies (declined via escape analysis),
throw-in-body state, and using-declaration dispose ordering (declined).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lahma
enabled auto-merge (squash)
July 9, 2026 11:51
lahma
added a commit
that referenced
this pull request
Jul 10, 2026
…ations (#2623) The tight loop previously admitted only expression-statement bodies, so the stopwatch-shaped body (var decl + 4-way else-if chain calling tiny closures + two member-read var decls) paid full per-statement ceremony: PrepareFor, Completion materialization, statement-list bookkeeping and the nested if suspension probes, 391k times per benchmark op. The body shape predicate now admits expression statements, var/let/const declarations (using/await-using excluded) and if/else chains over such statements, including declaration-free brace blocks. Control flow that routes completions (break/continue/return/labels/loops/switch/try), other declarations and AnnexB function-declaration branches still disqualify structurally, so every admitted completion is Normal by construction and the lane needs no completion routing. Dispatch happens through a new virtual JintStatement.ExecuteDiscarded (default: full Execute) with lean overrides for expression statements, if statements (unboxed GetBooleanValue test, no suspension-range checks, no UpdateEmpty), variable declarations (slot lane / cached-global route already end in Empty completions) and declaration-free nested blocks. Deferred errors keep the existing per-statement Engine._error poll protocol; statements after a deferred error do not run. Bodies with lexical declarations engage only when #2610 flattening is active: the pooled loop env carries the body slots and the tight loop re-establishes their TDZ per iteration exactly like the generic flattened arm. Rider: JintConditionalExpression now takes the unboxed GetBooleanValue path for its test in non-suspendable frames. Claude-Session: https://claude.ai/code/session_01BY8HoKam5H8vTPn1bMY2Hv Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 13, 2026
This was referenced Jul 18, 2026
lahma
pushed a commit
that referenced
this pull request
Jul 18, 2026
…#2709) Loop-body lexical flattening (#2610) folds an eligible body block's let/const bindings into the pooled loop environment. The eligibility gate (NamesOverlap) only checked body names against the header's declared names, missing the case where a body name shadows an outer binding a header expression references: for (let l = r; l < o; l++) { let r = e[l], o = t.get(r); ... } The header's r/o resolve to outer bindings, but flattening folded the body's r/o slots into the loop env, so the header read them while still uninitialized -> "r has not been initialized". V8 runs it fine. This is the shape in Turbopack's module-factory runtime, so it aborted Turbopack/Next.js renders. Add HeaderReferencesAnyBodyName, declining to flatten when any body slot name appears as an identifier in init/test/update. Over-approximates in the safe direction, mirroring EnvironmentEscapeAstVisitor.ClosureReferencesAny. The scan runs once at construction, so there is no per-iteration cost, and flattening still applies to headers that reference no shadowed name. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.
What
for (let ...) { const z = ...; ... }paid the block-environment ceremony on every iteration:unpark/attach the cached env, two
UpdateLexicalEnvironmentswaps, park +ResetSlots. When thebody block's bindings are slot-eligible (
BlockState.SlotNamesnon-null already implies all-lexicaldeclarations, ≤16 bindings and no escaping closures), contain no
usingdeclarations and don'tshadow the loop header's names, the body's slot layout is now concatenated onto the pooled loop
environment. Each iteration re-establishes the body range's TDZ with a handful of
Bindingstructcopies and runs the block contents in place — no environment work.
Under a debugger the normal block path runs instead (its fresh environment shadows the unused
flattened slots, so the fallback is semantically inert); suspendable frames never take the pooled
path to begin with.
Results
A/B (default job, adjacent runs):
ForBencmark guard rows resolved flat via
--launchCount 5(ReenteredInnerLetLoop 5-launch mean48.16 ms ≡ base 48.09 — its body has no lexicals, so flattening never arms there). Allocations
byte-identical everywhere; the modern rows drop a Gen0 bucket.
Tests
The stopwatch-modern shape, per-iteration
constfreshness, TDZ re-established acrosscontinue-skipped declarations, header-name shadowing (declined), capturing bodies (declined viaescape analysis), throw-in-body state, and
using-declaration dispose ordering (declined).Gating
All-TFM build ✅ · Jint.Tests ×2 ✅ · PublicInterface ×2 ✅ · Test262 99,426/0 ✅ (re-run in full
after rebasing onto #2607, which touches adjacent break/label semantics; one Atomics.waitAsync
wall-clock flake in an earlier run passed 8/8 in isolation)
🤖 Generated with Claude Code