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
57 changes: 57 additions & 0 deletions Compilation/AsyncArrowMoveNextEmitter.Variables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object, string> NoRenames = new Dictionary<object, string>();
private IReadOnlyDictionary<object, string> 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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
4 changes: 4 additions & 0 deletions Compilation/AsyncArrowMoveNextEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
60 changes: 60 additions & 0 deletions Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object, string> NoRenames = new Dictionary<object, string>();
private IReadOnlyDictionary<object, string> BlockScopeRenames => _analysis.BlockScopeRenames ?? NoRenames;

private static Token RenameToken(Token original, string lexeme) =>
new(original.Type, lexeme, original.Literal, original.Line, original.Start);

/// <summary>
/// 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
Expand Down Expand Up @@ -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);
Expand All @@ -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)
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
}
18 changes: 11 additions & 7 deletions Compilation/AsyncGeneratorStateAnalyzer.Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
}

Expand Down
12 changes: 8 additions & 4 deletions Compilation/AsyncGeneratorStateAnalyzer.Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
23 changes: 21 additions & 2 deletions Compilation/AsyncGeneratorStateAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ public record AsyncGeneratorFunctionAnalysis(
bool HasYieldStar,
bool HasTryCatch,
List<TryBlockInfo> TryBlocks,
List<Stmt.ForOf> ForOfLoopsWithSuspension // for...of loops containing yields/awaits that need enumerator hoisting
List<Stmt.ForOf> 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<object, string>? BlockScopeRenames = null
)
{
/// <summary>
Expand Down Expand Up @@ -104,13 +107,28 @@ 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<object, string> _renames = new Dictionary<object, string>();

/// <summary>
/// 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.
/// </summary>
private string StorageName(object node, string lexeme) =>
_renames.TryGetValue(node, out var renamed) ? renamed : lexeme;

/// <summary>
/// Analyzes an async generator function to determine suspension points and hoisted variables.
/// </summary>
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<string> parameters = [];
foreach (var param in func.Parameters)
Expand Down Expand Up @@ -147,7 +165,8 @@ public AsyncGeneratorFunctionAnalysis Analyze(Stmt.Function func)
HasYieldStar: _hasYieldStar,
HasTryCatch: _hasTryCatch,
TryBlocks: tryBlocks,
ForOfLoopsWithSuspension: [.. _forOfLoopsWithSuspension]
ForOfLoopsWithSuspension: [.. _forOfLoopsWithSuspension],
BlockScopeRenames: _renames
);
}

Expand Down
60 changes: 60 additions & 0 deletions Compilation/AsyncMoveNextEmitter.Statements.Variables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object, string> NoRenames = new Dictionary<object, string>();
private IReadOnlyDictionary<object, string> BlockScopeRenames => _analysis.BlockScopeRenames ?? NoRenames;

private static Token RenameToken(Token original, string lexeme) =>
new(original.Type, lexeme, original.Literal, original.Line, original.Start);

/// <summary>
/// Checks whether a variable is a captured function local with a function DC field available.
/// </summary>
Expand Down Expand Up @@ -40,6 +48,11 @@ private void StoreToDCField(string name, FieldBuilder dcField)
/// </summary>
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))
Expand Down Expand Up @@ -83,6 +96,9 @@ protected override void EmitVarDeclaration(Stmt.Var v)
/// </summary>
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))
Expand All @@ -102,6 +118,9 @@ protected override void EmitVariable(Expr.Variable v)
/// </summary>
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))
Expand Down Expand Up @@ -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);
}
}
Loading
Loading