221 lines
6.9 KiB
ObjectPascal
221 lines
6.9 KiB
ObjectPascal
unit Myc.Ast.Compiler.Binder.Upvalues;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.SysUtils,
|
|
System.Classes,
|
|
System.Generics.Collections,
|
|
Myc.Ast.Nodes,
|
|
Myc.Ast.Visitor,
|
|
Myc.Ast.Scope,
|
|
Myc.Data.Value,
|
|
Myc.Ast;
|
|
|
|
type
|
|
// This visitor analyzes the AST to find all variables that need to be "lifted" or "boxed"
|
|
// because they are captured by a nested lambda.
|
|
TUpvalueAnalyzer = class(TAstTransformer)
|
|
private
|
|
type
|
|
// Internal helper to track scopes during analysis without using IScopeDescriptor
|
|
TAnalysisScope = class
|
|
private
|
|
FParent: TAnalysisScope;
|
|
// Maps Name -> DeclarationNode.
|
|
// If Value is nil, it means the name is defined (e.g. parameter) but not a candidate for boxing.
|
|
FDeclarations: TDictionary<string, IVariableDeclarationNode>;
|
|
public
|
|
constructor Create(AParent: TAnalysisScope);
|
|
destructor Destroy; override;
|
|
|
|
procedure Define(const Name: string; const Node: IVariableDeclarationNode);
|
|
|
|
// Returns the scope where the symbol is defined and the declaration node (if any)
|
|
function Resolve(const Name: string; out ScopeDepth: Integer; out Node: IVariableDeclarationNode): Boolean;
|
|
end;
|
|
|
|
private
|
|
FBoxedDeclarations: THashSet<IVariableDeclarationNode>;
|
|
FCurrentScope: TAnalysisScope;
|
|
|
|
procedure MarkDeclarationForBoxing(const AName: string);
|
|
protected
|
|
// Overridden Visit methods to perform analysis during traversal.
|
|
function VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode; override;
|
|
function VisitIdentifier(const Node: IIdentifierNode): IAstNode; override;
|
|
function VisitVariableDeclaration(const Node: IVariableDeclarationNode): IAstNode; override;
|
|
public
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
|
|
function Execute(const ARootNode: IAstNode): IAstNode;
|
|
|
|
class function Analyze(const ARootNode: IAstNode): THashSet<IVariableDeclarationNode>; static;
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
System.Generics.Defaults,
|
|
Myc.Ast.Types;
|
|
|
|
{ TUpvalueAnalyzer.TAnalysisScope }
|
|
|
|
constructor TUpvalueAnalyzer.TAnalysisScope.Create(AParent: TAnalysisScope);
|
|
begin
|
|
inherited Create;
|
|
FParent := AParent;
|
|
FDeclarations := TDictionary<string, IVariableDeclarationNode>.Create;
|
|
end;
|
|
|
|
destructor TUpvalueAnalyzer.TAnalysisScope.Destroy;
|
|
begin
|
|
FDeclarations.Free;
|
|
inherited;
|
|
end;
|
|
|
|
procedure TUpvalueAnalyzer.TAnalysisScope.Define(const Name: string; const Node: IVariableDeclarationNode);
|
|
begin
|
|
FDeclarations.AddOrSetValue(Name, Node);
|
|
end;
|
|
|
|
function TUpvalueAnalyzer.TAnalysisScope.Resolve(const Name: string; out ScopeDepth: Integer; out Node: IVariableDeclarationNode): Boolean;
|
|
var
|
|
current: TAnalysisScope;
|
|
begin
|
|
ScopeDepth := 0;
|
|
current := Self;
|
|
while Assigned(current) do
|
|
begin
|
|
if current.FDeclarations.TryGetValue(Name, Node) then
|
|
begin
|
|
Result := True;
|
|
exit;
|
|
end;
|
|
Inc(ScopeDepth);
|
|
current := current.FParent;
|
|
end;
|
|
|
|
Node := nil;
|
|
Result := False;
|
|
end;
|
|
|
|
{ TUpvalueAnalyzer }
|
|
|
|
constructor TUpvalueAnalyzer.Create;
|
|
begin
|
|
inherited Create;
|
|
FBoxedDeclarations := THashSet<IVariableDeclarationNode>.Create;
|
|
// Create a root scope to handle top-level definitions cleanly
|
|
FCurrentScope := TAnalysisScope.Create(nil);
|
|
end;
|
|
|
|
destructor TUpvalueAnalyzer.Destroy;
|
|
begin
|
|
// Unwind scope stack if exception occurred or Execute wasn't fully clean
|
|
while Assigned(FCurrentScope.FParent) do
|
|
begin
|
|
var temp := FCurrentScope;
|
|
FCurrentScope := FCurrentScope.FParent;
|
|
temp.Free;
|
|
end;
|
|
FCurrentScope.Free;
|
|
|
|
FBoxedDeclarations.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
function TUpvalueAnalyzer.Execute(const ARootNode: IAstNode): IAstNode;
|
|
begin
|
|
Result := Accept(ARootNode);
|
|
end;
|
|
|
|
class function TUpvalueAnalyzer.Analyze(const ARootNode: IAstNode): THashSet<IVariableDeclarationNode>;
|
|
var
|
|
analyzer: TUpvalueAnalyzer;
|
|
begin
|
|
// Note: AParent (IScopeDescriptor) removed from arguments as this analyzer
|
|
// builds its own structural view and only cares about boxing internal variables.
|
|
|
|
if not Assigned(ARootNode) then
|
|
exit(THashSet<IVariableDeclarationNode>.Create);
|
|
|
|
analyzer := TUpvalueAnalyzer.Create;
|
|
try
|
|
analyzer.Execute(ARootNode);
|
|
Result := analyzer.FBoxedDeclarations;
|
|
analyzer.FBoxedDeclarations := nil; // Transfer ownership
|
|
finally
|
|
analyzer.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TUpvalueAnalyzer.MarkDeclarationForBoxing(const AName: string);
|
|
var
|
|
depth: Integer;
|
|
declNode: IVariableDeclarationNode;
|
|
begin
|
|
// Check if the symbol exists in our analysis scopes
|
|
if FCurrentScope.Resolve(AName, depth, declNode) then
|
|
begin
|
|
// If it is found in a parent scope (Depth > 0) AND we have a trackable declaration node for it
|
|
if (depth > 0) and Assigned(declNode) then
|
|
begin
|
|
FBoxedDeclarations.Add(declNode);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TUpvalueAnalyzer.VisitIdentifier(const Node: IIdentifierNode): IAstNode;
|
|
begin
|
|
// Check if this identifier refers to a variable from an outer scope
|
|
MarkDeclarationForBoxing(Node.Name);
|
|
|
|
// Return original node (Analysis pass only)
|
|
Result := Node;
|
|
end;
|
|
|
|
function TUpvalueAnalyzer.VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
// 1. Enter new analysis scope
|
|
FCurrentScope := TAnalysisScope.Create(FCurrentScope);
|
|
try
|
|
// 2. Register parameters (they mask outer variables)
|
|
// We pass 'nil' as the node because we currently don't box parameters,
|
|
// but we must ensure Resolve() finds them so we don't accidentally box a shadowed variable.
|
|
for i := 0 to Node.Parameters.Count - 1 do
|
|
begin
|
|
FCurrentScope.Define(Node.Parameters[i].Name, nil);
|
|
end;
|
|
|
|
// 3. Visit Body
|
|
Accept(Node.Body); // Recursive call
|
|
|
|
// Rebuild if needed (default CoW behavior)
|
|
Result := Node;
|
|
finally
|
|
// 4. Exit scope
|
|
var temp := FCurrentScope;
|
|
FCurrentScope := FCurrentScope.FParent;
|
|
temp.Free;
|
|
end;
|
|
end;
|
|
|
|
function TUpvalueAnalyzer.VisitVariableDeclaration(const Node: IVariableDeclarationNode): IAstNode;
|
|
begin
|
|
// 1. Visit initializer first (it executes in the CURRENT scope)
|
|
if Assigned(Node.Initializer) then
|
|
Accept(Node.Initializer);
|
|
|
|
// 2. Define the variable in the CURRENT scope
|
|
// Store the Node reference so we can add it to FBoxedDeclarations if captured.
|
|
FCurrentScope.Define(Node.Target.AsIdentifier.Name, Node);
|
|
|
|
Result := Node;
|
|
end;
|
|
|
|
end.
|