334 lines
11 KiB
ObjectPascal
334 lines
11 KiB
ObjectPascal
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<Boolean>)
|
|
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;
|
|
|
|
// --- Elements ---
|
|
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;
|
|
|
|
// Unification: Tuple (for [vectors], arguments, parameters, fields)
|
|
function VisitTuple(const Node: IAstNode): Boolean;
|
|
|
|
// Pipe Support
|
|
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);
|
|
|
|
// Elements
|
|
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);
|
|
|
|
// Unified List Type
|
|
Register(akTuple, VisitTuple);
|
|
|
|
// Pipes
|
|
Register(akPipe, VisitPipe);
|
|
end;
|
|
|
|
// --- List / Container Visitors ---
|
|
|
|
function TPurityAnalyzer.VisitTuple(const Node: IAstNode): Boolean;
|
|
var
|
|
i: Integer;
|
|
T: ITupleNode;
|
|
begin
|
|
T := Node.AsTuple;
|
|
// A tuple is pure if ALL its elements are pure.
|
|
for i := 0 to T.Count - 1 do
|
|
begin
|
|
if not IsNodePure(T.Items[i]) then
|
|
exit(False);
|
|
end;
|
|
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).
|
|
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 (Tuple) 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 Expression Tuple
|
|
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 Fields Tuple
|
|
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.VisitPipe(const Node: IAstNode): Boolean;
|
|
begin
|
|
var P := Node.AsPipe;
|
|
// A pipe is pure if its inputs are pure AND its transformation lambda is pure.
|
|
// P.Inputs is an ITupleNode (recursive Tuple of Tuples), so VisitTuple handles it.
|
|
Result := IsNodePure(P.Inputs) and IsNodePure(P.Transformation);
|
|
end;
|
|
|
|
end.
|