AST function purity analysis

This commit is contained in:
Michael Schimmel
2025-11-22 14:49:24 +01:00
parent 61b6a1742b
commit 240f794211
17 changed files with 429 additions and 68 deletions
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -26,7 +26,8 @@ uses
Myc.Ast.Compiler.Macros in '..\Src\AST\Myc.Ast.Compiler.Macros.pas',
Myc.Ast.Compiler.Specializer in '..\Src\AST\Myc.Ast.Compiler.Specializer.pas',
Myc.Ast.Environment in '..\Src\AST\Myc.Ast.Environment.pas',
Myc.Ast.Debugger in '..\Src\AST\Myc.Ast.Debugger.pas';
Myc.Ast.Debugger in '..\Src\AST\Myc.Ast.Debugger.pas',
Myc.Ast.Analysis.Purity in '..\Src\AST\Myc.Ast.Analysis.Purity.pas';
{$R *.res}
+7
View File
@@ -157,6 +157,7 @@
<DCCReference Include="..\Src\AST\Myc.Ast.Compiler.Specializer.pas"/>
<DCCReference Include="..\Src\AST\Myc.Ast.Environment.pas"/>
<DCCReference Include="..\Src\AST\Myc.Ast.Debugger.pas"/>
<DCCReference Include="..\Src\AST\Myc.Ast.Analysis.Purity.pas"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
@@ -212,6 +213,12 @@
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="Win64\Debug\ASTPlayground.rsm" Configuration="Debug" Class="DebugSymbols">
<Platform Name="Win64">
<RemoteName>ASTPlayground.rsm</RemoteName>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="Win64\Release\ASTPlayground.exe" Configuration="Release" Class="ProjectOutput">
<Platform Name="Win64">
<RemoteName>ASTPlayground.exe</RemoteName>
+1 -1
View File
@@ -197,7 +197,7 @@ object Form1: TForm1
'Expanded'
'Bound'
'Specialized')
ItemIndex = 0
ItemIndex = 3
Position.X = 672.000000000000000000
Position.Y = 576.000000000000000000
Size.Width = 104.000000000000000000
+16 -8
View File
@@ -110,7 +110,7 @@ type
procedure RTLListViewChange(Sender: TObject);
private
FCurrUnboundAst: IAstNode;
FCurrExec: TDataValue.TFunc;
FCurrExec: TCompiledFunction;
FEnvironment: TAstEnvironment;
FWorkspace: TAuraWorkspace;
FScriptUpdate: Boolean;
@@ -262,7 +262,7 @@ end;
function TForm1.ExecuteAst(const ANode: IAstNode): TDataValue;
begin
FCurrUnboundAst := ANode;
FCurrExec := nil; // Clear previous
FCurrExec := Default(TCompiledFunction);
// 1. Set strategy based on UI
if DebugBox.IsChecked then
@@ -273,12 +273,21 @@ begin
try
// Wrap in Lambda to compile
var funcDef := TAst.LambdaExpr([], ANode);
FCurrExec := FEnvironment.Compile(funcDef).Func;
Result := FCurrExec([]);
FCurrExec := FEnvironment.Compile(funcDef);
if Assigned(FCurrExec.Func) then
begin
Memo1.Lines.Add(
Format('Compiled. Signature: %s, IsPure=%s', [FCurrExec.StaticType.ToString, BoolToStr(FCurrExec.IsPure, true)])
);
end;
Result := FCurrExec.Func([]);
except
on E: Exception do
begin
FCurrExec.Func := nil;
Memo1.Lines.Add('--- ERROR ---');
Memo1.Lines.Add(E.ClassName + ': ' + E.Message);
Result := TDataValue.Void;
@@ -645,7 +654,7 @@ begin
FCurrUnboundAst := converter.Deserialize(jsonObj);
// Run the full pipeline via environment
FCurrExec := FEnvironment.Compile(FCurrUnboundAst).Func;
FCurrExec := FEnvironment.Compile(FCurrUnboundAst);
Memo1.Lines.Add('AST deserialized and bound successfully from JSON.');
Memo1.Lines.Add('You can now visualize it (Middle Mouse Click) or pretty-print it.');
@@ -655,7 +664,6 @@ begin
except
on E: Exception do
begin
FCurrExec := nil;
Memo1.Lines.Add('Error deserializing AST from JSON:');
Memo1.Lines.Add(E.Message);
Memo1.Lines.Add('--- Original JSON ---');
@@ -675,7 +683,7 @@ var
begin
Memo1.Lines.Clear;
if not Assigned(FCurrExec) then
if not Assigned(FCurrExec.Func) then
begin
Memo1.Lines.Add('No AST available to serialize. Please generate one first.');
exit;
@@ -703,7 +711,7 @@ begin
Memo1.Lines.Clear;
Memo1.Lines.Add('--- AST Dump ---');
if not Assigned(FCurrExec) then
if not Assigned(FCurrExec.Func) then
begin
Memo1.Lines.Add('No *compiled* AST has been generated yet. Click a test button first.');
exit;
+250
View File
@@ -0,0 +1,250 @@
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.
+1 -1
View File
@@ -368,7 +368,7 @@ begin
// If FLambdaCounter increased by more than just 1 (myself), children were visited.
hasNested := FLambdaCounter > (startCount + 1);
Result := TAst.LambdaExpr(newParams, newBody, finalLayout, nil, upvaluesList, hasNested);
Result := TAst.LambdaExpr(newParams, newBody, finalLayout, nil, upvaluesList, hasNested, Node.IsPure);
end;
function TAstBinder.VisitFunctionCall(const Node: IFunctionCallNode): IAstNode;
+1 -1
View File
@@ -219,7 +219,7 @@ begin
newBody := Accept(Node.Body);
Result := TAst.LambdaExpr(newParams, newBody, nil, nil, nil, False, TTypes.Unknown);
Result := TAst.LambdaExpr(newParams, newBody, nil, nil, nil, False, Node.IsPure, TTypes.Unknown);
end;
function TExpansionVisitor.VisitUnquote(const Node: IUnquoteNode): IAstNode;
+26 -7
View File
@@ -22,6 +22,7 @@ type
// This transformer runs *after* TypeChecker.
// It specializes all statically resolvable function calls (RTL and user-defined)
// by replacing them with nodes that have a direct StaticTarget.
// It propagates Purity information but does NOT perform Constant Folding yet.
TStaticSpecializer = class(TAstTransformer, IAstSpecializer)
private
FEnvironment: IEnvironment;
@@ -35,7 +36,6 @@ type
constructor Create(const AEnvironment: IEnvironment);
function Execute(const RootNode: IAstNode): IAstNode;
// Descriptor removed from signature as it is not needed for specialization
class function Specialize(const AEnvironment: IEnvironment; const RootNode: IAstNode): IAstNode; static;
end;
@@ -128,7 +128,16 @@ begin
if FEnvironment.MonomorphCache.TryGetValue(key, specializedMethod) then
begin
// 4a. Cache Hit (Environment)
Result := TAst.FunctionCall(newCallee, newArgs, specializedMethod.ReturnType, Node.IsTailCall, specializedMethod.Target);
// Propagate IsPure flag from cache to AST node
Result :=
TAst.FunctionCall(
newCallee,
newArgs,
specializedMethod.ReturnType,
Node.IsTailCall,
specializedMethod.Target,
specializedMethod.IsPure
);
exit;
end;
@@ -139,7 +148,16 @@ begin
// 5a. Cache Hit (RTL)
FEnvironment.MonomorphCache.Add(key, specializedMethod);
Result := TAst.FunctionCall(newCallee, newArgs, specializedMethod.ReturnType, Node.IsTailCall, specializedMethod.Target);
// Propagate IsPure flag from RTL definition to AST node
Result :=
TAst.FunctionCall(
newCallee,
newArgs,
specializedMethod.ReturnType,
Node.IsTailCall,
specializedMethod.Target,
specializedMethod.IsPure
);
exit;
end;
@@ -148,7 +166,7 @@ begin
if (funcDef <> nil) then
begin
// Cannot specialize closures safely without more complex analysis
// Cannot specialize closures safely without more complex analysis if they have state
if funcDef.Kind = akLambdaExpression then
begin
var lambdaDef := funcDef.AsLambdaExpression;
@@ -160,17 +178,18 @@ begin
end;
// 6a. Compile func with KNOWN TYPES
// This recursively triggers Bind -> Check -> Specialize for the callee body!
// This recursively triggers Bind -> Check -> Specialize -> Purity Inference for the callee body!
var compiled := FEnvironment.Compile(funcDef, argTypes);
// 6b. Store in cache
// Get the return type from the *full function type* returned by Compile
var returnType := compiled.StaticType.Signatures[0].ReturnType;
specializedMethod := TSpecializedMethod.Create(compiled.Func, returnType);
specializedMethod := TSpecializedMethod.Create(compiled.Func, returnType, compiled.IsPure);
FEnvironment.MonomorphCache.Add(key, specializedMethod);
// 6c. Return the new node
Result := TAst.FunctionCall(newCallee, newArgs, returnType, Node.IsTailCall, compiled.Func);
// Propagate the inferred IsPure flag to the AST node
Result := TAst.FunctionCall(newCallee, newArgs, returnType, Node.IsTailCall, compiled.Func, compiled.IsPure);
exit;
end;
+14 -2
View File
@@ -180,7 +180,18 @@ begin
begin
// Rebuild using factory.
// IMPORTANT: Pass Layout AND Descriptor (TCO runs after TypeChecker).
Result := TAst.LambdaExpr(newParams, newBody, Node.Layout, Node.Descriptor, Node.Upvalues, Node.HasNestedLambdas, Node.StaticType);
// (* FIX: Pass Node.IsPure to preserve it *)
Result :=
TAst.LambdaExpr(
newParams,
newBody,
Node.Layout,
Node.Descriptor,
Node.Upvalues,
Node.HasNestedLambdas,
Node.IsPure,
Node.StaticType
);
end;
end;
@@ -242,7 +253,8 @@ begin
newArgs,
Node.StaticType,
isTailCall,
Node.StaticTarget // Preserve the static target from Specializer
Node.StaticTarget, // Preserve the static target from Specializer
Node.IsTargetPure // (* FIX: Preserve the purity flag from Specializer! *)
);
end;
+2 -1
View File
@@ -400,7 +400,8 @@ begin
end;
// 9. Create new node with the Descriptor attached!
Result := TAst.LambdaExpr(newParams, newBody, Node.Layout, finalDescriptor, Node.Upvalues, Node.HasNestedLambdas, methodType);
Result :=
TAst.LambdaExpr(newParams, newBody, Node.Layout, finalDescriptor, Node.Upvalues, Node.HasNestedLambdas, Node.IsPure, methodType);
end;
function TTypeChecker.VisitFunctionCall(const Node: IFunctionCallNode): IAstNode;
+12 -2
View File
@@ -201,7 +201,12 @@ var
slot: Integer;
typ: IStaticType;
begin
LogFmt('LambdaExpression (HasNested: %s)', [Node.HasNestedLambdas.ToString(TUseBoolStrs.True)], Node);
// (* UPDATED: Added IsPure output *)
LogFmt(
'LambdaExpression (HasNested: %s, IsPure: %s)',
[Node.HasNestedLambdas.ToString(TUseBoolStrs.True), BoolToStr(Node.IsPure, True)],
Node
);
Indent;
// 1. Layout & Symbols
@@ -277,7 +282,12 @@ begin
else
staticStatus := 'nil';
LogFmt('FunctionCall (IsTailCall: %s, StaticTarget: %s%s)', [Node.IsTailCall.ToString(TUseBoolStrs.True), staticStatus, sigStr], Node);
// (* UPDATED: Added IsTargetPure output *)
LogFmt(
'FunctionCall (IsTailCall: %s, StaticTarget: %s%s, IsTargetPure: %s)',
[Node.IsTailCall.ToString(TUseBoolStrs.True), staticStatus, sigStr, BoolToStr(Node.IsTargetPure, True)],
Node
);
Indent;
Log('Callee:');
+24 -12
View File
@@ -41,7 +41,8 @@ type
public
Func: TDataValue.TFunc;
StaticType: IStaticType;
constructor Create(const AFunc: TDataValue.TFunc; const AStaticType: IStaticType);
IsPure: Boolean;
constructor Create(const AFunc: TDataValue.TFunc; const AStaticType: IStaticType; AIsPure: Boolean);
end;
IExecutionStrategy = interface
@@ -143,7 +144,8 @@ uses
Myc.Ast.Compiler.Binder,
Myc.Ast.Compiler.TypeChecker,
Myc.Ast.Compiler.Specializer,
Myc.Ast.Compiler.TCO;
Myc.Ast.Compiler.TCO,
Myc.Ast.Analysis.Purity;
{ TMonoCacheKey }
@@ -202,10 +204,11 @@ end;
{ TCompiledFunction }
constructor TCompiledFunction.Create(const AFunc: TDataValue.TFunc; const AStaticType: IStaticType);
constructor TCompiledFunction.Create(const AFunc: TDataValue.TFunc; const AStaticType: IStaticType; AIsPure: Boolean);
begin
Func := AFunc;
StaticType := AStaticType;
IsPure := AIsPure;
end;
type
@@ -344,6 +347,8 @@ end;
procedure TAstEnvironment.Define(const Name: String; const AScript: IAstNode);
begin
var compiled := Compile(AScript);
// Note: Define now potentially handles Pure flags internally via the Node metadata,
// but the runtime value is just the TDataValue.
RootScope.Define(Name, compiled.Func([]), compiled.StaticType);
end;
@@ -526,6 +531,7 @@ var
typedNode: IAstNode;
specialized: IAstNode;
tcoOptimized: IAstNode;
isPure: Boolean;
begin
// 0. Wrap in Lambda to create an isolatable compilation unit (scope)
var prg := TAst.LambdaExpr(Params, Node);
@@ -549,25 +555,27 @@ begin
// 4. Optimize (TCO)
tcoOptimized := TAstTCO.Optimize(specialized);
// 5. Create Evaluator
// We create a scope based on the descriptor to run the lambda body.
// The lambda itself (as a node) is evaluated to produce a closure.
// Since we wrapped the user code in a LambdaExpr, evaluating 'tcoOptimized'
// returns the closure (TFunc).
// 5. Purity Inference
// Check the body of the optimized lambda for purity.
// Note: tcoOptimized is a LambdaExpressionNode. Its body is what we check.
isPure := TPurityAnalyzer.IsPure(tcoOptimized.AsLambdaExpression.Body);
// 6. Create Evaluator
var visitor := FExecutionStrategy.CreateVisitor(descriptor.CreateScope(FRootScope));
// Execute the AST to get the TFunc (Closure)
var closure := tcoOptimized.Accept(visitor);
var closure := tcoOptimized.Accept(visitor).AsMethod();
// Wrap TCO handling for the final result
finalFunc :=
function(const Args: TArray<TDataValue>): TDataValue
begin
Result := closure.AsMethod()(Args);
Result := closure(Args);
TEvaluatorVisitor.HandleTCO(Result);
end;
Result := TCompiledFunction.Create(finalFunc, funcType);
// Pass IsPure flag to the compiled function record
Result := TCompiledFunction.Create(finalFunc, funcType, isPure);
end;
function TEnvironment.Compile(const Node: IFunctionDefinition; const ArgTypes: TArray<IStaticType>): TCompiledFunction;
@@ -578,6 +586,7 @@ var
finalFunc: TDataValue.TFunc;
typedNode, specialized, tcoOptimized: IAstNode;
isPure: Boolean;
begin
// Same steps as above, but Node is already a definition (Lambda)
var expanded := ExpandMacros(Node);
@@ -590,6 +599,9 @@ begin
specialized := Specialize(typedNode);
tcoOptimized := TAstTCO.Optimize(specialized);
// Purity Inference
isPure := TPurityAnalyzer.IsPure(tcoOptimized.AsLambdaExpression.Body);
var visitor := FExecutionStrategy.CreateVisitor(descriptor.CreateScope(FRootScope));
var closure := tcoOptimized.Accept(visitor);
@@ -600,7 +612,7 @@ begin
TEvaluatorVisitor.HandleTCO(Result);
end;
Result := TCompiledFunction.Create(finalFunc, funcType);
Result := TCompiledFunction.Create(finalFunc, funcType, isPure);
end;
function TEnvironment.ExpandMacros(const Node: IAstNode): IAstNode;
+6
View File
@@ -158,9 +158,11 @@ type
{$region 'private'}
function GetBody: IAstNode;
function GetParameters: TArray<IIdentifierNode>;
function GetIsPure: Boolean; // (* ADDED *)
{$endregion}
property Body: IAstNode read GetBody;
property Parameters: TArray<IIdentifierNode> read GetParameters;
property IsPure: Boolean read GetIsPure; // (* ADDED *)
end;
INopNode = interface(IAstTypedNode)
@@ -228,6 +230,7 @@ type
function GetUpvalues: TArray<TResolvedAddress>;
function GetHasNestedLambdas: Boolean;
// GetIsPure is inherited from IFunctionDefinition
{$endregion}
property Parameters: TArray<IIdentifierNode> read GetParameters;
@@ -238,6 +241,7 @@ type
property Upvalues: TArray<TResolvedAddress> read GetUpvalues;
property HasNestedLambdas: Boolean read GetHasNestedLambdas;
// IsPure is inherited
end;
IFunctionCallNode = interface(IAstTypedNode)
@@ -246,11 +250,13 @@ type
function GetArguments: TArray<IAstNode>;
function GetIsTailCall: Boolean;
function GetStaticTarget: TDataValue.TFunc;
function GetIsTargetPure: Boolean; // (* ADDED *)
{$endregion}
property Callee: IAstNode read GetCallee;
property Arguments: TArray<IAstNode> read GetArguments;
property IsTailCall: Boolean read GetIsTailCall;
property StaticTarget: TDataValue.TFunc read GetStaticTarget;
property IsTargetPure: Boolean read GetIsTargetPure; // (* ADDED *)
end;
// A node representing a macro call.
+16 -15
View File
@@ -47,11 +47,11 @@ type
public
Target: TDataValue.TFunc;
ReturnType: IStaticType;
constructor Create(const ATarget: TDataValue.TFunc; const AReturnType: IStaticType);
IsPure: Boolean; // (* ADDED *)
constructor Create(const ATarget: TDataValue.TFunc; const AReturnType: IStaticType; AIsPure: Boolean);
end;
// The cache holding TDataValue.TFunc wrappers for static native functions.
// Verwendet den generischen TSpecializedMethod-Record aus Myc.Ast.Types
TStaticBootstrapCache = TDictionary<TStaticSignatureKey, TSpecializedMethod>;
TRtlFunctionInfo = class
@@ -153,7 +153,7 @@ constructor TRtlExportAttribute.Create(const AName: string);
begin
inherited Create;
Self.Name := AName;
Self.IsPure := False; // Default to impure
Self.IsPure := False; // Default to impure (safe)
end;
constructor TRtlExportAttribute.Create(const AName: string; AIsPure: Boolean);
@@ -230,6 +230,15 @@ begin
inherited Destroy;
end;
{ TSpecializedMethod }
constructor TSpecializedMethod.Create(const ATarget: TDataValue.TFunc; const AReturnType: IStaticType; AIsPure: Boolean);
begin
Target := ATarget;
ReturnType := AReturnType;
IsPure := AIsPure;
end;
{ TRtlRegistry }
class constructor TRtlRegistry.Create;
@@ -252,7 +261,7 @@ begin
FStaticBootstrap := TStaticBootstrapCache.Create(TStaticSignatureKeyComparer.Create);
FStaticFuncMap := TRtlFunctionMap.Create;
// 2. Perform RTTI Scan (formerly Pass 1 of RegisterAll)
// 2. Perform RTTI Scan
ctx := TRttiContext.Create;
try
rtlType := ctx.GetType(TypeInfo(TRtlFunctions));
@@ -292,7 +301,7 @@ begin
if isDynamic then
begin
if Assigned(funcInfo.DynamicWrapper) and Assigned(funcInfo.DynamicWrapper) then
if Assigned(funcInfo.DynamicWrapper) then
raise EInvalidOpException.CreateFmt('Duplicate dynamic fallback defined for %s', [rtlName]);
var wrapperFactory :=
@@ -323,8 +332,8 @@ begin
var wrapper := CreateStaticWrapper(method, retType, argTypes, rttiParams);
if Assigned(wrapper) then
begin
methodRecord := TSpecializedMethod.Create(wrapper, retType);
// Use renamed static helper
// (* UPDATED: Capture IsPure from attribute *)
methodRecord := TSpecializedMethod.Create(wrapper, retType, exportAttr.IsPure);
RegisterStaticSpecialization(rtlName, argTypes, methodRecord);
end
else
@@ -366,8 +375,6 @@ var
key: TStaticSignatureKey;
begin
key := TStaticSignatureKey.Create(AName, AArgTypes);
// This allows multiple RTTI scans (e.g. in tests) without crashing,
// though only the class constructor should call this now.
FStaticBootstrap.AddOrSetValue(key, AMethod);
end;
@@ -724,12 +731,6 @@ begin
TRtlRegistry.RegisterAll(AScope);
end;
constructor TSpecializedMethod.Create(const ATarget: TDataValue.TFunc; const AReturnType: IStaticType);
begin
Target := ATarget;
ReturnType := AReturnType;
end;
initialization
// Register this library's functions with the central AST factory.
TAst.RegisterLibrary(RegisterRtlFunctions);
+11 -1
View File
@@ -486,7 +486,17 @@ begin
else
begin
// Use TAst factory and copy properties via interface getters
Result := TAst.LambdaExpr(newParams, newBody, Node.Layout, Node.Descriptor, Node.Upvalues, Node.HasNestedLambdas, Node.StaticType);
Result :=
TAst.LambdaExpr(
newParams,
newBody,
Node.Layout,
Node.Descriptor,
Node.Upvalues,
Node.HasNestedLambdas,
Node.IsPure,
Node.StaticType
);
end;
end;
+39 -15
View File
@@ -56,14 +56,15 @@ type
const AStaticType: IStaticType = nil
): ITernaryExpressionNode; static;
// Factory Updated: Includes Layout (mandatory) and Descriptor (optional)
// (* UPDATED Factory: Added AIsPure *)
class function LambdaExpr(
const AParameters: TArray<IIdentifierNode>;
const ABody: IAstNode;
const ALayout: IScopeLayout = nil; // Nil allowed only for raw AST construction (before Binder)
const ADescriptor: IScopeDescriptor = nil; // Nil allowed before TypeChecker
const ALayout: IScopeLayout = nil;
const ADescriptor: IScopeDescriptor = nil;
const AUpvalues: TArray<TResolvedAddress> = nil;
const AHasNestedLambdas: Boolean = False;
const AIsPure: Boolean = False; // (* ADDED *)
const AStaticType: IStaticType = nil
): ILambdaExpressionNode; static;
@@ -76,12 +77,14 @@ type
class function Unquote(const AExpression: IAstNode): IUnquoteNode; static;
class function UnquoteSplicing(const AExpression: IQuasiquoteNode): IUnquoteSplicingNode; static;
// (* UPDATED Factory: Added AIsTargetPure *)
class function FunctionCall(
const ACallee: IAstNode;
const AArguments: TArray<IAstNode>;
const AStaticType: IStaticType = nil;
const AIsTailCall: Boolean = False;
const AStaticTarget: TDataValue.TFunc = nil
const AStaticTarget: TDataValue.TFunc = nil;
const AIsTargetPure: Boolean = False // (* ADDED *)
): IFunctionCallNode; static;
class function MacroExpansionNode(
@@ -194,6 +197,7 @@ type
FDescriptor: IScopeDescriptor;
FUpvalues: TArray<TResolvedAddress>;
FHasNestedLambdas: Boolean;
FIsPure: Boolean; // (* ADDED *)
function GetParameters: TArray<IIdentifierNode>;
function GetBody: IAstNode;
@@ -201,6 +205,7 @@ type
function GetDescriptor: IScopeDescriptor;
function GetUpvalues: TArray<TResolvedAddress>;
function GetHasNestedLambdas: Boolean;
function GetIsPure: Boolean; // (* ADDED *)
function GetKind: TAstNodeKind; override;
public
constructor Create(
@@ -210,7 +215,8 @@ type
const ALayout: IScopeLayout;
const ADescriptor: IScopeDescriptor;
const AUpvalues: TArray<TResolvedAddress>;
const AHasNestedLambdas: Boolean
const AHasNestedLambdas: Boolean;
const AIsPure: Boolean // (* ADDED *)
);
destructor Destroy; override;
function Accept(const Visitor: IAstVisitor): TDataValue; override;
@@ -223,10 +229,12 @@ type
FArguments: TArray<IAstNode>;
FIsTailCall: Boolean;
FStaticTarget: TDataValue.TFunc;
FIsTargetPure: Boolean; // (* ADDED *)
function GetCallee: IAstNode;
function GetArguments: TArray<IAstNode>;
function GetIsTailCall: Boolean;
function GetStaticTarget: TDataValue.TFunc;
function GetIsTargetPure: Boolean; // (* ADDED *)
function GetKind: TAstNodeKind; override;
public
constructor Create(
@@ -234,12 +242,14 @@ type
const AArguments: TArray<IAstNode>;
const AStaticType: IStaticType;
const AIsTailCall: Boolean;
const AStaticTarget: TDataValue.TFunc
const AStaticTarget: TDataValue.TFunc;
const AIsTargetPure: Boolean // (* ADDED *)
);
function Accept(const Visitor: IAstVisitor): TDataValue; override;
function AsFunctionCall: IFunctionCallNode; override;
end;
// ... (Other node definitions unchanged) ...
TVariableDeclarationNode = class(TAstTypedNode, IVariableDeclarationNode)
private
FIdentifier: IIdentifierNode;
@@ -260,8 +270,6 @@ type
function AsVariableDeclaration: IVariableDeclarationNode; override;
end;
// --- Other internal node classes ---
TConstantNode = class(TAstTypedNode, IConstantNode)
private
FValue: TDataValue;
@@ -644,7 +652,6 @@ end;
class function TAst.Constant(const AValue: TDataValue; const AStaticType: IStaticType = nil): IConstantNode;
begin
var constType := AStaticType;
if constType = nil then
begin
case AValue.Kind of
@@ -655,7 +662,6 @@ begin
constType := TTypes.Unknown;
end;
end;
Result := TConstantNode.Create(AValue, constType);
end;
@@ -684,7 +690,8 @@ class function TAst.FunctionCall(
const AArguments: TArray<IAstNode>;
const AStaticType: IStaticType = nil;
const AIsTailCall: Boolean = False;
const AStaticTarget: TDataValue.TFunc = nil
const AStaticTarget: TDataValue.TFunc = nil;
const AIsTargetPure: Boolean = False // (* ADDED *)
): IFunctionCallNode;
begin
Result :=
@@ -694,7 +701,8 @@ begin
if AStaticType <> nil then AStaticType
else TTypes.Unknown,
AIsTailCall,
AStaticTarget
AStaticTarget,
AIsTargetPure
);
end;
@@ -759,6 +767,7 @@ class function TAst.LambdaExpr(
const ADescriptor: IScopeDescriptor = nil;
const AUpvalues: TArray<TResolvedAddress> = nil;
const AHasNestedLambdas: Boolean = False;
const AIsPure: Boolean = False; // (* ADDED *)
const AStaticType: IStaticType = nil
): ILambdaExpressionNode;
begin
@@ -771,7 +780,8 @@ begin
ALayout,
ADescriptor,
AUpvalues,
AHasNestedLambdas
AHasNestedLambdas,
AIsPure
);
end;
@@ -1247,7 +1257,8 @@ constructor TLambdaExpressionNode.Create(
const ALayout: IScopeLayout;
const ADescriptor: IScopeDescriptor;
const AUpvalues: TArray<TResolvedAddress>;
const AHasNestedLambdas: Boolean
const AHasNestedLambdas: Boolean;
const AIsPure: Boolean // (* ADDED *)
);
begin
inherited Create(AStaticType);
@@ -1257,6 +1268,7 @@ begin
FDescriptor := ADescriptor;
FUpvalues := AUpvalues;
FHasNestedLambdas := AHasNestedLambdas;
FIsPure := AIsPure;
// Consistency Check
if Assigned(FDescriptor) and (FDescriptor.Layout <> FLayout) then
@@ -1308,6 +1320,11 @@ begin
Result := FHasNestedLambdas;
end;
function TLambdaExpressionNode.GetIsPure: Boolean; // (* ADDED *)
begin
Result := FIsPure;
end;
function TLambdaExpressionNode.GetKind: TAstNodeKind;
begin
Result := akLambdaExpression;
@@ -1444,7 +1461,8 @@ constructor TFunctionCallNode.Create(
const AArguments: TArray<IAstNode>;
const AStaticType: IStaticType;
const AIsTailCall: Boolean;
const AStaticTarget: TDataValue.TFunc
const AStaticTarget: TDataValue.TFunc;
const AIsTargetPure: Boolean // (* ADDED *)
);
begin
inherited Create(AStaticType);
@@ -1452,6 +1470,7 @@ begin
FArguments := AArguments;
FIsTailCall := AIsTailCall;
FStaticTarget := AStaticTarget;
FIsTargetPure := AIsTargetPure;
end;
function TFunctionCallNode.Accept(const Visitor: IAstVisitor): TDataValue;
@@ -1489,6 +1508,11 @@ begin
Result := FStaticTarget;
end;
function TFunctionCallNode.GetIsTargetPure: Boolean; // (* ADDED *)
begin
Result := FIsTargetPure;
end;
{ TMacroExpansionNode }
constructor TMacroExpansionNode.Create(const ACallNode: IFunctionCallNode; const AExpandedBody: IAstNode);