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);

    strict private
        // Analysis Handlers (IAstNode signature)
        function VisitLambdaExpression(const Node: IAstNode): IAstNode;
        function VisitIdentifier(const Node: IAstNode): IAstNode;
        function VisitVariableDeclaration(const Node: IAstNode): IAstNode;

    protected
        procedure SetupHandlers; 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;

procedure TUpvalueAnalyzer.SetupHandlers;
begin
    inherited SetupHandlers; // Load default transformer logic

    // Override specific handlers for analysis
    Register(akLambdaExpression, VisitLambdaExpression);
    Register(akIdentifier, VisitIdentifier);
    Register(akVariableDeclaration, VisitVariableDeclaration);
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: IAstNode): IAstNode;
begin
    // Check if this identifier refers to a variable from an outer scope
    MarkDeclarationForBoxing(Node.AsIdentifier.Name);

    // Return original node (Analysis pass only)
    Result := Node;
end;

function TUpvalueAnalyzer.VisitLambdaExpression(const Node: IAstNode): IAstNode;
var
    L: ILambdaExpressionNode;
    i: Integer;
begin
    L := Node.AsLambdaExpression;

    // 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 L.Parameters.Count - 1 do
        begin
            FCurrentScope.Define(L.Parameters[i].Name, nil);
        end;

        // 3. Visit Body
        Accept(L.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: IAstNode): IAstNode;
var
    V: IVariableDeclarationNode;
begin
    V := Node.AsVariableDeclaration;

    // 1. Visit initializer first (it executes in the CURRENT scope)
    if Assigned(V.Initializer) then
        Accept(V.Initializer);

    // 2. Define the variable in the CURRENT scope
    // Store the Node reference so we can add it to FBoxedDeclarations if captured.
    FCurrentScope.Define(V.Target.AsIdentifier.Name, V);

    Result := Node;
end;

end.
