From e842f4f7bd195ab3ed485e38f36cd919d6bfd399 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Tue, 16 Jun 2026 12:49:33 -0700 Subject: [PATCH] Fix compiled async/async-gen/async-arrow block-scope shadow leaks (#766) + async-gen nested-block regression tests (#768) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #766 (compiled async functions, async generators, async arrows): wire the reusable GeneratorBlockScopeRenamer (#711) into AsyncStateAnalyzer/ AsyncMoveNextEmitter, AsyncGeneratorStateAnalyzer/AsyncGeneratorMoveNextEmitter, and the hand-rolled async-arrow analysis (AnalyzeAsyncArrow) + AsyncArrowMoveNextEmitter. A nested-block let/const that shadows an enclosing body-level binding (or parameter) now gets its own per-binding storage instead of aliasing the outer binding's hoisted state-machine field. Each analyzer keys hoisting by storage name; each emitter retokens the declaration/reference/operator nodes before resolution (a renamed binding is never a captured/DC name, so the existing function-DC routing falls through unchanged). Added a Compute(Expr.ArrowFunction) overload to the renamer and an optional BlockScopeRenames field to the async analyses. New emitter overrides registered in EmitterSyncTests.AllowedOverrides. #768 (interpreter async generator: nested-block const/let read at a yield before the first await): already resolved on main by the merged lazy-coroutine async-generator rewrite (which drives the body through the real Interpreter.ExecuteBlockAsync, so nested blocks scope correctly). This PR adds regression tests confirming and guarding it in both interpreter and compiler. (An earlier revision of this branch fixed the older eager-drain driver directly; dropped on rebase onto current main, where the rewrite supersedes it.) #767 (closure-captured shadow) intentionally deferred: a correct fix needs the program-wide ClosureAnalyzer + name-keyed display classes to understand block-scope renames — out of scope here (astronomically rare, low priority). Pinpointed blocker documented on the issue. Tests: new AsyncBlockScopeShadowTests (18, interpreter + compiler). Touched-area sweep (async/generator/block-scope/emitter-sync/yield) 1899 passing. --- .../AsyncArrowMoveNextEmitter.Variables.cs | 57 +++++ Compilation/AsyncArrowMoveNextEmitter.cs | 4 + ...AsyncGeneratorMoveNextEmitter.Variables.cs | 60 +++++ ...AsyncGeneratorStateAnalyzer.Expressions.cs | 18 +- .../AsyncGeneratorStateAnalyzer.Statements.cs | 12 +- Compilation/AsyncGeneratorStateAnalyzer.cs | 23 +- ...yncMoveNextEmitter.Statements.Variables.cs | 60 +++++ Compilation/AsyncStateAnalyzer.Expressions.cs | 24 +- Compilation/AsyncStateAnalyzer.Statements.cs | 14 +- Compilation/AsyncStateAnalyzer.cs | 24 +- Compilation/GeneratorBlockScopeRenamer.cs | 41 +++- Compilation/ILCompiler.Async.cs | 65 +++-- SharpTS.Tests/Compilation/EmitterSyncTests.cs | 37 ++- .../SharedTests/AsyncBlockScopeShadowTests.cs | 227 ++++++++++++++++++ 14 files changed, 601 insertions(+), 65 deletions(-) create mode 100644 SharpTS.Tests/SharedTests/AsyncBlockScopeShadowTests.cs diff --git a/Compilation/AsyncArrowMoveNextEmitter.Variables.cs b/Compilation/AsyncArrowMoveNextEmitter.Variables.cs index a2e46190..e1a1c31d 100644 --- a/Compilation/AsyncArrowMoveNextEmitter.Variables.cs +++ b/Compilation/AsyncArrowMoveNextEmitter.Variables.cs @@ -6,8 +6,21 @@ namespace SharpTS.Compilation; public partial class AsyncArrowMoveNextEmitter { + // Per-binding storage names for block-scoped let/const shadows (#766), shared with the analyzer via + // the analysis. Empty for the common no-shadow case (and for expression-bodied arrows). + private static readonly IReadOnlyDictionary NoRenames = new Dictionary(); + private IReadOnlyDictionary BlockScopeRenames => _analysis.BlockScopeRenames ?? NoRenames; + + private static Token RenameToken(Token original, string lexeme) => + new(original.Type, lexeme, original.Literal, original.Line, original.Start); + protected override void EmitVariable(Expr.Variable v) { + // Resolve a shadowing block-scoped binding to its own storage before resolution (#766); a renamed + // binding is never a captured/DC name, so the capture checks below correctly fall through. + if (BlockScopeRenames.TryGetValue(v, out var renamed)) + v = v with { Name = RenameToken(v.Name, renamed) }; + string name = v.Name.Lexeme; // A capture promoted into the enclosing function's display class (#625) is read through @@ -90,6 +103,9 @@ protected override void EmitVariable(Expr.Variable v) protected override void EmitAssign(Expr.Assign a) { + if (BlockScopeRenames.TryGetValue(a, out var renamed)) + a = a with { Name = RenameToken(a.Name, renamed) }; + string name = a.Name.Lexeme; EmitExpression(a.Value); @@ -354,4 +370,45 @@ private void EmitLoadOuterFunctionDC() _il.Emit(OpCodes.Unbox, _builder.OuterStateMachineType!); _il.Emit(OpCodes.Ldfld, _ctx!.OuterFunctionDCField!); } + + // Const declarations, compound/logical assignment, and increment/decrement reach the variable + // through the operator node's name token (or the increment operand). Rewriting that token to the + // shadowing binding's storage name before delegating to the base routes both the read and the + // write to the right field/local (#766). The base re-enters EmitVarDeclaration / EmitVariable / + // EmitStoreVariable (all of which resolve by name here), so the rename flows through consistently. + + protected override void EmitConstDeclaration(Stmt.Const c) + { + if (BlockScopeRenames.TryGetValue(c, out var renamed)) + c = c with { Name = RenameToken(c.Name, renamed) }; + base.EmitConstDeclaration(c); + } + + protected override void EmitCompoundAssign(Expr.CompoundAssign ca) + { + if (BlockScopeRenames.TryGetValue(ca, out var renamed)) + ca = ca with { Name = RenameToken(ca.Name, renamed) }; + base.EmitCompoundAssign(ca); + } + + protected override void EmitLogicalAssign(Expr.LogicalAssign la) + { + if (BlockScopeRenames.TryGetValue(la, out var renamed)) + la = la with { Name = RenameToken(la.Name, renamed) }; + base.EmitLogicalAssign(la); + } + + protected override void EmitPrefixIncrement(Expr.PrefixIncrement pi) + { + if (pi.Operand is Expr.Variable v && BlockScopeRenames.TryGetValue(v, out var renamed)) + pi = pi with { Operand = v with { Name = RenameToken(v.Name, renamed) } }; + base.EmitPrefixIncrement(pi); + } + + protected override void EmitPostfixIncrement(Expr.PostfixIncrement poi) + { + if (poi.Operand is Expr.Variable v && BlockScopeRenames.TryGetValue(v, out var renamed)) + poi = poi with { Operand = v with { Name = RenameToken(v.Name, renamed) } }; + base.EmitPostfixIncrement(poi); + } } diff --git a/Compilation/AsyncArrowMoveNextEmitter.cs b/Compilation/AsyncArrowMoveNextEmitter.cs index 1d32047a..96909326 100644 --- a/Compilation/AsyncArrowMoveNextEmitter.cs +++ b/Compilation/AsyncArrowMoveNextEmitter.cs @@ -270,6 +270,10 @@ protected override void RegisterLoopLocal(string name, LocalBuilder local) protected override void EmitVarDeclaration(Stmt.Var v) { + // Route a shadowing block-scoped binding to its own storage (#766). + if (BlockScopeRenames.TryGetValue(v, out var renamed)) + v = v with { Name = RenameToken(v.Name, renamed) }; + if (v.Initializer != null) { EmitExpression(v.Initializer); diff --git a/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs b/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs index 1cc8369d..54ee6c6d 100644 --- a/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs +++ b/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs @@ -9,6 +9,14 @@ public partial class AsyncGeneratorMoveNextEmitter // capturing arrow inside the async generator body gets its $functionDC threaded in (#725). protected override FieldBuilder? GetFunctionDCField() => _builder.FunctionDCField; + // Per-binding storage names for block-scoped let/const shadows (#766), shared with the analyzer via + // the analysis. Empty for the common no-shadow case (and for analyses built without the renamer). + private static readonly IReadOnlyDictionary NoRenames = new Dictionary(); + private IReadOnlyDictionary BlockScopeRenames => _analysis.BlockScopeRenames ?? NoRenames; + + private static Token RenameToken(Token original, string lexeme) => + new(original.Type, lexeme, original.Literal, original.Line, original.Start); + /// /// Routes reads and writes of a captured-AND-mutated async-generator local/parameter through the /// shared function display class (#725) so a write inside an arrow/callback and the generator body @@ -42,6 +50,11 @@ private void StoreToDCField(FieldBuilder dcField) protected override void EmitVariable(Expr.Variable v) { + // Resolve a shadowing block-scoped binding to its own storage before any DC routing (#766); + // a renamed binding is never a captured/DC name, so the DC check below correctly falls through. + if (BlockScopeRenames.TryGetValue(v, out var renamed)) + v = v with { Name = RenameToken(v.Name, renamed) }; + if (TryGetFunctionDCField(v.Name.Lexeme, out var dcField)) { _il.Emit(OpCodes.Ldarg_0); @@ -56,6 +69,9 @@ protected override void EmitVariable(Expr.Variable v) protected override void EmitVarDeclaration(Stmt.Var v) { + if (BlockScopeRenames.TryGetValue(v, out var renamed)) + v = v with { Name = RenameToken(v.Name, renamed) }; + if (TryGetFunctionDCField(v.Name.Lexeme, out var dcField)) { if (v.Initializer != null) @@ -76,6 +92,9 @@ protected override void EmitVarDeclaration(Stmt.Var v) protected override void EmitAssign(Expr.Assign a) { + if (BlockScopeRenames.TryGetValue(a, out var renamed)) + a = a with { Name = RenameToken(a.Name, renamed) }; + if (TryGetFunctionDCField(a.Name.Lexeme, out var dcField)) { EmitExpression(a.Value); @@ -104,4 +123,45 @@ protected override void EmitStoreVariable(string name) base.EmitStoreVariable(name); } + + // Const declarations, compound/logical assignment, and increment/decrement reach the variable + // through the operator node's name token (or the increment operand). Rewriting that token to the + // shadowing binding's storage name before delegating to the base routes both the read and the + // write to the right field/local (#766). A renamed binding is never a DC/captured name, so the + // base path (which re-enters EmitVariable / EmitStoreVariable) resolves it as a plain slot. + + protected override void EmitConstDeclaration(Stmt.Const c) + { + if (BlockScopeRenames.TryGetValue(c, out var renamed)) + c = c with { Name = RenameToken(c.Name, renamed) }; + base.EmitConstDeclaration(c); + } + + protected override void EmitCompoundAssign(Expr.CompoundAssign ca) + { + if (BlockScopeRenames.TryGetValue(ca, out var renamed)) + ca = ca with { Name = RenameToken(ca.Name, renamed) }; + base.EmitCompoundAssign(ca); + } + + protected override void EmitLogicalAssign(Expr.LogicalAssign la) + { + if (BlockScopeRenames.TryGetValue(la, out var renamed)) + la = la with { Name = RenameToken(la.Name, renamed) }; + base.EmitLogicalAssign(la); + } + + protected override void EmitPrefixIncrement(Expr.PrefixIncrement pi) + { + if (pi.Operand is Expr.Variable v && BlockScopeRenames.TryGetValue(v, out var renamed)) + pi = pi with { Operand = v with { Name = RenameToken(v.Name, renamed) } }; + base.EmitPrefixIncrement(pi); + } + + protected override void EmitPostfixIncrement(Expr.PostfixIncrement poi) + { + if (poi.Operand is Expr.Variable v && BlockScopeRenames.TryGetValue(v, out var renamed)) + poi = poi with { Operand = v with { Name = RenameToken(v.Name, renamed) } }; + base.EmitPostfixIncrement(poi); + } } diff --git a/Compilation/AsyncGeneratorStateAnalyzer.Expressions.cs b/Compilation/AsyncGeneratorStateAnalyzer.Expressions.cs index f538c1bb..01286ff6 100644 --- a/Compilation/AsyncGeneratorStateAnalyzer.Expressions.cs +++ b/Compilation/AsyncGeneratorStateAnalyzer.Expressions.cs @@ -88,7 +88,8 @@ protected override void VisitAwait(Expr.Await expr) protected override void VisitVariable(Expr.Variable expr) { - var name = expr.Name.Lexeme; + // Resolve to the binding's (possibly disambiguated) storage name (#766). + var name = StorageName(expr, expr.Name.Lexeme); // Track variables used in for...of loop bodies (for hoisting when loop contains suspension) foreach (var loop in _forOfStack) @@ -104,22 +105,25 @@ protected override void VisitVariable(Expr.Variable expr) protected override void VisitAssign(Expr.Assign expr) { - if (_seenSuspension && _declaredVariables.Contains(expr.Name.Lexeme)) - _variablesUsedAfterSuspension.Add(expr.Name.Lexeme); + var name = StorageName(expr, expr.Name.Lexeme); + if (_seenSuspension && _declaredVariables.Contains(name)) + _variablesUsedAfterSuspension.Add(name); base.VisitAssign(expr); } protected override void VisitCompoundAssign(Expr.CompoundAssign expr) { - if (_seenSuspension && _declaredVariables.Contains(expr.Name.Lexeme)) - _variablesUsedAfterSuspension.Add(expr.Name.Lexeme); + var name = StorageName(expr, expr.Name.Lexeme); + if (_seenSuspension && _declaredVariables.Contains(name)) + _variablesUsedAfterSuspension.Add(name); base.VisitCompoundAssign(expr); } protected override void VisitLogicalAssign(Expr.LogicalAssign expr) { - if (_seenSuspension && _declaredVariables.Contains(expr.Name.Lexeme)) - _variablesUsedAfterSuspension.Add(expr.Name.Lexeme); + var name = StorageName(expr, expr.Name.Lexeme); + if (_seenSuspension && _declaredVariables.Contains(name)) + _variablesUsedAfterSuspension.Add(name); base.VisitLogicalAssign(expr); } diff --git a/Compilation/AsyncGeneratorStateAnalyzer.Statements.cs b/Compilation/AsyncGeneratorStateAnalyzer.Statements.cs index fe5a1587..eec53008 100644 --- a/Compilation/AsyncGeneratorStateAnalyzer.Statements.cs +++ b/Compilation/AsyncGeneratorStateAnalyzer.Statements.cs @@ -8,17 +8,21 @@ public partial class AsyncGeneratorStateAnalyzer protected override void VisitVar(Stmt.Var stmt) { - _declaredVariables.Add(stmt.Name.Lexeme); + // Track declaration under its (possibly disambiguated) storage name (#766). + var name = StorageName(stmt, stmt.Name.Lexeme); + _declaredVariables.Add(name); if (!_seenSuspension) - _variablesDeclaredBeforeSuspension.Add(stmt.Name.Lexeme); + _variablesDeclaredBeforeSuspension.Add(name); base.VisitVar(stmt); } protected override void VisitConst(Stmt.Const stmt) { - _declaredVariables.Add(stmt.Name.Lexeme); + // Track declaration under its (possibly disambiguated) storage name (#766). + var name = StorageName(stmt, stmt.Name.Lexeme); + _declaredVariables.Add(name); if (!_seenSuspension) - _variablesDeclaredBeforeSuspension.Add(stmt.Name.Lexeme); + _variablesDeclaredBeforeSuspension.Add(name); base.VisitConst(stmt); } diff --git a/Compilation/AsyncGeneratorStateAnalyzer.cs b/Compilation/AsyncGeneratorStateAnalyzer.cs index db95f228..d1f41e1d 100644 --- a/Compilation/AsyncGeneratorStateAnalyzer.cs +++ b/Compilation/AsyncGeneratorStateAnalyzer.cs @@ -53,7 +53,10 @@ public record AsyncGeneratorFunctionAnalysis( bool HasYieldStar, bool HasTryCatch, List TryBlocks, - List ForOfLoopsWithSuspension // for...of loops containing yields/awaits that need enumerator hoisting + List ForOfLoopsWithSuspension, // for...of loops containing yields/awaits that need enumerator hoisting + // Per-binding storage names for block-scoped let/const declarations that shadow an enclosing + // binding, keyed by declaration/reference AST node (see GeneratorBlockScopeRenamer, #766/#711). + IReadOnlyDictionary? BlockScopeRenames = null ) { /// @@ -104,6 +107,17 @@ private enum TryRegion { None, Try, Catch, Finally } // Reusable visitor for analyzing captures private readonly CaptureAnalysisVisitor _captureVisitor = new(); + // Block-scope shadow renames for this function (#766). Maps a declaration/reference AST node to the + // disambiguated storage name its binding uses; nodes absent from the map keep their source lexeme. + private IReadOnlyDictionary _renames = new Dictionary(); + + /// + /// Translates a declaration/reference node's source lexeme to its disambiguated storage name (#766), + /// or returns the lexeme unchanged when the binding is not a renamed shadow. + /// + private string StorageName(object node, string lexeme) => + _renames.TryGetValue(node, out var renamed) ? renamed : lexeme; + /// /// Analyzes an async generator function to determine suspension points and hoisted variables. /// @@ -111,6 +125,10 @@ public AsyncGeneratorFunctionAnalysis Analyze(Stmt.Function func) { Reset(); + // Disambiguate block-scoped let/const declarations that shadow an enclosing binding so the + // hoisting decision below is made per-binding rather than per-name (#766, async analog of #711). + _renames = GeneratorBlockScopeRenamer.Compute(func); + // Collect parameters as variables that need hoisting HashSet parameters = []; foreach (var param in func.Parameters) @@ -147,7 +165,8 @@ public AsyncGeneratorFunctionAnalysis Analyze(Stmt.Function func) HasYieldStar: _hasYieldStar, HasTryCatch: _hasTryCatch, TryBlocks: tryBlocks, - ForOfLoopsWithSuspension: [.. _forOfLoopsWithSuspension] + ForOfLoopsWithSuspension: [.. _forOfLoopsWithSuspension], + BlockScopeRenames: _renames ); } diff --git a/Compilation/AsyncMoveNextEmitter.Statements.Variables.cs b/Compilation/AsyncMoveNextEmitter.Statements.Variables.cs index 47658fb5..c79f4fda 100644 --- a/Compilation/AsyncMoveNextEmitter.Statements.Variables.cs +++ b/Compilation/AsyncMoveNextEmitter.Statements.Variables.cs @@ -7,6 +7,14 @@ public partial class AsyncMoveNextEmitter { protected override FieldBuilder? GetHoistedVariableField(string name) => _builder.GetVariableField(name); + // Per-binding storage names for block-scoped let/const shadows (#766), shared with the analyzer via + // the analysis. Empty for the common no-shadow case (and for analyses built without the renamer). + private static readonly IReadOnlyDictionary NoRenames = new Dictionary(); + private IReadOnlyDictionary BlockScopeRenames => _analysis.BlockScopeRenames ?? NoRenames; + + private static Token RenameToken(Token original, string lexeme) => + new(original.Type, lexeme, original.Literal, original.Line, original.Start); + /// /// Checks whether a variable is a captured function local with a function DC field available. /// @@ -40,6 +48,11 @@ private void StoreToDCField(string name, FieldBuilder dcField) /// protected override void EmitVarDeclaration(Stmt.Var v) { + // Resolve a shadowing block-scoped binding to its own storage before any DC routing (#766); + // a renamed binding is never a captured/DC name, so the DC check below correctly falls through. + if (BlockScopeRenames.TryGetValue(v, out var renamed)) + v = v with { Name = RenameToken(v.Name, renamed) }; + string name = v.Name.Lexeme; if (TryGetFunctionDCField(name, out var dcField)) @@ -83,6 +96,9 @@ protected override void EmitVarDeclaration(Stmt.Var v) /// protected override void EmitVariable(Expr.Variable v) { + if (BlockScopeRenames.TryGetValue(v, out var renamed)) + v = v with { Name = RenameToken(v.Name, renamed) }; + string name = v.Name.Lexeme; if (TryGetFunctionDCField(name, out var dcField)) @@ -102,6 +118,9 @@ protected override void EmitVariable(Expr.Variable v) /// protected override void EmitAssign(Expr.Assign a) { + if (BlockScopeRenames.TryGetValue(a, out var renamed)) + a = a with { Name = RenameToken(a.Name, renamed) }; + string name = a.Name.Lexeme; if (TryGetFunctionDCField(name, out var dcField)) @@ -165,4 +184,45 @@ protected override void EmitStoreVariable(string name) base.EmitStoreVariable(name); } + + // Const declarations, compound/logical assignment, and increment/decrement reach the variable + // through the operator node's name token (or the increment operand). Rewriting that token to the + // shadowing binding's storage name before delegating to the base routes both the read and the + // write to the right field/local (#766). A renamed binding is never a DC/captured name, so the + // base path (which re-enters EmitVariable / EmitStoreVariable) resolves it as a plain slot. + + protected override void EmitConstDeclaration(Stmt.Const c) + { + if (BlockScopeRenames.TryGetValue(c, out var renamed)) + c = c with { Name = RenameToken(c.Name, renamed) }; + base.EmitConstDeclaration(c); + } + + protected override void EmitCompoundAssign(Expr.CompoundAssign ca) + { + if (BlockScopeRenames.TryGetValue(ca, out var renamed)) + ca = ca with { Name = RenameToken(ca.Name, renamed) }; + base.EmitCompoundAssign(ca); + } + + protected override void EmitLogicalAssign(Expr.LogicalAssign la) + { + if (BlockScopeRenames.TryGetValue(la, out var renamed)) + la = la with { Name = RenameToken(la.Name, renamed) }; + base.EmitLogicalAssign(la); + } + + protected override void EmitPrefixIncrement(Expr.PrefixIncrement pi) + { + if (pi.Operand is Expr.Variable v && BlockScopeRenames.TryGetValue(v, out var renamed)) + pi = pi with { Operand = v with { Name = RenameToken(v.Name, renamed) } }; + base.EmitPrefixIncrement(pi); + } + + protected override void EmitPostfixIncrement(Expr.PostfixIncrement poi) + { + if (poi.Operand is Expr.Variable v && BlockScopeRenames.TryGetValue(v, out var renamed)) + poi = poi with { Operand = v with { Name = RenameToken(v.Name, renamed) } }; + base.EmitPostfixIncrement(poi); + } } diff --git a/Compilation/AsyncStateAnalyzer.Expressions.cs b/Compilation/AsyncStateAnalyzer.Expressions.cs index 66e2f09f..db3f4454 100644 --- a/Compilation/AsyncStateAnalyzer.Expressions.cs +++ b/Compilation/AsyncStateAnalyzer.Expressions.cs @@ -53,31 +53,35 @@ internal void RecordAwaitPoint(Expr.Await? expr) protected override void VisitVariable(Expr.Variable expr) { - // Track variable usage after await - if (_seenAwait && _declaredVariables.Contains(expr.Name.Lexeme)) - _variablesUsedAfterAwait.Add(expr.Name.Lexeme); + // Track variable usage after await, under its (possibly disambiguated) storage name (#766). + var name = StorageName(expr, expr.Name.Lexeme); + if (_seenAwait && _declaredVariables.Contains(name)) + _variablesUsedAfterAwait.Add(name); // No base call needed - leaf node } protected override void VisitAssign(Expr.Assign expr) { - // Track assignment to variable after await - if (_seenAwait && _declaredVariables.Contains(expr.Name.Lexeme)) - _variablesUsedAfterAwait.Add(expr.Name.Lexeme); + // Track assignment to variable after await, under its storage name (#766). + var name = StorageName(expr, expr.Name.Lexeme); + if (_seenAwait && _declaredVariables.Contains(name)) + _variablesUsedAfterAwait.Add(name); base.VisitAssign(expr); } protected override void VisitCompoundAssign(Expr.CompoundAssign expr) { - if (_seenAwait && _declaredVariables.Contains(expr.Name.Lexeme)) - _variablesUsedAfterAwait.Add(expr.Name.Lexeme); + var name = StorageName(expr, expr.Name.Lexeme); + if (_seenAwait && _declaredVariables.Contains(name)) + _variablesUsedAfterAwait.Add(name); base.VisitCompoundAssign(expr); } protected override void VisitLogicalAssign(Expr.LogicalAssign expr) { - if (_seenAwait && _declaredVariables.Contains(expr.Name.Lexeme)) - _variablesUsedAfterAwait.Add(expr.Name.Lexeme); + var name = StorageName(expr, expr.Name.Lexeme); + if (_seenAwait && _declaredVariables.Contains(name)) + _variablesUsedAfterAwait.Add(name); base.VisitLogicalAssign(expr); } diff --git a/Compilation/AsyncStateAnalyzer.Statements.cs b/Compilation/AsyncStateAnalyzer.Statements.cs index d8e4b743..6f1a2590 100644 --- a/Compilation/AsyncStateAnalyzer.Statements.cs +++ b/Compilation/AsyncStateAnalyzer.Statements.cs @@ -8,20 +8,22 @@ public partial class AsyncStateAnalyzer protected override void VisitVar(Stmt.Var stmt) { - // Track variable declaration - _declaredVariables.Add(stmt.Name.Lexeme); + // Track variable declaration under its (possibly disambiguated) storage name (#766). + var name = StorageName(stmt, stmt.Name.Lexeme); + _declaredVariables.Add(name); if (!_seenAwait) - _variablesDeclaredBeforeAwait.Add(stmt.Name.Lexeme); + _variablesDeclaredBeforeAwait.Add(name); base.VisitVar(stmt); } protected override void VisitConst(Stmt.Const stmt) { - // Track const variable declaration - _declaredVariables.Add(stmt.Name.Lexeme); + // Track const variable declaration under its (possibly disambiguated) storage name (#766). + var name = StorageName(stmt, stmt.Name.Lexeme); + _declaredVariables.Add(name); if (!_seenAwait) - _variablesDeclaredBeforeAwait.Add(stmt.Name.Lexeme); + _variablesDeclaredBeforeAwait.Add(name); base.VisitConst(stmt); } diff --git a/Compilation/AsyncStateAnalyzer.cs b/Compilation/AsyncStateAnalyzer.cs index 1e4b7528..730bc38b 100644 --- a/Compilation/AsyncStateAnalyzer.cs +++ b/Compilation/AsyncStateAnalyzer.cs @@ -55,7 +55,11 @@ public record AsyncFunctionAnalysis( bool HasTryCatch, bool UsesThis, List AsyncArrows, - List TryBlocks = null! // Try blocks with await tracking + List TryBlocks = null!, // Try blocks with await tracking + // Per-binding storage names for block-scoped let/const declarations that shadow an enclosing + // binding, keyed by declaration/reference AST node (see GeneratorBlockScopeRenamer, #766/#711). + // Null for analyses built without the renamer (e.g. async arrows); treated as empty. + IReadOnlyDictionary? BlockScopeRenames = null ) { /// @@ -97,6 +101,17 @@ private enum TryRegion { None, Try, Catch, Finally } // Reusable visitor for analyzing arrow function captures private readonly CaptureAnalysisVisitor _captureVisitor = new(); + // Block-scope shadow renames for this function (#766). Maps a declaration/reference AST node to the + // disambiguated storage name its binding uses; nodes absent from the map keep their source lexeme. + private IReadOnlyDictionary _renames = new Dictionary(); + + /// + /// Translates a declaration/reference node's source lexeme to its disambiguated storage name (#766), + /// or returns the lexeme unchanged when the binding is not a renamed shadow. + /// + private string StorageName(object node, string lexeme) => + _renames.TryGetValue(node, out var renamed) ? renamed : lexeme; + /// /// Analyzes an async function to determine await points and hoisted variables. /// @@ -104,6 +119,10 @@ public AsyncFunctionAnalysis Analyze(Stmt.Function func) { Reset(); + // Disambiguate block-scoped let/const declarations that shadow an enclosing binding so the + // hoisting decision below is made per-binding rather than per-name (#766, async analog of #711). + _renames = GeneratorBlockScopeRenamer.Compute(func); + // Collect parameters as variables that need hoisting HashSet parameters = []; foreach (var param in func.Parameters) @@ -140,7 +159,8 @@ public AsyncFunctionAnalysis Analyze(Stmt.Function func) HasTryCatch: _hasTryCatch, UsesThis: _usesThis, AsyncArrows: [.. _asyncArrows], - TryBlocks: tryBlocks + TryBlocks: tryBlocks, + BlockScopeRenames: _renames ); } diff --git a/Compilation/GeneratorBlockScopeRenamer.cs b/Compilation/GeneratorBlockScopeRenamer.cs index 99ec94a0..4e6ae32a 100644 --- a/Compilation/GeneratorBlockScopeRenamer.cs +++ b/Compilation/GeneratorBlockScopeRenamer.cs @@ -5,9 +5,13 @@ namespace SharpTS.Compilation; /// /// Computes per-binding storage names for block-scoped (let/const) declarations inside a -/// generator body that shadow an enclosing binding of the same name. +/// suspension state-machine body that shadow an enclosing binding of the same name. /// /// +/// Although named for generators (where it originated, #711), this pass is mode-agnostic: every +/// suspension state machine that hoists live-across-suspension locals to name-keyed fields shares the +/// same shadow-collision root cause, so it is reused by the async-function and async-generator state +/// machines too ( / , #766). /// The generator state machine hoists every local that lives across a yield into a field keyed /// by its source name (see ), and resolves non-hoisted locals through a /// flat, name-keyed local map. Both flatten lexical scope, so two block-scoped bindings that share a @@ -45,23 +49,35 @@ internal sealed class GeneratorBlockScopeRenamer : AstVisitorBase /// /// Returns the node→storage-name map for , empty when nothing shadows. /// - public static IReadOnlyDictionary Compute(Stmt.Function func) + public static IReadOnlyDictionary Compute(Stmt.Function func) => + Compute(func.Name.Lexeme, func.Parameters, func.Body); + + /// + /// Arrow-function overload (#766). Arrows have no self-name, and only a block body can hold + /// block-scoped declarations (an expression-bodied arrow has none, so it has no shadows to rename). + /// + public static IReadOnlyDictionary Compute(Expr.ArrowFunction arrow) => + Compute(selfName: null, arrow.Parameters, arrow.BlockBody); + + private static IReadOnlyDictionary Compute( + string? selfName, List parameters, List? body) { var renamer = new GeneratorBlockScopeRenamer(); - new ClosureReferenceCollector(renamer._closureReferenced).Run(func); + if (body == null) return renamer._renames; // expression-bodied arrow: nothing to rename + + new ClosureReferenceCollector(renamer._closureReferenced).Run(body); renamer.PushScope(); - // The generator's own name is in scope inside the body (a named function expression can call + // The callable's own name is in scope inside the body (a named function expression can call // itself by it); a nested-block let/const may shadow it, so seed it for shadow detection. // Never renamed (it is not a hoisted local — it resolves to the function/method itself). - if (!string.IsNullOrEmpty(func.Name.Lexeme)) - renamer.CurrentScope[func.Name.Lexeme] = func.Name.Lexeme; + if (!string.IsNullOrEmpty(selfName)) + renamer.CurrentScope[selfName] = selfName; // Parameters live in the function scope; an inner block let/const may shadow them. Never renamed. - foreach (var p in func.Parameters) + foreach (var p in parameters) renamer.CurrentScope[p.Name.Lexeme] = p.Name.Lexeme; - if (func.Body != null) - foreach (var stmt in func.Body) - renamer.Visit(stmt); + foreach (var stmt in body) + renamer.Visit(stmt); renamer.PopScope(); return renamer._renames; @@ -256,10 +272,9 @@ private sealed class ClosureReferenceCollector : AstVisitorBase public ClosureReferenceCollector(HashSet names) => _names = names; - public void Run(Stmt.Function func) + public void Run(List body) { - if (func.Body != null) - foreach (var s in func.Body) Visit(s); + foreach (var s in body) Visit(s); } protected override void VisitArrowFunction(Expr.ArrowFunction expr) { _depth++; base.VisitArrowFunction(expr); _depth--; } diff --git a/Compilation/ILCompiler.Async.cs b/Compilation/ILCompiler.Async.cs index 594dfd71..898f9e3f 100644 --- a/Compilation/ILCompiler.Async.cs +++ b/Compilation/ILCompiler.Async.cs @@ -276,11 +276,25 @@ private void DefineAsyncArrowStateMachines( /// Analyzes an async arrow function to determine its await points and hoisted variables. /// Uses pooled HashSets for intermediate analysis to reduce allocations. /// - private (int AwaitCount, HashSet HoistedLocals) AnalyzeAsyncArrow(Expr.ArrowFunction arrow) + // Block-scope shadow renames for the async arrow currently being analyzed (#766). Set at the top of + // AnalyzeAsyncArrow and consulted by the AnalyzeArrow*ForAwaits walkers, which are non-reentrant (a + // nested arrow is a separate AnalyzeAsyncArrow pass — the Expr.ArrowFunction walker case is a no-op, + // so this field is never clobbered mid-walk). Mirrors the renamer wiring in the visitor analyzers. + private IReadOnlyDictionary _arrowBlockScopeRenames = new Dictionary(); + + /// Storage name for an async-arrow declaration/reference node (#766), or the lexeme unchanged. + private string ArrowStorageName(object node, string lexeme) => + _arrowBlockScopeRenames.TryGetValue(node, out var renamed) ? renamed : lexeme; + + private (int AwaitCount, HashSet HoistedLocals, IReadOnlyDictionary Renames) AnalyzeAsyncArrow(Expr.ArrowFunction arrow) { var awaitCount = 0; var seenAwait = false; + // Disambiguate nested-block let/const shadows so the hoisting decision below is per-binding + // rather than per-name (#766, async analog of #711). + _arrowBlockScopeRenames = GeneratorBlockScopeRenamer.Compute(arrow); + // Clear and reuse pooled HashSets _async.DeclaredVars.Clear(); _async.UsedAfterAwait.Clear(); @@ -313,11 +327,12 @@ private void DefineAsyncArrowStateMachines( var hoistedLocals = new HashSet(_async.DeclaredBeforeAwait); hoistedLocals.IntersectWith(_async.UsedAfterAwait); - // Remove parameters from hoisted locals (they're stored separately) + // Remove parameters from hoisted locals (they're stored separately). Parameters are never + // renamed, so removing by source lexeme is correct. foreach (var param in arrow.Parameters) hoistedLocals.Remove(param.Name.Lexeme); - return (awaitCount, hoistedLocals); + return (awaitCount, hoistedLocals, _arrowBlockScopeRenames); } private void AnalyzeArrowStmtForAwaits(Stmt stmt, ref int awaitCount, ref bool seenAwait, @@ -326,18 +341,24 @@ private void AnalyzeArrowStmtForAwaits(Stmt stmt, ref int awaitCount, ref bool s switch (stmt) { case Stmt.Var v: - declaredVariables.Add(v.Name.Lexeme); + { + var name = ArrowStorageName(v, v.Name.Lexeme); // #766: per-binding storage name + declaredVariables.Add(name); if (!seenAwait) - declaredBeforeAwait.Add(v.Name.Lexeme); + declaredBeforeAwait.Add(name); if (v.Initializer != null) AnalyzeArrowExprForAwaits(v.Initializer, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); break; + } case Stmt.Const c: - declaredVariables.Add(c.Name.Lexeme); + { + var name = ArrowStorageName(c, c.Name.Lexeme); // #766: per-binding storage name + declaredVariables.Add(name); if (!seenAwait) - declaredBeforeAwait.Add(c.Name.Lexeme); + declaredBeforeAwait.Add(name); AnalyzeArrowExprForAwaits(c.Initializer, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); break; + } case Stmt.Expression e: AnalyzeArrowExprForAwaits(e.Expr, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); break; @@ -429,14 +450,20 @@ private void AnalyzeArrowExprForAwaits(Expr expr, ref int awaitCount, ref bool s AnalyzeArrowExprForAwaits(a.Expression, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); break; case Expr.Variable v: - if (seenAwait && declaredVariables.Contains(v.Name.Lexeme)) - usedAfterAwait.Add(v.Name.Lexeme); + { + var name = ArrowStorageName(v, v.Name.Lexeme); // #766 + if (seenAwait && declaredVariables.Contains(name)) + usedAfterAwait.Add(name); break; + } case Expr.Assign a: - if (seenAwait && declaredVariables.Contains(a.Name.Lexeme)) - usedAfterAwait.Add(a.Name.Lexeme); + { + var name = ArrowStorageName(a, a.Name.Lexeme); // #766 + if (seenAwait && declaredVariables.Contains(name)) + usedAfterAwait.Add(name); AnalyzeArrowExprForAwaits(a.Value, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); break; + } case Expr.Binary b: AnalyzeArrowExprForAwaits(b.Left, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); AnalyzeArrowExprForAwaits(b.Right, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); @@ -506,10 +533,13 @@ private void AnalyzeArrowExprForAwaits(Expr expr, ref int awaitCount, ref bool s AnalyzeArrowExprForAwaits(e, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); break; case Expr.CompoundAssign ca: - if (seenAwait && declaredVariables.Contains(ca.Name.Lexeme)) - usedAfterAwait.Add(ca.Name.Lexeme); + { + var name = ArrowStorageName(ca, ca.Name.Lexeme); // #766 + if (seenAwait && declaredVariables.Contains(name)) + usedAfterAwait.Add(name); AnalyzeArrowExprForAwaits(ca.Value, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); break; + } case Expr.CompoundSet cs: AnalyzeArrowExprForAwaits(cs.Object, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); AnalyzeArrowExprForAwaits(cs.Value, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); @@ -677,7 +707,10 @@ private void EmitAsyncArrowMoveNext(AsyncArrowStateMachineBuilder arrowBuilder, new HashSet(arrow.Parameters.Select(p => p.Name.Lexeme)), false, // HasTryCatch - will be detected during emission arrowBuilder.Captures.Contains("this"), - [] // No nested async arrows handled yet + [], // No nested async arrows handled yet + // #766: carry the block-scope shadow renames so the arrow emitter routes a shadowing + // nested-block let/const to its own storage instead of the outer binding's hoisted field. + BlockScopeRenames: arrowAnalysis.Renames ); // Create a new context for arrow MoveNext emission @@ -1174,7 +1207,9 @@ private void EmitTopLevelAsyncArrowBodies() new HashSet(arrow.Parameters.Select(p => p.Name.Lexeme)), false, // HasTryCatch - detected during emission false, // UsesThis - standalone arrows don't have 'this' binding by default - [] // No nested async arrows handled in this pass + [], // No nested async arrows handled in this pass + // #766: carry the block-scope shadow renames into the standalone arrow emitter too. + BlockScopeRenames: arrowAnalysis.Renames ); // Determine if this arrow is inside a class (for private field access) diff --git a/SharpTS.Tests/Compilation/EmitterSyncTests.cs b/SharpTS.Tests/Compilation/EmitterSyncTests.cs index ee6ed891..5b2f21ec 100644 --- a/SharpTS.Tests/Compilation/EmitterSyncTests.cs +++ b/SharpTS.Tests/Compilation/EmitterSyncTests.cs @@ -58,10 +58,18 @@ public class EmitterSyncTests "EmitTemplateLiteral", // Template literals with await-safe temp storage "EmitTaggedTemplateLiteral", // Tagged template literals in async context // --- Closure mutation sharing --- - "EmitVariable", // Route captured function locals through function DC - "EmitAssign", // Route captured function locals through function DC + "EmitVariable", // Route captured function locals through function DC (+ #766 rename) + "EmitAssign", // Route captured function locals through function DC (+ #766 rename) "EmitStoreVariable", // Route captured function locals through function DC - "EmitVarDeclaration", // Route captured function locals through function DC + "EmitVarDeclaration", // Route captured function locals through function DC (+ #766 rename) + // --- #766: a nested-block let/const that shadows an enclosing binding gets its own storage; + // these overrides retoken the operator node so the read/write land on the shadow's own + // field/local instead of the outer binding's hoisted field (async analog of #711) --- + "EmitConstDeclaration", // #766: route a shadowing const declaration to its own slot + "EmitCompoundAssign", // #766: route a shadowing compound assignment to its own slot + "EmitLogicalAssign", // #766: route a shadowing logical assignment to its own slot + "EmitPrefixIncrement", // #766: route a shadowing prefix ++/-- to its own slot + "EmitPostfixIncrement", // #766: route a shadowing postfix ++/-- to its own slot }, [typeof(AsyncArrowMoveNextEmitter)] = new() { @@ -92,6 +100,15 @@ public class EmitterSyncTests "EmitAwait", // Core: suspend/resume state machine "EmitArrowFunction", // Nested async arrows in state machine "EmitExpressionAsDouble", // Literal optimization + // --- #766: a nested-block let/const that shadows an enclosing binding gets its own storage; + // these overrides retoken the operator node so the read/write land on the shadow's own + // field/local instead of the outer binding's hoisted field (async analog of #711). + // (EmitVariable/EmitAssign/EmitVarDeclaration above are also rename-aware now.) --- + "EmitConstDeclaration", // #766: route a shadowing const declaration to its own slot + "EmitCompoundAssign", // #766: route a shadowing compound assignment to its own slot + "EmitLogicalAssign", // #766: route a shadowing logical assignment to its own slot + "EmitPrefixIncrement", // #766: route a shadowing prefix ++/-- to its own slot + "EmitPostfixIncrement", // #766: route a shadowing postfix ++/-- to its own slot }, [typeof(GeneratorMoveNextEmitter)] = new() { @@ -160,10 +177,18 @@ public class EmitterSyncTests "EmitSuper", // This field indirection // --- #725: route a captured-and-mutated local through the function display class --- "GetFunctionDCField", // Exposes <>__functionDC so a capturing arrow threads it in - "EmitVariable", // Read a captured-and-mutated local through the function DC - "EmitAssign", // Write a captured-and-mutated local through the function DC + "EmitVariable", // Read a captured-and-mutated local through the function DC (+ #766 rename) + "EmitAssign", // Write a captured-and-mutated local through the function DC (+ #766 rename) "EmitStoreVariable", // Store side of compound/logical/increment through the function DC - "EmitVarDeclaration", // Initialize a captured-and-mutated local into the function DC + "EmitVarDeclaration", // Initialize a captured-and-mutated local into the function DC (+ #766 rename) + // --- #766: a nested-block let/const that shadows an enclosing binding gets its own storage; + // these overrides retoken the operator node so the read/write land on the shadow's own + // field/local instead of the outer binding's hoisted field (async analog of #711) --- + "EmitConstDeclaration", // #766: route a shadowing const declaration to its own slot + "EmitCompoundAssign", // #766: route a shadowing compound assignment to its own slot + "EmitLogicalAssign", // #766: route a shadowing logical assignment to its own slot + "EmitPrefixIncrement", // #766: route a shadowing prefix ++/-- to its own slot + "EmitPostfixIncrement", // #766: route a shadowing postfix ++/-- to its own slot // --- #559: non-local exits must run an enclosing flag-based finally first (async #500) --- "EmitBreak", // Route a break leaving a try through its finally(s) "EmitContinue", // Route a continue leaving a try through its finally(s) diff --git a/SharpTS.Tests/SharedTests/AsyncBlockScopeShadowTests.cs b/SharpTS.Tests/SharedTests/AsyncBlockScopeShadowTests.cs new file mode 100644 index 00000000..c36af088 --- /dev/null +++ b/SharpTS.Tests/SharedTests/AsyncBlockScopeShadowTests.cs @@ -0,0 +1,227 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +/// +/// Block-scope shadowing inside async functions and async generators (#766), and the +/// interpreter async-generator nested-block binding-before-await bug (#768). Mirrors the +/// compiled-generator coverage in (#711), extended to the +/// suspension-by-await/yield state machines. Runs against both interpreter and compiler. +/// +public class AsyncBlockScopeShadowTests +{ + #region Async function (#766) + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncFunction_NestedBlockConstShadow_DoesNotLeakToOuter(ExecutionMode mode) + { + // A const in a nested block that shadows an outer body-level const must get its own slot + // instead of clobbering the outer binding's hoisted state-machine field (#766, async #711). + var source = """ + async function af(): Promise { + const r = 100; + { const r = 0; await Promise.resolve(r); } + return r; + } + async function main() { console.log(await af()); } + main(); + """; + + Assert.Equal("100\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncFunction_NestedBlockShadow_LiveAcrossAwait_GetsOwnField(ExecutionMode mode) + { + // The inner shadow is itself read after an await, so it must hoist to its OWN field, distinct + // from the outer binding's field — both must survive the suspension independently (#766). + var source = """ + async function af(): Promise { + const r = 100; + let inner = 0; + { + const r = 5; + await Promise.resolve(0); + inner = r; + } + return inner + "/" + r; + } + async function main() { console.log(await af()); } + main(); + """; + + Assert.Equal("5/100\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncFunction_NestedBlockLetShadow_ReassignedAcrossAwait(ExecutionMode mode) + { + // A let shadow compound-assigned across awaits keeps its own value, separate from the outer + // binding (#766). + var source = """ + async function af(): Promise { + let r = 7; + let captured = 0; + { + let r = 1; + r += 1; + await Promise.resolve(0); + r += 10; + captured = r; + } + return captured + "/" + r; + } + async function main() { console.log(await af()); } + main(); + """; + + Assert.Equal("12/7\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncFunction_NestedBlockShadowsParameter(ExecutionMode mode) + { + // An inner block const may shadow a (hoisted) parameter without clobbering it (#766). + var source = """ + async function af(r: number): Promise { + let captured = 0; + { const r = 99; await Promise.resolve(0); captured = r; } + return captured + "/" + r; + } + async function main() { console.log(await af(5)); } + main(); + """; + + Assert.Equal("99/5\n", TestHarness.Run(source, mode)); + } + + #endregion + + #region Async arrow (#766) + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncArrow_NestedBlockConstShadow_DoesNotLeakToOuter(ExecutionMode mode) + { + // Same shadow leak in a compiled async arrow's state machine (#766). + var source = """ + const af = async (): Promise => { + const r = 100; + { const r = 0; await Promise.resolve(r); } + return r; + }; + async function main() { console.log(await af()); } + main(); + """; + + Assert.Equal("100\n", TestHarness.Run(source, mode)); + } + + #endregion + + #region Async generator (#766 + #768) + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncGenerator_NestedBlockConstShadow_DoesNotLeakToOuter(ExecutionMode mode) + { + // Compiled: the nested-block shadow leaked onto the outer binding's hoisted field → [0, 0] (#766). + // Interpreter: the inner block const yielded before any await read as undefined → [undefined, 100] (#768). + var source = """ + async function* ag(): AsyncGenerator { + const r = 100; + { const r = 0; yield r; } + yield r; + } + async function main() { + const g = ag(); + let a = await g.next(); + let b = await g.next(); + console.log(a.value + "," + b.value); + } + main(); + """; + + Assert.Equal("0,100\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncGenerator_NestedBlockShadow_LiveAcrossYield_GetsOwnField(ExecutionMode mode) + { + // Both the inner shadow and the outer binding survive the suspension on their own slots (#766). + var source = """ + async function* ag(): AsyncGenerator { + const r = 100; + { + const r = 0; + yield r; + yield r + 1; + } + yield r; + } + async function main() { + const g = ag(); + let out = ""; + for (let i = 0; i < 3; i++) out += (await g.next()).value + (i < 2 ? "," : ""); + console.log(out); + } + main(); + """; + + Assert.Equal("0,1,100\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncGenerator_NestedBlockBindingBeforeAwait_YieldsValue(ExecutionMode mode) + { + // #768: distinct names (no shadowing). The interpreter lost a nested-block binding's value when + // it was yielded before the generator first suspended on an await → [undefined, 100]. + var source = """ + async function* ag(): AsyncGenerator { + const a = 100; + { const b = 0; yield b; } + yield a; + } + async function main() { + const g = ag(); + let x = await g.next(); + let y = await g.next(); + console.log(x.value + "," + y.value); + } + main(); + """; + + Assert.Equal("0,100\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void AsyncGenerator_NestedBlockLetBeforeAwait_Reassigned(ExecutionMode mode) + { + // #768 variant: a nested-block let read/updated before any await must keep its value. + var source = """ + async function* ag(): AsyncGenerator { + { let b = 1; b += 4; yield b; } + yield 9; + } + async function main() { + const g = ag(); + let x = await g.next(); + let y = await g.next(); + console.log(x.value + "," + y.value); + } + main(); + """; + + Assert.Equal("5,9\n", TestHarness.Run(source, mode)); + } + + #endregion +}