unit Myc.Ast.Compiler.TypeChecker;

interface

uses
    System.SysUtils,
    System.Classes,
    System.Generics.Collections,
    Myc.Data.Scalar,
    Myc.Data.Value,
    Myc.Ast.Nodes,
    Myc.Ast.Visitor,
    Myc.Ast.Scope,
    Myc.Ast.Types,
    Myc.Ast.Identities,
    Myc.Ast;

type
    IAstTypeChecker = interface(IAstVisitor)
        function Execute(const RootNode: IAstNode): IAstNode;
    end;

    TTypeChecker = class(TAstTransformer, IAstTypeChecker)
    private
        type
            TTypeContext = class
            private
                FParent: TTypeContext;
                FLayout: IScopeLayout;
                FSlotTypes: TArray<IStaticType>;
                FUpvalueTypes: TArray<IStaticType>;
            public
                constructor Create(
                    AParent: TTypeContext;
                    ALayout: IScopeLayout;
                    const AUpvalueTypes: TArray<IStaticType>;
                    ADescriptor: IScopeDescriptor
                );
                function LookupType(const Address: TResolvedAddress): IStaticType;
                procedure SetType(SlotIndex: Integer; AType: IStaticType);
                property Types: TArray<IStaticType> read FSlotTypes;
            end;

    private
        FCurrentContext: TTypeContext;
        FLog: ICompilerLog;
        FRootScope: IExecutionScope;

        function CreateContextChain(L: IScopeLayout): TTypeContext;

        // Helpers for Null Propagation
        function PrepareBaseType(const BaseNode: IAstNode; out IsOptional: Boolean): IStaticType;
        function ApplyOptionality(const AType: IStaticType; IsOptional: Boolean): IStaticType;

    strict private
        // Typed Handlers (non-virtual, IAstNode signature)
        function VisitIdentifier(const Node: IAstNode): IAstNode;
        function VisitVariableDeclaration(const Node: IAstNode): IAstNode;
        function VisitAssignment(const Node: IAstNode): IAstNode;
        function VisitLambdaExpression(const Node: IAstNode): IAstNode;
        function VisitFunctionCall(const Node: IAstNode): IAstNode;
        function VisitBlockExpression(const Node: IAstNode): IAstNode;
        function VisitIfExpression(const Node: IAstNode): IAstNode;
        function VisitMemberAccess(const Node: IAstNode): IAstNode;
        function VisitIndexer(const Node: IAstNode): IAstNode;
        function VisitCreateSeries(const Node: IAstNode): IAstNode;
        function VisitSeriesLength(const Node: IAstNode): IAstNode;
        function VisitRecurNode(const Node: IAstNode): IAstNode;
        function VisitNop(const Node: IAstNode): IAstNode;
        function VisitRecordLiteral(const Node: IAstNode): IAstNode;
        function VisitConstant(const Node: IAstNode): IAstNode;
        function VisitKeyword(const Node: IAstNode): IAstNode;

        // Pipe Support
        function VisitPipeInput(const Node: IAstNode): IAstNode;
        function VisitPipe(const Node: IAstNode): IAstNode;

    protected
        procedure SetupHandlers; override;

    public
        constructor Create(const RootLayout: IScopeLayout; const RootScope: IExecutionScope; const ALog: ICompilerLog);
        destructor Destroy; override;

        function Execute(const RootNode: IAstNode): IAstNode;

        class function CheckTypes(
            const RootNode: IAstNode;
            const Layout: IScopeLayout;
            const RootScope: IExecutionScope;
            const ALog: ICompilerLog
        ): IAstNode; static;
    end;

implementation

uses
    System.Generics.Defaults,
    Myc.Data.Keyword;

{ TTypeChecker.TTypeContext }

constructor TTypeChecker.TTypeContext.Create(
    AParent: TTypeContext;
    ALayout: IScopeLayout;
    const AUpvalueTypes: TArray<IStaticType>;
    ADescriptor: IScopeDescriptor
);
var
    i: Integer;
begin
    inherited Create;
    FParent := AParent;
    FLayout := ALayout;
    FUpvalueTypes := AUpvalueTypes;

    if Assigned(FLayout) then
    begin
        SetLength(FSlotTypes, FLayout.SlotCount);

        if Assigned(ADescriptor) then
        begin
            // Load known types from descriptor (e.g. for Root Scope / RTL)
            for i := 0 to High(FSlotTypes) do
                FSlotTypes[i] := ADescriptor.GetSymbolType(i);
        end
        else
        begin
            // Initialize with Unknown for new scopes
            for i := 0 to High(FSlotTypes) do
                FSlotTypes[i] := TTypes.Unknown;
        end;
    end;
end;

function TTypeChecker.TTypeContext.LookupType(const Address: TResolvedAddress): IStaticType;
var
    ctx: TTypeContext;
    i: Integer;
begin
    case Address.Kind of
        akLocalOrParent:
        begin
            ctx := Self;
            for i := 1 to Address.ScopeDepth do
            begin
                if not Assigned(ctx.FParent) then
                begin
                    Result := TTypes.Unknown;
                    Exit;
                end;
                ctx := ctx.FParent;
            end;

            if (Address.SlotIndex >= 0) and (Address.SlotIndex < Length(ctx.FSlotTypes)) then
                Result := ctx.FSlotTypes[Address.SlotIndex]
            else
                Result := TTypes.Unknown;
        end;
        akUpvalue:
        begin
            if (Address.SlotIndex >= 0) and (Address.SlotIndex < Length(FUpvalueTypes)) then
                Result := FUpvalueTypes[Address.SlotIndex]
            else
                Result := TTypes.Unknown;
        end;
    else
        Result := TTypes.Unknown;
    end;
end;

procedure TTypeChecker.TTypeContext.SetType(SlotIndex: Integer; AType: IStaticType);
begin
    if (SlotIndex >= 0) and (SlotIndex < Length(FSlotTypes)) then
        FSlotTypes[SlotIndex] := AType;
end;

{ TTypeChecker }

function TTypeChecker.CreateContextChain(L: IScopeLayout): TTypeContext;
var
    p: TTypeContext;
    desc: IScopeDescriptor;
begin
    if L = nil then
        exit(nil);

    p := CreateContextChain(L.Parent);

    desc := nil;
    if (L.Parent = nil) and Assigned(FRootScope) then
    begin
        desc := FRootScope.Descriptor;
    end;

    Result := TTypeContext.Create(p, L, [], desc);
end;

constructor TTypeChecker.Create(const RootLayout: IScopeLayout; const RootScope: IExecutionScope; const ALog: ICompilerLog);
begin
    inherited Create;
    FLog := ALog;
    FRootScope := RootScope;

    FCurrentContext := CreateContextChain(RootLayout);
    if FCurrentContext = nil then
        FCurrentContext := TTypeContext.Create(nil, nil, [], nil);
end;

destructor TTypeChecker.Destroy;
begin
    while Assigned(FCurrentContext) do
    begin
        var temp := FCurrentContext;
        FCurrentContext := FCurrentContext.FParent;
        temp.Free;
    end;
    inherited;
end;

procedure TTypeChecker.SetupHandlers;
begin
    inherited SetupHandlers; // Load Defaults

    Register(akIdentifier, VisitIdentifier);
    Register(akVariableDeclaration, VisitVariableDeclaration);
    Register(akAssignment, VisitAssignment);
    Register(akLambdaExpression, VisitLambdaExpression);
    Register(akFunctionCall, VisitFunctionCall);
    Register(akBlockExpression, VisitBlockExpression);
    Register(akIfExpression, VisitIfExpression);
    Register(akMemberAccess, VisitMemberAccess);
    Register(akIndexer, VisitIndexer);
    Register(akCreateSeries, VisitCreateSeries);
    Register(akSeriesLength, VisitSeriesLength);
    Register(akRecur, VisitRecurNode);
    Register(akNop, VisitNop);
    Register(akRecordLiteral, VisitRecordLiteral);
    Register(akConstant, VisitConstant);
    Register(akKeyword, VisitKeyword);

    // Pipe Support
    Register(akPipeInput, VisitPipeInput);
    Register(akPipe, VisitPipe);
end;

class function TTypeChecker.CheckTypes(
    const RootNode: IAstNode;
    const Layout: IScopeLayout;
    const RootScope: IExecutionScope;
    const ALog: ICompilerLog
): IAstNode;
var
    startLayout: IScopeLayout;
begin
    if Assigned(Layout) then
        startLayout := Layout.Parent
    else
        startLayout := nil;

    var checker := TTypeChecker.Create(startLayout, RootScope, ALog) as IAstTypeChecker;
    Result := checker.Execute(RootNode);
end;

function TTypeChecker.Execute(const RootNode: IAstNode): IAstNode;
begin
    Result := Accept(RootNode);
    if not Assigned(Result) then
        Result := TAst.Block([], nil);
end;

// --- Helper ---

function TTypeChecker.PrepareBaseType(const BaseNode: IAstNode; out IsOptional: Boolean): IStaticType;
var
    baseType: IStaticType;
begin
    baseType := BaseNode.AsTypedNode.StaticType;
    IsOptional := baseType.IsOptional;
    Result := TTypes.Unwrap(baseType);
end;

function TTypeChecker.ApplyOptionality(const AType: IStaticType; IsOptional: Boolean): IStaticType;
begin
    if IsOptional and (AType.Kind <> stUnknown) then
        Result := TTypes.MakeOptional(AType)
    else
        Result := AType;
end;

// --- Visits ---

function TTypeChecker.VisitConstant(const Node: IAstNode): IAstNode;
begin
    // Base implementation already returns Node, but here we explicitly confirm identity for clarity
    Result := Node;
end;

function TTypeChecker.VisitKeyword(const Node: IAstNode): IAstNode;
begin
    Result := Node;
end;

function TTypeChecker.VisitIdentifier(const Node: IAstNode): IAstNode;
var
    I: IIdentifierNode;
    typ: IStaticType;
    adr: TResolvedAddress;
    identity: INamedIdentity;
begin
    I := Node.AsIdentifier;
    adr := I.Address;
    identity := I.Identity.AsNamed;

    if adr.Kind = akUnresolved then
    begin
        Result := TAst.Identifier(identity, adr, TTypes.Unknown);
        Exit;
    end;

    typ := FCurrentContext.LookupType(adr);
    Result := TAst.Identifier(identity, adr, typ);
end;

function TTypeChecker.VisitRecurNode(const Node: IAstNode): IAstNode;
var
    R: IRecurNode;
    args: IArgumentList;
begin
    R := Node.AsRecur;
    // Use inherited to transform arguments, then reconstruct with type Void
    // Note: inherited VisitRecurNode returns IAstNode which is a RecurNode.
    // We can call Accept on arguments list directly to avoid intermediate node creation if desired,
    // but relying on inherited logic keeps it consistent.

    // Efficient approach: Accept the arguments list directly (it's a child).
    args := Accept(R.Arguments).AsArgumentList;
    Result := TAst.Recur(Node.Identity, args, TTypes.Void);
end;

function TTypeChecker.VisitVariableDeclaration(const Node: IAstNode): IAstNode;
var
    V: IVariableDeclarationNode;
    initType: IStaticType;
    newInitializer, newIdent: IAstNode;
    adr: TResolvedAddress;
    identNode: IIdentifierNode;
begin
    V := Node.AsVariableDeclaration;
    identNode := V.Target.AsIdentifier;
    adr := identNode.Address;
    initType := TTypes.Unknown;

    if adr.Kind = akUnresolved then
    begin
        if Assigned(V.Initializer) then
            Accept(V.Initializer);
        Result := Node;
        Exit;
    end;

    if Assigned(V.Initializer) then
        newInitializer := Accept(V.Initializer)
    else
        newInitializer := nil;

    if Assigned(newInitializer) then
        initType := newInitializer.AsTypedNode.StaticType;

    if initType.Kind <> stUnknown then
        FCurrentContext.SetType(adr.SlotIndex, initType);

    newIdent := TAst.Identifier(identNode.Identity.AsNamed, adr, initType);
    Result := TAst.VarDecl(Node.Identity, newIdent, newInitializer, initType, V.IsBoxed);
end;

function TTypeChecker.VisitAssignment(const Node: IAstNode): IAstNode;
var
    A: IAssignmentNode;
    targetType, sourceType: IStaticType;
    newIdent, newValue: IAstNode;
    adr: TResolvedAddress;
    identNode: IIdentifierNode;
begin
    A := Node.AsAssignment;
    identNode := A.Target.AsIdentifier;
    newIdent := Accept(A.Target);
    targetType := newIdent.AsTypedNode.StaticType;
    adr := identNode.Address;

    if adr.Kind = akUnresolved then
    begin
        Accept(A.Value);
        Result := Node;
        Exit;
    end;

    newValue := Accept(A.Value);
    sourceType := newValue.AsTypedNode.StaticType;

    if (targetType.Kind <> stUnknown) and (sourceType.Kind <> stUnknown) then
    begin
        if not TTypeRules.CanAssign(targetType, sourceType) then
        begin
            if Assigned(FLog) then
                FLog.AddError(Format('Cannot assign type %s to %s', [sourceType.ToString, targetType.ToString]), Node);
        end;
    end;

    if (targetType.Kind = stUnknown) and (sourceType.Kind <> stUnknown) then
    begin
        FCurrentContext.SetType(adr.SlotIndex, sourceType);
        newIdent := TAst.Identifier(identNode.Identity.AsNamed, adr, sourceType);
        targetType := sourceType;
    end;

    Result := TAst.Assign(Node.Identity, newIdent, newValue, targetType);
end;

function TTypeChecker.VisitBlockExpression(const Node: IAstNode): IAstNode;
var
    newBlock: IBlockExpressionNode;
    blockType: IStaticType;
    exprs: IExpressionList;
begin
    // Inherited logic transforms all expressions in the list
    newBlock := inherited VisitBlockExpression(Node).AsBlockExpression;
    exprs := newBlock.Expressions;

    if exprs.Count > 0 then
        blockType := exprs[exprs.Count - 1].AsTypedNode.StaticType
    else
        blockType := TTypes.Void;

    // Return new block with calculated type
    Result := TAst.Block(Node.Identity, exprs, blockType);
end;

function TTypeChecker.VisitLambdaExpression(const Node: IAstNode): IAstNode;
var
    L: ILambdaExpressionNode;
    newParams: TArray<IIdentifierNode>;
    newBody: IAstNode;
    bodyType, methodType: IStaticType;
    paramTypes: TArray<IStaticType>;
    upvalueTypes: TArray<IStaticType>;
    i: Integer;
    finalDescriptor: IScopeDescriptor;
    paramIdent: IIdentifierNode;
    injectedType: IStaticType;
begin
    L := Node.AsLambdaExpression;

    // 1. Resolve Upvalue Types
    var upvalueAddrs := L.Upvalues;
    SetLength(upvalueTypes, Length(upvalueAddrs));

    for i := 0 to High(upvalueAddrs) do
    begin
        var lookupAddr := upvalueAddrs[i];
        if lookupAddr.Kind = akLocalOrParent then
        begin
            if lookupAddr.ScopeDepth > 0 then
                Dec(lookupAddr.ScopeDepth);
        end;
        upvalueTypes[i] := FCurrentContext.LookupType(lookupAddr);
    end;

    // 2. Enter New Scope
    FCurrentContext := TTypeContext.Create(FCurrentContext, L.Layout, upvalueTypes, nil);
    try
        SetLength(newParams, L.Parameters.Count);
        SetLength(paramTypes, L.Parameters.Count);

        for i := 0 to L.Parameters.Count - 1 do
        begin
            paramIdent := L.Parameters[i];

            // Check if there is already a type assigned (e.g. injected by Pipe Visitor)
            injectedType := paramIdent.AsTypedNode.StaticType;

            if injectedType.Kind = stUnknown then
                paramTypes[i] := TTypes.Unknown
            else
                paramTypes[i] := injectedType;

            if paramIdent.Address.Kind <> akUnresolved then
                FCurrentContext.SetType(paramIdent.Address.SlotIndex, paramTypes[i]);

            newParams[i] := TAst.Identifier(paramIdent.Identity.AsNamed, paramIdent.Address, paramTypes[i]);
        end;

        newBody := Accept(L.Body);

        bodyType := newBody.AsTypedNode.StaticType;
        methodType := TTypes.CreateMethod(paramTypes, bodyType);

        finalDescriptor := TScope.CreateDescriptor(L.Layout, FCurrentContext.Types);
    finally
        var temp := FCurrentContext;
        FCurrentContext := FCurrentContext.FParent;
        temp.Free;
    end;

    var paramList := TParameterList.Create(newParams, L.Parameters.Identity);
    Result :=
        TAst.LambdaExpr(Node.Identity, paramList, newBody, L.Layout, finalDescriptor, L.Upvalues, L.HasNestedLambdas, L.IsPure, methodType);
end;

function TTypeChecker.VisitFunctionCall(const Node: IAstNode): IAstNode;
var
    newCall: IFunctionCallNode;
    calleeType, retType: IStaticType;
    i, j: Integer;
    argTypes: TArray<IStaticType>;
    hasUnknownArgs: Boolean;
    bestSig: IMethodSignature;
    match: Boolean;
begin
    // Use inherited to visit Callee and Arguments first
    newCall := inherited VisitFunctionCall(Node).AsFunctionCall;

    // Now analyze types on the transformed children
    var newCallee := newCall.Callee;
    var newArgs := newCall.Arguments;

    SetLength(argTypes, newArgs.Count);
    hasUnknownArgs := False;

    for i := 0 to newArgs.Count - 1 do
    begin
        argTypes[i] := newArgs[i].AsTypedNode.StaticType;
        if argTypes[i].Kind = stUnknown then
            hasUnknownArgs := True;
    end;

    calleeType := newCallee.AsTypedNode.StaticType;
    retType := TTypes.Unknown;

    if calleeType.Kind = stMethod then
    begin
        if not hasUnknownArgs then
        begin
            bestSig := nil;
            for var sig in calleeType.Signatures do
            begin
                if Length(sig.ParamTypes) <> Length(argTypes) then
                    continue;
                match := True;
                for j := 0 to High(argTypes) do
                    if not TTypeRules.CanAssign(sig.ParamTypes[j], argTypes[j]) then
                    begin
                        match := False;
                        break;
                    end;
                if match then
                begin
                    bestSig := sig;
                    break;
                end;
            end;

            if Assigned(bestSig) then
                retType := bestSig.ReturnType
            else if Assigned(FLog) then
                FLog.AddError(Format('No matching signature found for method call on %s', [calleeType.ToString]), Node);
        end;
    end
    else if calleeType.Kind <> stUnknown then
        if Assigned(FLog) then
            FLog.AddError(Format('Cannot invoke type %s as a function.', [calleeType.ToString]), Node);

    // Return new node with Types
    Result := TAst.FunctionCall(Node.Identity, newCallee, newArgs, retType, newCall.IsTailCall, nil, False);
end;

function TTypeChecker.VisitIfExpression(const Node: IAstNode): IAstNode;
var
    newIf: IIfExpressionNode;
    resType: IStaticType;
begin
    newIf := inherited VisitIfExpression(Node).AsIfExpression;

    if (newIf.ElseBranch <> nil) then
        resType := TTypeRules.Promote(newIf.ThenBranch.AsTypedNode.StaticType, newIf.ElseBranch.AsTypedNode.StaticType)
    else
        resType := TTypes.MakeOptional(newIf.ThenBranch.AsTypedNode.StaticType);

    Result := TAst.IfExpr(Node.Identity, newIf.Condition, newIf.ThenBranch, newIf.ElseBranch, resType);
end;

function TTypeChecker.VisitIndexer(const Node: IAstNode): IAstNode;
var
    I: IIndexerNode;
    newBase, newIndex: IAstNode;
    baseType, elemType: IStaticType;
    isOpt: Boolean;
begin
    I := Node.AsIndexer;
    newBase := Accept(I.Base);
    newIndex := Accept(I.Index);
    baseType := PrepareBaseType(newBase, isOpt);

    elemType := TTypes.Unknown;
    if baseType.Kind = stSeries then
        elemType := baseType.ElementType
    else if baseType.Kind = stRecordSeries then
        elemType := TTypes.CreateRecord(baseType.Definition);

    elemType := ApplyOptionality(elemType, isOpt);
    Result := TAst.Indexer(Node.Identity, newBase, newIndex, elemType);
end;

function TTypeChecker.VisitMemberAccess(const Node: IAstNode): IAstNode;
var
    M: IMemberAccessNode;
    newBase: IAstNode;
    baseType, resType: IStaticType;
    idx: Integer;
    isOpt: Boolean;
begin
    M := Node.AsMemberAccess;
    newBase := Accept(M.Base);
    baseType := PrepareBaseType(newBase, isOpt);
    resType := TTypes.Unknown;

    if (baseType.Kind = stRecord) or (baseType.Kind = stRecordSeries) then
    begin
        idx := baseType.Definition.IndexOf(M.Member.Value);
        if idx >= 0 then
        begin
            var fieldType := TTypes.FromScalarKind(baseType.Definition.Items[idx].Value);
            if baseType.Kind = stRecordSeries then
                resType := TTypes.CreateSeries(fieldType)
            else
                resType := fieldType;
        end
        else if Assigned(FLog) then
            FLog.AddError('Member not found', Node);
    end;

    resType := ApplyOptionality(resType, isOpt);
    Result := TAst.MemberAccess(Node.Identity, newBase, M.Member, resType);
end;

function TTypeChecker.VisitRecordLiteral(const Node: IAstNode): IAstNode;
var
    R: IRecordLiteralNode;
    i: Integer;
    fieldTypes: TArray<TPair<IKeyword, IStaticType>>;
    scalarFieldTypes: TArray<TPair<IKeyword, TScalar.TKind>>;
    isScalar: Boolean;
    valType: IStaticType;
    key: IKeyword;
    newFieldList: IRecordFieldList;
begin
    R := Node.AsRecordLiteral;
    // Transform fields using inherited recursion
    newFieldList := inherited VisitRecordFieldList(R.Fields).AsRecordFieldList;

    // Now analyze the transformed fields
    var count := newFieldList.Count;
    SetLength(fieldTypes, count);
    SetLength(scalarFieldTypes, count);
    isScalar := True;

    for i := 0 to count - 1 do
    begin
        var field := newFieldList[i];
        key := field.Key.Value;
        valType := field.Value.AsTypedNode.StaticType;

        // Check if strict scalar (no optionals allowed in packed ScalarRecord)
        if (valType.Kind in [stOrdinal, stFloat, stBoolean, stDateTime, stKeyword]) and (not valType.IsOptional) then
        begin
            var kind: TScalar.TKind;
            case valType.Kind of
                stOrdinal: kind := TScalar.TKind.Ordinal;
                stFloat: kind := TScalar.TKind.Float;
                stBoolean: kind := TScalar.TKind.Boolean;
                stDateTime: kind := TScalar.TKind.DateTime;
                stKeyword: kind := TScalar.TKind.Keyword;
            else
                kind := TScalar.TKind.Ordinal;
            end;
            scalarFieldTypes[i] := TPair<IKeyword, TScalar.TKind>.Create(key, kind);
        end
        else
        begin
            isScalar := False;
        end;

        fieldTypes[i] := TPair<IKeyword, IStaticType>.Create(key, valType);
    end;

    // Build Definition
    var scalarDef: IScalarRecordDefinition := nil;
    var genericDef: IGenericRecordDefinition := nil;
    var resultType: IStaticType;

    if isScalar and (count > 0) then
    begin
        scalarDef := TKeywordMappingRegistry<TScalar.TKind>.Intern(scalarFieldTypes);
        resultType := TTypes.CreateRecord(scalarDef);
    end
    else
    begin
        genericDef := TGenericRecordRegistry.Intern(fieldTypes);
        resultType := TTypes.CreateGenericRecord(genericDef);
    end;

    Result := TAst.RecordLiteral(Node.Identity, newFieldList, scalarDef, genericDef, resultType);
end;

function TTypeChecker.VisitCreateSeries(const Node: IAstNode): IAstNode;
var
    C: ICreateSeriesNode;
    elemType: IStaticType;
    def: string;
begin
    C := Node.AsCreateSeries;
    def := C.Definition;
    // Simple heuristic for type
    if def.StartsWith('[') then
        elemType := TTypes.CreateRecord(nil) // Placeholder, normally parses JSON
    else
        elemType := TTypes.FromScalarKind(TScalar.StringToKind(def));

    Result := TAst.CreateSeries(Node.Identity.AsDefinition, TTypes.CreateSeries(elemType));
end;

function TTypeChecker.VisitSeriesLength(const Node: IAstNode): IAstNode;
begin
    Result := TAst.SeriesLength(Node.Identity, Accept(Node.AsSeriesLength.Series).AsIdentifier, TTypes.Ordinal);
end;

function TTypeChecker.VisitNop(const Node: IAstNode): IAstNode;
begin
    Result := TAst.Nop(Node.Identity, TTypes.Void);
end;

// =============================================================================
// PIPE IMPLEMENTATION (Type Checking)
// =============================================================================

function TTypeChecker.VisitPipeInput(const Node: IAstNode): IAstNode;
var
    P: IPipeInputNode;
    newSource: IIdentifierNode;
    sourceType: IStaticType;
begin
    P := Node.AsPipeInput;
    // Transform source identifier (resolves type)
    newSource := Accept(P.StreamSource).AsIdentifier;
    sourceType := newSource.AsTypedNode.StaticType;

    // 1. Verify Source is a Series-compatible type
    if (sourceType.Kind <> stUnknown) then
    begin
        if not ((sourceType.Kind = stSeries) or (sourceType.Kind = stRecordSeries)) then
        begin
            if Assigned(FLog) then
                FLog.AddError(
                    Format('Pipe input "%s" must be a Series or RecordSeries, but got %s.', [newSource.Name, sourceType.ToString]),
                    Node
                );
        end
        else if (sourceType.Kind = stRecordSeries) then
        begin
            // 2. Verify Selectors exist in Record Definition
            var def := sourceType.Definition;
            for var sel in P.Selectors do
            begin
                if def.IndexOf(sel.Value) < 0 then
                begin
                    if Assigned(FLog) then
                        FLog.AddError(Format('Field ":%s" not found in stream "%s".', [sel.Value.Name, newSource.Name]), sel);
                end;
            end;
        end;
    end;

    // Reuse selectors (they are just keywords, no type checking needed)
    Result := TAst.PipeInput(newSource, P.Selectors, Node.Identity.Location);
end;

function TTypeChecker.VisitPipe(const Node: IAstNode): IAstNode;
var
    P: IPipeNode;
    i, k: Integer;
    inputNode: IPipeInputNode;
    newInputs: TArray<IPipeInputNode>;
    streamType: IStaticType;
    paramTypes: TList<IStaticType>;
    lambda: ILambdaExpressionNode;
    newParams: TArray<IIdentifierNode>;
    inferredType: IStaticType;
begin
    P := Node.AsPipe;
    SetLength(newInputs, P.Inputs.Count);
    paramTypes := TList<IStaticType>.Create;
    try
        // 1. Visit Inputs and collect types for Lambda parameters
        for i := 0 to P.Inputs.Count - 1 do
        begin
            // Recurse on inputs to resolve their sources
            inputNode := Accept(P.Inputs[i]).AsPipeInput;
            newInputs[i] := inputNode;

            streamType := inputNode.StreamSource.AsTypedNode.StaticType;

            // Flatten logic: One lambda param per selector
            for var sel in inputNode.Selectors do
            begin
                inferredType := TTypes.Unknown;
                if streamType.Kind = stRecordSeries then
                begin
                    // Extract field type from definition
                    var def := streamType.Definition;
                    var idx := def.IndexOf(sel.Value);
                    if idx >= 0 then
                        inferredType := TTypes.FromScalarKind(def.Items[idx].Value);
                end
                else if streamType.Kind = stSeries then
                begin
                    // If simple series, use its element type
                    if Assigned(streamType.ElementType) then
                        inferredType := streamType.ElementType
                    else
                        inferredType := TTypes.Ordinal; // Fallback default
                end;

                paramTypes.Add(inferredType);
            end;
        end;

        // 2. Prepare Lambda with Inferred Types
        lambda := P.Transformation;

        if lambda.Parameters.Count <> paramTypes.Count then
        begin
            if Assigned(FLog) then
                FLog.AddError(
                    Format(
                        'Pipe lambda expects %d parameters (one per selector), but got %d.',
                        [paramTypes.Count, lambda.Parameters.Count]
                    ),
                    lambda
                );
        end;

        SetLength(newParams, lambda.Parameters.Count);
        for k := 0 to lambda.Parameters.Count - 1 do
        begin
            var oldP := lambda.Parameters[k];
            var typeToInject :=
                if k < paramTypes.Count then paramTypes[k]
                else TTypes.Unknown;

            // Create new identifier with the inferred type!
            newParams[k] := TAst.Identifier(oldP.Identity.AsNamed, oldP.Address, typeToInject);
        end;

        // Recreate Lambda NODE with Typed Parameters
        var preTypedLambda :=
            TAst.LambdaExpr(
                lambda.Identity,
                TParameterList.Create(newParams, lambda.Parameters.Identity),
                lambda.Body,
                lambda.Layout,
                lambda.Descriptor,
                lambda.Upvalues,
                lambda.HasNestedLambdas,
                lambda.IsPure,
                TTypes.Unknown // Will be recalculated in Accept
            );

        // 3. Visit the Lambda (This will now type-check the Body using the types we just injected)
        var typedLambda := Accept(preTypedLambda).AsLambdaExpression;

        // 4. Infer Pipe Return Type & Validate Strictness
        var lambdaRetType := typedLambda.AsTypedNode.StaticType.Signatures[0].ReturnType;
        var pipeType: IStaticType;

        if lambdaRetType.Kind = stRecord then
        begin
            // Valid: Record -> RecordSeries
            pipeType := TTypes.CreateRecordSeries(lambdaRetType.Definition);
        end
        else
        begin
            // STRICT CHECK: Scalars are NOT allowed.
            if (lambdaRetType.Kind <> stUnknown) and (lambdaRetType.Kind <> stVoid) then
            begin
                if Assigned(FLog) then
                    FLog.AddError(
                        Format('Pipe function must return a Record (e.g. {:res ...}). Type "%s" is invalid.', [lambdaRetType.ToString]),
                        lambda
                    );
            end;

            // If Void or Unknown, or Error case:
            pipeType := TTypes.Unknown;
        end;

        Result := TAst.Pipe(Node.Identity, TPipeInputList.Create(newInputs, P.Inputs.Identity), typedLambda, pipeType);
    finally
        paramTypes.Free;
    end;
end;

end.
