Files
MycLib/Src/AST/Myc.Ast.Analysis.Purity.pas
T
2025-11-22 14:49:24 +01:00

251 lines
8.8 KiB
ObjectPascal

unit Myc.Ast.Analysis.Purity;
interface
uses
System.SysUtils,
Myc.Data.Value,
Myc.Ast.Nodes,
Myc.Ast.Visitor,
Myc.Ast.Scope;
type
/// <summary>
/// Analyzes an AST to determine if it is referentially transparent and free of side effects.
/// </summary>
TPurityAnalyzer = class(TAstVisitor<Boolean>)
protected
// Default behavior: Visit children. If all children return True, then True.
// However, we must explicitly define what is allowed.
function Accept(const Node: IAstNode): Boolean; override;
// --- Safe Leaves / Constructs ---
function VisitConstant(const Node: IConstantNode): Boolean; override;
function VisitKeyword(const Node: IKeywordNode): Boolean; override;
function VisitIfExpression(const Node: IIfExpressionNode): Boolean; override;
function VisitTernaryExpression(const Node: ITernaryExpressionNode): Boolean; override;
function VisitBlockExpression(const Node: IBlockExpressionNode): Boolean; override;
function VisitRecordLiteral(const Node: IRecordLiteralNode): Boolean; override;
function VisitVariableDeclaration(const Node: IVariableDeclarationNode): Boolean; override;
function VisitSeriesLength(const Node: ISeriesLengthNode): Boolean; override;
// --- Critical Checks ---
function VisitIdentifier(const Node: IIdentifierNode): Boolean; override;
function VisitFunctionCall(const Node: IFunctionCallNode): Boolean; override;
function VisitRecurNode(const Node: IRecurNode): Boolean; override;
// --- Forbidden Constructs (Side Effects / Unsafe) ---
function VisitAssignment(const Node: IAssignmentNode): Boolean; override;
function VisitAddSeriesItem(const Node: IAddSeriesItemNode): Boolean; override;
// Allocation is considered pure in this context (it creates a new value, doesn't mutate existing world)
function VisitCreateSeries(const Node: ICreateSeriesNode): Boolean; override;
function VisitIndexer(const Node: IIndexerNode): Boolean; override;
function VisitMemberAccess(const Node: IMemberAccessNode): Boolean; override;
// Ignored / Irrelevant for Runtime Purity (Compile-time constructs or Definitions)
function VisitLambdaExpression(const Node: ILambdaExpressionNode): Boolean; override;
function VisitMacroDefinition(const Node: IMacroDefinitionNode): Boolean; override;
function VisitQuasiquote(const Node: IQuasiquoteNode): Boolean; override;
function VisitUnquote(const Node: IUnquoteNode): Boolean; override;
function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): Boolean; override;
function VisitMacroExpansionNode(const Node: IMacroExpansionNode): Boolean; override;
function VisitNop(const Node: INopNode): Boolean; 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
Result := analyzer.Accept(RootNode);
finally
analyzer.Free;
end;
end;
function TPurityAnalyzer.Accept(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 specific Visit method via generic base
Result := Node.Accept(Self).AsGeneric<Boolean>;
end;
// --- Safe Leaves ---
function TPurityAnalyzer.VisitConstant(const Node: IConstantNode): Boolean;
begin
Result := True;
end;
function TPurityAnalyzer.VisitKeyword(const Node: IKeywordNode): Boolean;
begin
Result := True;
end;
function TPurityAnalyzer.VisitNop(const Node: INopNode): Boolean;
begin
Result := True;
end;
// --- Identifier: Only local variables are safe ---
function TPurityAnalyzer.VisitIdentifier(const Node: IIdentifierNode): 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.
Result := (Node.Address.Kind = akLocalOrParent) and (Node.Address.ScopeDepth = 0);
end;
// --- Function Call: The Core Logic ---
function TPurityAnalyzer.VisitFunctionCall(const Node: IFunctionCallNode): Boolean;
begin
// 1. The target function MUST be marked as Pure (from RTL or previous inference).
if not Node.IsTargetPure then
exit(False);
// 2. All arguments must be pure expressions.
for var arg in Node.Arguments do
if not Accept(arg) then
exit(False);
Result := True;
end;
// --- Recursion ---
function TPurityAnalyzer.VisitRecurNode(const Node: IRecurNode): Boolean;
begin
// 'recur' is just control flow. It is pure if its arguments are pure.
for var arg in Node.Arguments do
if not Accept(arg) then
exit(False);
Result := True;
end;
// --- Structures: Recursive Checks ---
function TPurityAnalyzer.VisitIfExpression(const Node: IIfExpressionNode): Boolean;
begin
Result := Accept(Node.Condition) and Accept(Node.ThenBranch) and Accept(Node.ElseBranch);
end;
function TPurityAnalyzer.VisitTernaryExpression(const Node: ITernaryExpressionNode): Boolean;
begin
Result := Accept(Node.Condition) and Accept(Node.ThenBranch) and Accept(Node.ElseBranch);
end;
function TPurityAnalyzer.VisitBlockExpression(const Node: IBlockExpressionNode): Boolean;
begin
for var expr in Node.Expressions do
if not Accept(expr) then
exit(False);
Result := True;
end;
function TPurityAnalyzer.VisitVariableDeclaration(const Node: IVariableDeclarationNode): 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 := Accept(Node.Initializer);
end;
function TPurityAnalyzer.VisitRecordLiteral(const Node: IRecordLiteralNode): Boolean;
begin
for var field in Node.Fields do
if not Accept(field.Value) then
exit(False);
Result := True;
end;
function TPurityAnalyzer.VisitIndexer(const Node: IIndexerNode): Boolean;
begin
// Reading from a structure is pure if the indices/base are pure.
Result := Accept(Node.Base) and Accept(Node.Index);
end;
function TPurityAnalyzer.VisitMemberAccess(const Node: IMemberAccessNode): Boolean;
begin
Result := Accept(Node.Base);
end;
function TPurityAnalyzer.VisitSeriesLength(const Node: ISeriesLengthNode): Boolean;
begin
// Querying length is pure.
// The series identifier check happens in VisitIdentifier.
Result := True;
end;
// --- Forbidden (Impure) ---
function TPurityAnalyzer.VisitAssignment(const Node: IAssignmentNode): Boolean;
begin
// Mutation of variables is defined as impure.
Result := False;
end;
function TPurityAnalyzer.VisitAddSeriesItem(const Node: IAddSeriesItemNode): Boolean;
begin
// Mutation of a series (side effect).
Result := False;
end;
function TPurityAnalyzer.VisitCreateSeries(const Node: ICreateSeriesNode): 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: ILambdaExpressionNode): 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: IMacroDefinitionNode): Boolean;
begin
Result := True; // Compile-time construct
end;
function TPurityAnalyzer.VisitQuasiquote(const Node: IQuasiquoteNode): Boolean;
begin
Result := True; // Structural construction
end;
function TPurityAnalyzer.VisitUnquote(const Node: IUnquoteNode): Boolean;
begin
Result := Accept(Node.Expression);
end;
function TPurityAnalyzer.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): Boolean;
begin
Result := Accept(Node.Expression);
end;
function TPurityAnalyzer.VisitMacroExpansionNode(const Node: IMacroExpansionNode): Boolean;
begin
// We analyze the already expanded body.
Result := Accept(Node.ExpandedBody);
end;
end.