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; FUpvalueTypes: TArray; public constructor Create( AParent: TTypeContext; ALayout: IScopeLayout; const AUpvalueTypes: TArray; ADescriptor: IScopeDescriptor ); function LookupType(const Address: TResolvedAddress): IStaticType; procedure SetType(SlotIndex: Integer; AType: IStaticType); property Types: TArray 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; 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; newBody: IAstNode; bodyType, methodType: IStaticType; paramTypes: TArray; upvalueTypes: TArray; 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; 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[idx]); 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>; scalarFieldTypes: TArray>; 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.Create(key, kind); end else begin isScalar := False; end; fieldTypes[i] := TPair.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.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; streamType: IStaticType; paramTypes: TList; lambda: ILambdaExpressionNode; newParams: TArray; inferredType: IStaticType; begin P := Node.AsPipe; SetLength(newInputs, P.Inputs.Count); paramTypes := TList.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[idx]); 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.