unit Myc.Ast.Analysis.Purity; interface uses System.SysUtils, Myc.Data.Value, Myc.Ast.Nodes, Myc.Ast.Visitor, Myc.Ast.Scope, Myc.Ast; type // Analyzes an AST to determine if it is referentially transparent and free of side effects. TPurityAnalyzer = class(TAstVisitor) strict private // --- Safe Leaves / Constructs --- function VisitConstant(const Node: IAstNode): Boolean; function VisitKeyword(const Node: IAstNode): Boolean; function VisitIfExpression(const Node: IAstNode): Boolean; function VisitCondExpression(const Node: IAstNode): Boolean; function VisitBlockExpression(const Node: IAstNode): Boolean; function VisitRecordLiteral(const Node: IAstNode): Boolean; function VisitVariableDeclaration(const Node: IAstNode): Boolean; function VisitSeriesLength(const Node: IAstNode): Boolean; // --- List Visitors (Aggregation Logic: All must be pure) --- function VisitParameterList(const Node: IAstNode): Boolean; function VisitArgumentList(const Node: IAstNode): Boolean; function VisitExpressionList(const Node: IAstNode): Boolean; function VisitRecordFieldList(const Node: IAstNode): Boolean; function VisitRecordField(const Node: IAstNode): Boolean; // --- Critical Checks --- function VisitIdentifier(const Node: IAstNode): Boolean; function VisitFunctionCall(const Node: IAstNode): Boolean; function VisitRecurNode(const Node: IAstNode): Boolean; // --- Forbidden Constructs (Side Effects / Unsafe) --- function VisitAssignment(const Node: IAstNode): Boolean; function VisitAddSeriesItem(const Node: IAstNode): Boolean; // Allocation is considered pure in this context function VisitCreateSeries(const Node: IAstNode): Boolean; function VisitIndexer(const Node: IAstNode): Boolean; function VisitMemberAccess(const Node: IAstNode): Boolean; // Ignored / Irrelevant for Runtime Purity function VisitLambdaExpression(const Node: IAstNode): Boolean; function VisitMacroDefinition(const Node: IAstNode): Boolean; function VisitQuasiquote(const Node: IAstNode): Boolean; function VisitUnquote(const Node: IAstNode): Boolean; function VisitUnquoteSplicing(const Node: IAstNode): Boolean; function VisitMacroExpansionNode(const Node: IAstNode): Boolean; function VisitNop(const Node: IAstNode): Boolean; // Pipe Support (Structural Check) function VisitPipeInput(const Node: IAstNode): Boolean; function VisitPipeSelectorList(const Node: IAstNode): Boolean; function VisitPipeInputList(const Node: IAstNode): Boolean; function VisitPipe(const Node: IAstNode): Boolean; // Helper to check if a node is pure (handling nil gracefully) function IsNodePure(const Node: IAstNode): Boolean; protected procedure SetupHandlers; override; public class function IsPure(const RootNode: IAstNode): Boolean; end; implementation { TPurityAnalyzer } class function TPurityAnalyzer.IsPure(const RootNode: IAstNode): Boolean; begin var analyzer := TPurityAnalyzer.Create; try // Use the visitor's central dispatch method Result := analyzer.Visit(RootNode); finally analyzer.Free; end; end; function TPurityAnalyzer.IsNodePure(const Node: IAstNode): Boolean; begin // If a node is nil (e.g. optional else-branch), it's "nothing", which is pure. if not Assigned(Node) then exit(True); // Dispatch to registry Result := Visit(Node); end; procedure TPurityAnalyzer.SetupHandlers; begin // Core Register(akConstant, VisitConstant); Register(akIdentifier, VisitIdentifier); Register(akKeyword, VisitKeyword); // Lists Register(akParameterList, VisitParameterList); Register(akArgumentList, VisitArgumentList); Register(akExpressionList, VisitExpressionList); Register(akRecordFieldList, VisitRecordFieldList); Register(akRecordField, VisitRecordField); // Structural Register(akIfExpression, VisitIfExpression); Register(akCondExpression, VisitCondExpression); Register(akLambdaExpression, VisitLambdaExpression); Register(akFunctionCall, VisitFunctionCall); Register(akMacroExpansion, VisitMacroExpansionNode); Register(akBlockExpression, VisitBlockExpression); Register(akVariableDeclaration, VisitVariableDeclaration); Register(akAssignment, VisitAssignment); Register(akMacroDefinition, VisitMacroDefinition); Register(akQuasiquote, VisitQuasiquote); Register(akUnquote, VisitUnquote); Register(akUnquoteSplicing, VisitUnquoteSplicing); Register(akIndexer, VisitIndexer); Register(akMemberAccess, VisitMemberAccess); Register(akRecordLiteral, VisitRecordLiteral); Register(akCreateSeries, VisitCreateSeries); Register(akAddSeriesItem, VisitAddSeriesItem); Register(akSeriesLength, VisitSeriesLength); Register(akRecur, VisitRecurNode); Register(akNop, VisitNop); // Pipes Register(akPipeInput, VisitPipeInput); Register(akPipeSelectorList, VisitPipeSelectorList); Register(akPipeInputList, VisitPipeInputList); Register(akPipe, VisitPipe); end; // --- List Visitors --- function TPurityAnalyzer.VisitParameterList(const Node: IAstNode): Boolean; begin // Declarations are pure Result := True; end; function TPurityAnalyzer.VisitArgumentList(const Node: IAstNode): Boolean; begin for var item in Node.AsArgumentList do if not IsNodePure(item) then exit(False); Result := True; end; function TPurityAnalyzer.VisitExpressionList(const Node: IAstNode): Boolean; begin for var item in Node.AsExpressionList do if not IsNodePure(item) then exit(False); Result := True; end; function TPurityAnalyzer.VisitRecordFieldList(const Node: IAstNode): Boolean; begin for var item in Node.AsRecordFieldList do if not IsNodePure(item) then exit(False); Result := True; end; function TPurityAnalyzer.VisitRecordField(const Node: IAstNode): Boolean; begin // Key is usually pure (Keyword), check Value var F := Node.AsRecordField; Result := IsNodePure(F.Key) and IsNodePure(F.Value); end; // --- Safe Leaves --- function TPurityAnalyzer.VisitConstant(const Node: IAstNode): Boolean; begin Result := True; end; function TPurityAnalyzer.VisitKeyword(const Node: IAstNode): Boolean; begin Result := True; end; function TPurityAnalyzer.VisitNop(const Node: IAstNode): Boolean; begin Result := True; end; // --- Identifier: Only local variables are safe --- function TPurityAnalyzer.VisitIdentifier(const Node: IAstNode): Boolean; begin // We only allow access to local variables (ScopeDepth = 0). // Accessing Parent/Upvalues (ScopeDepth > 0) makes the function state-dependent (closure state), // effectively impure regarding referential transparency across different closure instances, // unless we could prove the upvalue is constant (which we don't track yet). // Note: Parameters are also ScopeDepth=0 in the Binder logic. var I := Node.AsIdentifier; Result := (I.Address.Kind = akLocalOrParent) and (I.Address.ScopeDepth = 0); end; // --- Function Call: The Core Logic --- function TPurityAnalyzer.VisitFunctionCall(const Node: IAstNode): Boolean; begin var C := Node.AsFunctionCall; // 1. The target function MUST be marked as Pure (from RTL or previous inference). if not C.IsTargetPure then exit(False); // 2. All arguments must be pure expressions. Result := IsNodePure(C.Arguments); end; // --- Recursion --- function TPurityAnalyzer.VisitRecurNode(const Node: IAstNode): Boolean; begin // 'recur' is just control flow. It is pure if its arguments are pure. Result := IsNodePure(Node.AsRecur.Arguments); end; // --- Structures: Recursive Checks --- function TPurityAnalyzer.VisitIfExpression(const Node: IAstNode): Boolean; begin var E := Node.AsIfExpression; Result := IsNodePure(E.Condition) and IsNodePure(E.ThenBranch) and IsNodePure(E.ElseBranch); end; function TPurityAnalyzer.VisitCondExpression(const Node: IAstNode): Boolean; begin var E := Node.AsCondExpression; // All conditions and all branches must be pure for var pair in E.Pairs do begin if not (IsNodePure(pair.Condition) and IsNodePure(pair.Branch)) then Exit(False); end; // And the Else branch Result := IsNodePure(E.ElseBranch); end; function TPurityAnalyzer.VisitBlockExpression(const Node: IAstNode): Boolean; begin // Delegate to ExpressionList Result := IsNodePure(Node.AsBlockExpression.Expressions); end; function TPurityAnalyzer.VisitVariableDeclaration(const Node: IAstNode): Boolean; begin // 'def x = ...' is locally pure if the initializer is pure. // It mutates the local scope (stack), but that is contained within the function execution. Result := IsNodePure(Node.AsVariableDeclaration.Initializer); end; function TPurityAnalyzer.VisitRecordLiteral(const Node: IAstNode): Boolean; begin // Delegate to RecordFieldList Result := IsNodePure(Node.AsRecordLiteral.Fields); end; function TPurityAnalyzer.VisitIndexer(const Node: IAstNode): Boolean; begin // Reading from a structure is pure if the indices/base are pure. var I := Node.AsIndexer; Result := IsNodePure(I.Base) and IsNodePure(I.Index); end; function TPurityAnalyzer.VisitMemberAccess(const Node: IAstNode): Boolean; begin Result := IsNodePure(Node.AsMemberAccess.Base); end; function TPurityAnalyzer.VisitSeriesLength(const Node: IAstNode): Boolean; begin // Querying length is pure. // The series identifier check happens in VisitIdentifier. Result := True; end; // --- Forbidden (Impure) --- function TPurityAnalyzer.VisitAssignment(const Node: IAstNode): Boolean; begin // Mutation of variables is defined as impure. Result := False; end; function TPurityAnalyzer.VisitAddSeriesItem(const Node: IAstNode): Boolean; begin // Mutation of a series (side effect). Result := False; end; function TPurityAnalyzer.VisitCreateSeries(const Node: IAstNode): Boolean; begin // Creating a NEW object is considered pure in this context, // as it does not mutate existing global state. Result := True; end; // --- Irrelevant / Nested (Definitions are pure, execution logic checked separately) --- function TPurityAnalyzer.VisitLambdaExpression(const Node: IAstNode): Boolean; begin // Defining a function is a pure operation. // Whether the function itself is pure when executed is determined when *that* function is compiled. Result := True; end; function TPurityAnalyzer.VisitMacroDefinition(const Node: IAstNode): Boolean; begin Result := True; // Compile-time construct end; function TPurityAnalyzer.VisitQuasiquote(const Node: IAstNode): Boolean; begin Result := True; // Structural construction end; function TPurityAnalyzer.VisitUnquote(const Node: IAstNode): Boolean; begin Result := IsNodePure(Node.AsUnquote.Expression); end; function TPurityAnalyzer.VisitUnquoteSplicing(const Node: IAstNode): Boolean; begin Result := IsNodePure(Node.AsUnquoteSplicing.Expression); end; function TPurityAnalyzer.VisitMacroExpansionNode(const Node: IAstNode): Boolean; begin // We analyze the already expanded body. Result := IsNodePure(Node.AsMacroExpansion.ExpandedBody); end; // --- Pipe Support --- function TPurityAnalyzer.VisitPipeInput(const Node: IAstNode): Boolean; begin var P := Node.AsPipeInput; // Input node itself is declarative. // But we check its parts just in case. Result := IsNodePure(P.StreamSource) and IsNodePure(P.Selectors); end; function TPurityAnalyzer.VisitPipeSelectorList(const Node: IAstNode): Boolean; begin for var item in Node.AsPipeSelectorList do if not IsNodePure(item) then exit(False); Result := True; end; function TPurityAnalyzer.VisitPipeInputList(const Node: IAstNode): Boolean; begin for var item in Node.AsPipeInputList do if not IsNodePure(item) then exit(False); Result := True; end; function TPurityAnalyzer.VisitPipe(const Node: IAstNode): Boolean; begin var P := Node.AsPipe; // A pipe is pure if its input definitions are pure AND its transformation lambda is pure. Result := IsNodePure(P.Inputs) and IsNodePure(P.Transformation); end; end.