diff --git a/Jint.Tests/Runtime/LoopBodyFlatteningTests.cs b/Jint.Tests/Runtime/LoopBodyFlatteningTests.cs index 82cc631d5..6a27306eb 100644 --- a/Jint.Tests/Runtime/LoopBodyFlatteningTests.cs +++ b/Jint.Tests/Runtime/LoopBodyFlatteningTests.cs @@ -243,4 +243,59 @@ public void UsingDeclarationsKeepBlockDisposeSemantics() Assert.Equal("b0,d0,b1,d1", result); } + + [Fact] + public void ClosureInDestructuringDefaultDeclinesEnvironmentReuse() + { + var engine = new Engine(); + // a closure embedded in a destructuring pattern's DEFAULT value captures the loop's + // initial per-iteration environment; reusing/pooling the environment in place makes it + // observe the final i (or a reset binding) instead of the captured iteration's value + var result = engine.Evaluate(""" + (function () { + const fns = []; + for (let i = 0, { f = () => i } = {}; i < 3; i++) { fns.push(f); } + return fns[0]() + ',' + fns[2](); + })() + """).AsString(); + + Assert.Equal("0,0", result); + } + + [Fact] + public void ClosureInArrayPatternDefaultDeclinesEnvironmentReuse() + { + var engine = new Engine(); + var result = engine.Evaluate(""" + (function () { + const fns = []; + for (let i = 0, [f = () => i] = []; i < 3; i++) { fns.push(f); } + return fns[0]() + ',' + fns[2](); + })() + """).AsString(); + + Assert.Equal("0,0", result); + } + + [Fact] + public void ReenteringLoopWithEscapedPatternClosureKeepsBindingAlive() + { + var engine = new Engine(); + // re-entering the loop must not reset the binding the escaped closure captured + // (a pooled environment would make the earlier closure throw or observe garbage) + var result = engine.Evaluate(""" + (function () { + function run() { + const fns = []; + for (let i = 0, { f = () => i } = {}; i < 2; i++) { fns.push(f); } + return fns; + } + const first = run(); + const second = run(); + return first[0]() + ',' + second[0](); + })() + """).AsString(); + + Assert.Equal("0,0", result); + } } diff --git a/Jint/Runtime/Interpreter/Statements/JintForStatement.cs b/Jint/Runtime/Interpreter/Statements/JintForStatement.cs index 1911c178a..ede5ecb6d 100644 --- a/Jint/Runtime/Interpreter/Statements/JintForStatement.cs +++ b/Jint/Runtime/Interpreter/Statements/JintForStatement.cs @@ -786,6 +786,15 @@ private static bool ForLoopMayCapture(ForStatement statement) { foreach (var decl in vd.Declarations) { + // a destructuring pattern's default values can embed closures too: + // for (let i = 0, {f = () => i} = {}; ...) + if (decl.Id is not Identifier + && (JintFunctionDefinition.EnvironmentEscapeAstVisitor.IsCapturing(decl.Id) + || JintFunctionDefinition.EnvironmentEscapeAstVisitor.MayEscape(decl.Id))) + { + return true; + } + if (decl.Init is not null) { if (JintFunctionDefinition.EnvironmentEscapeAstVisitor.IsCapturing(decl.Init)