Files
MycLib/Src/AST/Myc.Ast.Analysis.Purity.pas
T
Michael Schimmel 22674b962b Generic Visitors
2026-01-03 19:14:18 +01:00

382 lines
13 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;
// --- 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.