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; protected function VisitIdentifier(const Node: IIdentifierNode): IAstNode; override; function VisitVariableDeclaration(const Node: IVariableDeclarationNode): IAstNode; override; function VisitAssignment(const Node: IAssignmentNode): IAstNode; override; function VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode; override; function VisitFunctionCall(const Node: IFunctionCallNode): IAstNode; override; function VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode; override; function VisitIfExpression(const Node: IIfExpressionNode): IAstNode; override; function VisitMemberAccess(const Node: IMemberAccessNode): IAstNode; override; function VisitIndexer(const Node: IIndexerNode): IAstNode; override; function VisitCreateSeries(const Node: ICreateSeriesNode): IAstNode; override; function VisitSeriesLength(const Node: ISeriesLengthNode): IAstNode; override; function VisitRecurNode(const Node: IRecurNode): IAstNode; override; function VisitNop(const Node: INopNode): IAstNode; override; function VisitRecordLiteral(const Node: IRecordLiteralNode): IAstNode; override; function VisitConstant(const Node: IConstantNode): IAstNode; override; function VisitKeyword(const Node: IKeywordNode): IAstNode; override; // Pipe Support function VisitPipeInput(const Node: IPipeInputNode): IAstNode; override; function VisitPipe(const Node: IPipeNode): IAstNode; 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; 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: IConstantNode): IAstNode; begin Result := Node; end; function TTypeChecker.VisitKeyword(const Node: IKeywordNode): IAstNode; begin Result := Node; end; function TTypeChecker.VisitIdentifier(const Node: IIdentifierNode): IAstNode; var typ: IStaticType; adr: TResolvedAddress; identity: INamedIdentity; begin adr := Node.Address; identity := Node.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: IRecurNode): IAstNode; var newArgs: TArray; i: Integer; begin SetLength(newArgs, Node.Arguments.Count); for i := 0 to Node.Arguments.Count - 1 do newArgs[i] := Accept(Node.Arguments[i]); var argList := TArgumentList.Create(newArgs, Node.Arguments.Identity); Result := TAst.Recur(Node.Identity, argList, TTypes.Void); end; function TTypeChecker.VisitVariableDeclaration(const Node: IVariableDeclarationNode): IAstNode; var initType: IStaticType; newInitializer, newIdent: IAstNode; adr: TResolvedAddress; identNode: IIdentifierNode; begin identNode := Node.Target.AsIdentifier; adr := identNode.Address; initType := TTypes.Unknown; if adr.Kind = akUnresolved then begin if Assigned(Node.Initializer) then Accept(Node.Initializer); Result := Node; Exit; end; if Assigned(Node.Initializer) then newInitializer := Accept(Node.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, Node.IsBoxed); end; function TTypeChecker.VisitAssignment(const Node: IAssignmentNode): IAstNode; var targetType, sourceType: IStaticType; newIdent, newValue: IAstNode; adr: TResolvedAddress; identNode: IIdentifierNode; begin identNode := Node.Target.AsIdentifier; newIdent := Accept(Node.Target); targetType := newIdent.AsTypedNode.StaticType; adr := identNode.Address; if adr.Kind = akUnresolved then begin Accept(Node.Value); Result := Node; Exit; end; newValue := Accept(Node.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: IBlockExpressionNode): IAstNode; var blockType: IStaticType; newExprs: TArray; i: Integer; begin SetLength(newExprs, Node.Expressions.Count); for i := 0 to Node.Expressions.Count - 1 do newExprs[i] := Accept(Node.Expressions[i]); if Length(newExprs) > 0 then blockType := newExprs[High(newExprs)].AsTypedNode.StaticType else blockType := TTypes.Void; var exprList := TExpressionList.Create(newExprs, Node.Expressions.Identity); Result := TAst.Block(Node.Identity, exprList, blockType); end; function TTypeChecker.VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode; var newParams: TArray; newBody: IAstNode; bodyType, methodType: IStaticType; paramTypes: TArray; upvalueTypes: TArray; i: Integer; finalDescriptor: IScopeDescriptor; paramIdent: IIdentifierNode; injectedType: IStaticType; begin // 1. Resolve Upvalue Types var upvalueAddrs := Node.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, Node.Layout, upvalueTypes, nil); try SetLength(newParams, Node.Parameters.Count); SetLength(paramTypes, Node.Parameters.Count); for i := 0 to Node.Parameters.Count - 1 do begin paramIdent := Node.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(Node.Body); bodyType := newBody.AsTypedNode.StaticType; methodType := TTypes.CreateMethod(paramTypes, bodyType); finalDescriptor := TScope.CreateDescriptor(Node.Layout, FCurrentContext.Types); finally var temp := FCurrentContext; FCurrentContext := FCurrentContext.FParent; temp.Free; end; var paramList := TParameterList.Create(newParams, Node.Parameters.Identity); Result := TAst.LambdaExpr( Node.Identity, paramList, newBody, Node.Layout, finalDescriptor, Node.Upvalues, Node.HasNestedLambdas, Node.IsPure, methodType ); end; function TTypeChecker.VisitFunctionCall(const Node: IFunctionCallNode): IAstNode; var calleeType, retType: IStaticType; i, j: Integer; newCallee: IAstNode; newArgs: TArray; argTypes: TArray; hasUnknownArgs: Boolean; bestSig: IMethodSignature; match: Boolean; begin newCallee := Accept(Node.Callee); SetLength(newArgs, Node.Arguments.Count); SetLength(argTypes, Node.Arguments.Count); hasUnknownArgs := False; for i := 0 to Node.Arguments.Count - 1 do begin newArgs[i] := Accept(Node.Arguments[i]); 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); var argList := TArgumentList.Create(newArgs, Node.Arguments.Identity); Result := TAst.FunctionCall(Node.Identity, newCallee, argList, retType, Node.IsTailCall, nil, False); end; function TTypeChecker.VisitIfExpression(const Node: IIfExpressionNode): IAstNode; var newCond, newThen, newElse: IAstNode; resType: IStaticType; begin newCond := Accept(Node.Condition); newThen := Accept(Node.ThenBranch); newElse := Accept(Node.ElseBranch); // Simple promotion logic if (newElse <> nil) then resType := TTypeRules.Promote(newThen.AsTypedNode.StaticType, newElse.AsTypedNode.StaticType) else resType := TTypes.MakeOptional(newThen.AsTypedNode.StaticType); Result := TAst.IfExpr(Node.Identity, newCond, newThen, newElse, resType); end; function TTypeChecker.VisitIndexer(const Node: IIndexerNode): IAstNode; var newBase, newIndex: IAstNode; baseType, elemType: IStaticType; isOpt: Boolean; begin newBase := Accept(Node.Base); newIndex := Accept(Node.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: IMemberAccessNode): IAstNode; var newBase: IAstNode; baseType, resType: IStaticType; idx: Integer; isOpt: Boolean; begin newBase := Accept(Node.Base); baseType := PrepareBaseType(newBase, isOpt); resType := TTypes.Unknown; if (baseType.Kind = stRecord) or (baseType.Kind = stRecordSeries) then begin idx := baseType.Definition.IndexOf(Node.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, Node.Member, resType); end; function TTypeChecker.VisitRecordLiteral(const Node: IRecordLiteralNode): IAstNode; var i: Integer; newFields: TArray; fieldTypes: TArray>; scalarFieldTypes: TArray>; isScalar: Boolean; valType: IStaticType; key: IKeyword; visitedValue: IAstNode; newFieldList: IRecordFieldList; begin SetLength(newFields, Node.Fields.Count); SetLength(fieldTypes, Node.Fields.Count); SetLength(scalarFieldTypes, Node.Fields.Count); isScalar := True; for i := 0 to Node.Fields.Count - 1 do begin var oldField := Node.Fields[i]; visitedValue := Accept(oldField.Value); // Keys are guaranteed to be keywords by parser key := oldField.Key.Value; // Recreate the field node with the typed value newFields[i] := TAst.RecordField(oldField.Identity, oldField.Key, visitedValue); // Analyze Type valType := visitedValue.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; // Map StaticType Kind to Scalar Kind 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; // Should not happen given check above 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 (Length(newFields) > 0) then begin scalarDef := TKeywordMappingRegistry.Intern(scalarFieldTypes); resultType := TTypes.CreateRecord(scalarDef); end else begin genericDef := TGenericRecordRegistry.Intern(fieldTypes); resultType := TTypes.CreateGenericRecord(genericDef); end; newFieldList := TRecordFieldList.Create(newFields, Node.Fields.Identity); Result := TAst.RecordLiteral(Node.Identity, newFieldList, scalarDef, genericDef, resultType); end; function TTypeChecker.VisitCreateSeries(const Node: ICreateSeriesNode): IAstNode; var elemType: IStaticType; def: string; begin def := Node.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: ISeriesLengthNode): IAstNode; begin Result := TAst.SeriesLength(Node.Identity, Accept(Node.Series).AsIdentifier, TTypes.Ordinal); end; function TTypeChecker.VisitNop(const Node: INopNode): IAstNode; begin Result := TAst.Nop(Node.Identity, TTypes.Void); end; // ============================================================================= // PIPE IMPLEMENTATION (Type Checking) // ============================================================================= function TTypeChecker.VisitPipeInput(const Node: IPipeInputNode): IAstNode; var newSource: IIdentifierNode; sourceType: IStaticType; newSelectors: TArray; i: Integer; begin newSource := Accept(Node.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 Node.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; // Reconstruct list simply to pass through SetLength(newSelectors, Node.Selectors.Count); for i := 0 to Node.Selectors.Count - 1 do newSelectors[i] := Node.Selectors[i]; Result := TAst.PipeInput(newSource, TAst.PipeSelectorList(newSelectors, Node.Selectors.Identity.Location), Node.Identity.Location); end; function TTypeChecker.VisitPipe(const Node: IPipeNode): IAstNode; var i, k: Integer; inputNode: IPipeInputNode; newInputs: TArray; streamType: IStaticType; paramTypes: TList; lambda: ILambdaExpressionNode; newParams: TArray; inferredType: IStaticType; begin SetLength(newInputs, Node.Inputs.Count); paramTypes := TList.Create; try // 1. Visit Inputs and collect types for Lambda parameters for i := 0 to Node.Inputs.Count - 1 do begin inputNode := Accept(Node.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 := Node.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, Node.Inputs.Identity), typedLambda, pipeType); finally paramTypes.Free; end; end; end.