Run expression-only for-loop bodies through a tight per-iteration cycle#2605
Merged
Conversation
A for statement whose body contains only expression statements cannot break, continue, carry labels or declare bindings - so when the frame additionally cannot suspend (no generator/async), is not being debugged, has no per-statement constraint checks and its completion values are dead (function bodies), the per-iteration bookkeeping in ForBodyEvaluation is unobservable: suspension probes, Completion struct churn, per-statement PrepareFor/RunBeforeExecuteStatementChecks and the accumulated-value tracking. Such loops now run a bare test -> body-expressions -> update cycle. Correctness notes: - deferred errors (Engine._error) are converted per body statement via JintStatementList.HandleError, exactly like the statement list does; .NET exceptions propagate to the enclosing list unchanged - the tight path REFERENCES the body's own handler instances (JintBlockStatement.SingleStatement/StatementList) instead of building duplicates: the enclosing function's handler subtree is constructed per evaluation, so ctor-time Build() calls would allocate a fresh subtree per op (measured +264 B/op before, byte-parity after) - constraint-configured engines (MaxStatements etc.) keep the generic path via the frozen ShouldRunBeforeExecuteStatementChecks gate LoopDispatchBenchmarks A/B (default job, adjacent runs): | row | base | tight | | |-------------------|----------|----------|--------| | EmptyLoop | 3.037 ms | 1.046 ms | -65.6% | | VariableBoundLoop | 3.374 ms | 1.248 ms | -63.0% | | CounterAdd | 3.889 ms | 1.933 ms | -50.3% | | LocalCopy | 4.592 ms | 2.805 ms | -38.9% | | StringAppend | 4.486 ms | 2.601 ms | -42.0% | | ComparisonOnly | 4.960 ms | 4.917 ms | flat (if-statement body stays generic) | The loop floor drops from ~30 to ~10 ns/iteration. Allocations flat (+10-20 B/op = the three new handler fields, constructed per op like the rest of the function-body subtree). EvalHotReusedEngine +1.5-3%: observable frames (eval/script top level) still take the generic path; the follow-up dead-completion-value analysis moves them onto the tight path and is gated against exactly that row. Tests pin: function-local computation through the tight path, script top-level completion values staying intact (tight path must not engage), throw propagation with loop state, let-loop reusable-environment interaction, and MaxStatements enforcement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lahma
enabled auto-merge (squash)
July 9, 2026 07:07
This was referenced Jul 13, 2026
This was referenced Jul 20, 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.
What
A
forstatement whose body contains only expression statements cannotbreak,continue, carrylabels or declare bindings — so when the frame additionally cannot suspend (no generator/async), is
not being debugged, has no per-statement constraint checks and its completion values are dead
(function bodies), the per-iteration bookkeeping in
ForBodyEvaluationis unobservable: suspensionprobes,
Completionstruct churn, per-statementPrepareFor/RunBeforeExecuteStatementChecksandaccumulated-value tracking. Such loops now run a bare test → body-expressions → update cycle.
Correctness notes
Engine._error) are converted per body statement viaJintStatementList.HandleError, exactly like the statement list does; .NET exceptions propagateto the enclosing list unchanged
(
JintBlockStatement.SingleStatement/StatementList) instead of building duplicates: theenclosing function's handler subtree is constructed per evaluation, so ctor-time
Build()callswould allocate a fresh subtree per op (measured +264 B/op with duplicates, byte-parity after)
ShouldRunBeforeExecuteStatementChecksgate — pinned by a testResults
LoopDispatchBenchmarks A/B (default job, adjacent runs):
The function-local loop floor drops from ~30 to ~10 ns/iteration. Allocations flat (+10–20 B/op =
three new handler fields, constructed per op like the rest of the function-body subtree).
EvalHotReusedEngine+1.5–3%: observable frames (eval/script top level) still take the genericpath; the follow-up dead-completion-value PR moves them onto the tight path and is gated against
exactly that row.
Tests
Function-local computation through the tight path, script top-level completion values staying intact
(the tight path must not engage there), throw propagation with loop state, let-loop
reusable-environment interaction, MaxStatements enforcement.
Gating
All-TFM build ✅ · Jint.Tests ×2 ✅ · PublicInterface ×2 ✅ · Test262 99,260/0 ✅
🤖 Generated with Claude Code