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
83 changes: 83 additions & 0 deletions Jint.Tests/Runtime/LoopBodyFlatteningTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,89 @@ public void ThrowMidBodyLeavesConsistentState()
Assert.Equal("8:1", result); // 0+1+3+4, one catch
}

[Fact]
public void HeaderInitReferencingOuterNameShadowedByBodyReadsOuter()
{
var engine = new Engine();
// the header init reads `r`; the body block declares its own `r`. The names do not overlap
// the header's declared name (`l`), so the old NamesOverlap gate let flattening fold the
// body's `r` slot into the loop env — where the init's `r` then resolved to the
// still-uninitialized slot and threw a spurious TDZ. The init's `r` is an OUTER reference.
var result = engine.Evaluate("""
(function () {
let r = 5, o = 8, seen = [];
for (let l = r; l < o; l++) {
let r = l * 10;
seen.push(r);
}
return seen.join(',');
})()
""").AsString();

Assert.Equal("50,60,70", result);
}

[Fact]
public void HeaderTestReferencingOuterNameShadowedByBodyReadsOuter()
{
var engine = new Engine();
// same shape, but the outer name is read from the loop test rather than the init
var result = engine.Evaluate("""
(function () {
let r = 3, seen = [];
for (let l = 0; l < r; l++) {
let r = l * 10;
seen.push(r);
}
return seen.join(',');
})()
""").AsString();

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

[Fact]
public void HeaderUpdateReferencingOuterNameShadowedByBodyReadsOuter()
{
var engine = new Engine();
// the update reads outer `step`; the body shadows `step`
var result = engine.Evaluate("""
(function () {
let step = 2, seen = [];
for (let l = 0; l < 6; l += step) {
let step = l * 10;
seen.push(step);
}
return seen.join(',');
})()
""").AsString();

Assert.Equal("0,20,40", result);
}

[Fact]
public void TurbopackChunkShapeResolvesModuleFactory()
{
var engine = new Engine();
// reduced from a Turbopack runtime chunk: a header `for (let l = r; ...)` over a body that
// block-scopes `r` and `o` while the header reads the outer `r`/`o` — the exact shape that
// aborted the Next.js bootstrap with "r has not been initialized"
var result = engine.Evaluate("""
(function () {
let e = [0, 0, 'fa', 'fb'];
let t = new Map();
let r = 2, o = 4, n;
for (let l = r; l < o; l++) {
let r = e[l], o = t.get(r);
if (o) { n = o; break; }
}
return 'ok:' + (n === undefined);
})()
""").AsString();

Assert.Equal("ok:true", result);
}

[Fact]
public void UsingDeclarationsKeepBlockDisposeSemantics()
{
Expand Down
47 changes: 46 additions & 1 deletion Jint/Runtime/Interpreter/Statements/JintForStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ public JintForStatement(ForStatement statement) : base(statement)
if (bodyState.SlotNames is { } bodyNames
&& _boundNames!.Count + bodyNames.Length <= 16
&& !HasUsingDeclarations(bodyState)
&& !NamesOverlap(_loopSlotNames!, bodyNames))
&& !NamesOverlap(_loopSlotNames!, bodyNames)
&& !HeaderReferencesAnyBodyName(statement, bodyNames))
{
var headerCount = _loopSlotNames!.Length;
var combinedNames = new Key[headerCount + bodyNames.Length];
Expand Down Expand Up @@ -464,6 +465,50 @@ private static bool NamesOverlap(Key[] headerNames, Key[] bodyNames)
return false;
}

/// <summary>
/// Whether the loop header (init, test, or update) contains an identifier matching a body-block
/// lexical name. Flattening folds the body's let/const slots into the loop environment, so such a
/// name resolves there — but the header scope excludes the body block's declarations, so the
/// reference targets an <b>outer</b> binding the body shadows. Folding it in resolves the header
/// reference against the still-uninitialized body slot, a spurious TDZ ("x has not been
/// initialized"). Over-approximates in the safe direction (a matching property name or a
/// nested-scope declaration only forgoes the optimization), mirroring
/// <c>EnvironmentEscapeAstVisitor.ClosureReferencesAny</c>.
/// </summary>
private static bool HeaderReferencesAnyBodyName(ForStatement statement, Key[] bodyNames)
{
return (statement.Init is not null && ReferencesAnyName(statement.Init, bodyNames))
|| (statement.Test is not null && ReferencesAnyName(statement.Test, bodyNames))
|| (statement.Update is not null && ReferencesAnyName(statement.Update, bodyNames));
}

private static bool ReferencesAnyName(Node node, Key[] names)
{
if (node.Type == NodeType.Identifier)
{
var name = ((Identifier) node).Name;
foreach (var candidate in names)
{
if (string.Equals(candidate.Name, name, StringComparison.Ordinal))
{
return true;
}
}

return false;
}

foreach (var childNode in node.ChildNodes)
{
if (ReferencesAnyName(childNode, names))
{
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