Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions Jint.Tests/Runtime/LoopBodyFlatteningTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
namespace Jint.Tests.Runtime;

/// <summary>
/// Pins the loop-body lexical flattening (JintForStatement): an eligible body block's let/const
/// bindings fold into the pooled loop environment, and the semantics must be indistinguishable
/// from the per-iteration block environment.
/// </summary>
public class LoopBodyFlatteningTests
{
[Fact]
public void StopwatchModernShapeComputesCorrectly()
{
var engine = new Engine();
// the stopwatch-modern inner-loop shape: let header, const body bindings, if/else chain
var result = engine.Evaluate("""
(function () {
let hits = 0;
for (let x = 0; x < 8; x++) {
const z = x ^ 1;
const doubled = z * 2;
if (z % 2 == 0) { hits += doubled; }
else if (z % 3 == 0) { hits += 100; }
}
return hits;
})()
""").AsNumber();

// z values: 1,0,3,2,5,4,7,6 → even z (0,2,4,6) add 2z = 0+4+8+12 = 24; z=3 adds 100
Assert.Equal(124, result);
}

[Fact]
public void ConstIsFreshPerIterationAndTdzReestablished()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var seen = [];
for (let i = 0; i < 3; i++) {
try { touch; } catch (e) { seen.push('tdz' + i); }
const touch = i * 10;
seen.push(touch);
}
return seen.join(',');
})()
""").AsString();

Assert.Equal("tdz0,0,tdz1,10,tdz2,20", result);
}

[Fact]
public void ContinueReestablishesTdzForSkippedDeclarations()
{
var engine = new Engine();
// continue jumps before `late` is initialized; the next iteration must still see TDZ,
// not the previous iteration's value
var result = engine.Evaluate("""
(function () {
var seen = [];
for (let i = 0; i < 4; i++) {
const early = i;
if (i % 2 == 0) { continue; }
try { seen.push(late); } catch (e) { seen.push('tdz'); }
const late = 'v' + i;
seen.push(late, early);
}
return seen.join(',');
})()
""").AsString();

Assert.Equal("tdz,v1,1,tdz,v3,3", result);
}

[Fact]
public void ShadowingHeaderNameKeepsBlockScoping()
{
var engine = new Engine();
// body const shadows the header let: names overlap, flattening must decline and the
// block env must keep proper shadowing semantics
var result = engine.Evaluate("""
(function () {
var seen = [];
for (let v = 0; v < 3; v++) {
seen.push(v);
{
const v = 'inner';
seen.push(v);
}
}
var direct = [];
for (let w = 0; w < 2; w++) {
const w2 = w + 10;
direct.push(w2);
}
return seen.join(',') + '|' + direct.join(',');
})()
""").AsString();

Assert.Equal("0,inner,1,inner,2,inner|10,11", result);
}

[Fact]
public void CapturingBodyKeepsPerIterationSemantics()
{
var engine = new Engine();
// closures capture the body const: escape analysis must exclude flattening (and env
// reuse), so each captured binding is distinct
var result = engine.Evaluate("""
(function () {
const fns = [];
for (let i = 0; i < 3; i++) {
const snapshot = i * 2;
fns.push(() => snapshot);
}
return fns.map(f => f()).join(',');
})()
""").AsString();

Assert.Equal("0,2,4", result);
}

[Fact]
public void ThrowMidBodyLeavesConsistentState()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var caught = 0, total = 0;
for (let i = 0; i < 5; i++) {
try {
const val = i;
if (i === 2) { throw new Error('x'); }
total += val;
} catch (e) {
caught++;
}
}
return total + ':' + caught;
})()
""").AsString();

Assert.Equal("8:1", result); // 0+1+3+4, one catch
}

[Fact]
public void UsingDeclarationsKeepBlockDisposeSemantics()
{
var engine = new Engine();
// using declarations need block-exit dispose per iteration: flattening must decline
var result = engine.Evaluate("""
(function () {
const order = [];
for (let i = 0; i < 2; i++) {
using res = { [Symbol.dispose]() { order.push('d' + i); } };
order.push('b' + i);
}
return order.join(',');
})()
""").AsString();

Assert.Equal("b0,d0,b1,d1", result);
}
}
10 changes: 10 additions & 0 deletions Jint/Runtime/Interpreter/Statements/JintBlockStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ internal sealed class JintBlockStatement : JintStatement<NestedBlockStatement>
// exposed so enclosing loop fast paths can reuse the exact handler instances of this block
internal JintStatement? SingleStatement => _singleStatement;
internal JintStatementList? StatementList => _statementList;
internal BlockState State => _blockState;

// Reuse cache for this block's fixed-slot environment. Held on the handler instance — which is built
// per statement list, i.e. per engine — rather than on the shared BlockState: BlockState lives on the
Expand Down Expand Up @@ -323,6 +324,15 @@ private void SetupDisposeSuspension(Engine engine, AsyncFunctionInstance asyncFn
PromiseOperations.PerformPromiseThen(engine, promise, onFulfilled, onRejected, null!);
}

/// <summary>
/// Executes the block's contents against the CURRENT lexical environment, without creating,
/// attaching or parking the block environment. For enclosing-loop flattening only: the caller
/// owns an environment that already carries this block's slot layout (re-TDZ'd per iteration)
/// and has verified there are no dispose resources to run.
/// </summary>
internal Completion ExecuteFlattenedContents(EvaluationContext context)
=> _statementList is not null ? _statementList.Execute(context) : ExecuteSingle(context);

private Completion ExecuteSingle(EvaluationContext context)
{
Completion blockValue;
Expand Down
97 changes: 94 additions & 3 deletions Jint/Runtime/Interpreter/Statements/JintForStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ internal sealed class JintForStatement : JintStatement<ForStatement>
private readonly JintExpressionStatement? _tightSingleStatement;
private readonly JintStatementList? _tightBodyList;

// Body-lexical flattening: when the body block's let/const bindings are slot-eligible
// (no function/class declarations, no escaping closures — BlockState.SlotNames implies both),
// contain no using declarations and don't shadow the loop header's names, they fold into the
// pooled loop environment. The body then runs against that environment directly, eliding the
// block-env attach/swap/park ceremony per iteration; the body's slot range is re-TDZ'd before
// each iteration instead.
private readonly bool _bodyFlattened;
private readonly int _flattenedHeaderSlotCount;
private readonly JintBlockStatement? _flattenedBodyBlock;

private readonly bool _shouldCreatePerIterationEnvironment;
private readonly bool _canReuseIterationEnvironment;

Expand Down Expand Up @@ -100,6 +110,30 @@ public JintForStatement(ForStatement statement) : base(statement)
_incrementCanDiscard = _increment.HasDiscardFastPath;
}

if (_canPoolLoopEnv && _body.BlockStatement is { } flattenCandidate)
{
var bodyState = flattenCandidate.State;
if (bodyState.SlotNames is { } bodyNames
&& _boundNames!.Count + bodyNames.Length <= 16
&& !HasUsingDeclarations(bodyState)
&& !NamesOverlap(_loopSlotNames!, bodyNames))
{
var headerCount = _loopSlotNames!.Length;
var combinedNames = new Key[headerCount + bodyNames.Length];
var combinedTemplates = new Binding[combinedNames.Length];
System.Array.Copy(_loopSlotNames, combinedNames, headerCount);
System.Array.Copy(_loopSlotTemplates!, combinedTemplates, headerCount);
System.Array.Copy(bodyNames, 0, combinedNames, headerCount, bodyNames.Length);
System.Array.Copy(bodyState.SlotTemplates!, 0, combinedTemplates, headerCount, bodyNames.Length);

_loopSlotNames = combinedNames;
_loopSlotTemplates = combinedTemplates;
_flattenedHeaderSlotCount = headerCount;
_flattenedBodyBlock = flattenCandidate;
_bodyFlattened = true;
}
}

if (_test is not null && IsTightBodyShape(statement.Body))
{
_tightBodyEligible = true;
Expand Down Expand Up @@ -255,7 +289,9 @@ protected override Completion ExecuteInternal(EvaluationContext context)
}
}

completion = ForBodyEvaluation(context, suspendData?.AccumulatedValue ?? JsValue.Undefined, skipTestOnce: resumingInBody, resumeUpdateOnce: resumingInUpdate);
// body flattening engages only when the pooled combined-slot environment is live
var flattenActive = _bodyFlattened && loopEnv is not null && _canPoolLoopEnv && suspendable is null;
completion = ForBodyEvaluation(context, suspendData?.AccumulatedValue ?? JsValue.Undefined, skipTestOnce: resumingInBody, resumeUpdateOnce: resumingInUpdate, flattenActive);
return completion;
}
finally
Expand Down Expand Up @@ -319,6 +355,48 @@ private static void ResetSlots(Binding[] slots, Binding[] templates)
}
}

/// <summary>
/// Re-establishes the TDZ of the flattened body's slot range before an iteration; the header
/// range is left untouched (a reused iteration environment keeps its header bindings).
/// </summary>
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
private static void ResetBodySlotRange(Binding[] slots, Binding[] templates, int headerCount)
{
for (var i = headerCount; i < templates.Length; i++)
{
slots[i] = templates[i];
}
}

private static bool HasUsingDeclarations(JintBlockStatement.BlockState state)
{
foreach (var declaration in state.Declarations)
{
if (declaration.Declaration is VariableDeclaration { Kind: VariableDeclarationKind.Using or VariableDeclarationKind.AwaitUsing })
{
return true;
}
}

return false;
}

private static bool NamesOverlap(Key[] headerNames, Key[] bodyNames)
{
foreach (var bodyName in bodyNames)
{
foreach (var headerName in headerNames)
{
if (bodyName == headerName)
{
return true;
}
}
}

return false;
}

/// <summary>
/// Checks if the given node is inside this for statement's body, test, or update (but NOT init).
/// Used to determine if we're resuming from a yield/await inside the loop.
Expand Down Expand Up @@ -362,7 +440,7 @@ private bool IsNodeInsideForStatementExcludingInit(Node node)
/// <summary>
/// https://tc39.es/ecma262/#sec-forbodyevaluation
/// </summary>
private Completion ForBodyEvaluation(EvaluationContext context, JsValue initialValue, bool skipTestOnce, bool resumeUpdateOnce)
private Completion ForBodyEvaluation(EvaluationContext context, JsValue initialValue, bool skipTestOnce, bool resumeUpdateOnce, bool flattenActive = false)
{
var v = initialValue;

Expand Down Expand Up @@ -416,7 +494,20 @@ private Completion ForBodyEvaluation(EvaluationContext context, JsValue initialV
var suspendable = context.Engine.ExecutionContext.Suspendable;
if (!resumeUpdateOnce)
{
var result = _body.Execute(context);
Completion result;
if (flattenActive && !context.DebugMode)
{
// the pooled loop environment carries the body's slots: re-establish their TDZ
// and run the block contents in place. Under a debugger the normal block path
// runs instead — its fresh env shadows the (uninitialized) flattened slots.
var env = (DeclarativeEnvironment) context.Engine.ExecutionContext.LexicalEnvironment;
ResetBodySlotRange(env._slots!, _loopSlotTemplates!, _flattenedHeaderSlotCount);
result = _flattenedBodyBlock!.ExecuteFlattenedContents(context);
}
else
{
result = _body.Execute(context);
}
if (!result.Value.IsEmpty)
{
v = result.Value;
Expand Down
Loading