Files
MycLib/Src/AST/Myc.Ast.Evaluator.pas
T
Michael Schimmel a052dfb20f AST testing
2025-11-23 00:24:43 +01:00

644 lines
23 KiB
ObjectPascal

unit Myc.Ast.Evaluator;
interface
uses
System.SysUtils,
System.Classes,
System.Generics.Collections,
Myc.Data.Scalar,
Myc.Data.Value,
Myc.Ast.Nodes,
Myc.Ast.Scope;
type
// The standard AST evaluator for production use.
// This class inherits directly from TInterfacedObject as it is an
// Interpreter (AST -> TDataValue), not a Transformer (AST -> IAstNode).
TEvaluatorVisitor = class(TInterfacedObject, IAstVisitor, IEvaluatorVisitor)
private
FScope: IExecutionScope;
protected
// IAstVisitor methods made virtual for TDebugEvaluatorVisitor to override
function VisitConstant(const Node: IConstantNode): TDataValue; virtual;
function VisitIdentifier(const Node: IIdentifierNode): TDataValue; virtual;
function VisitKeyword(const Node: IKeywordNode): TDataValue; virtual;
function VisitIfExpression(const Node: IIfExpressionNode): TDataValue; virtual;
function VisitTernaryExpression(const Node: ITernaryExpressionNode): TDataValue; virtual;
function VisitLambdaExpression(const Node: ILambdaExpressionNode): TDataValue; virtual;
function VisitMacroDefinition(const Node: IMacroDefinitionNode): TDataValue; virtual;
function VisitQuasiquote(const Node: IQuasiquoteNode): TDataValue; virtual;
function VisitUnquote(const Node: IUnquoteNode): TDataValue; virtual;
function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): TDataValue; virtual;
function VisitFunctionCall(const Node: IFunctionCallNode): TDataValue; virtual;
function VisitMacroExpansionNode(const Node: IMacroExpansionNode): TDataValue; virtual;
function VisitBlockExpression(const Node: IBlockExpressionNode): TDataValue; virtual;
function VisitVariableDeclaration(const Node: IVariableDeclarationNode): TDataValue; virtual;
function VisitAssignment(const Node: IAssignmentNode): TDataValue; virtual;
function VisitIndexer(const Node: IIndexerNode): TDataValue; virtual;
function VisitMemberAccess(const Node: IMemberAccessNode): TDataValue; virtual;
function VisitRecordLiteral(const Node: IRecordLiteralNode): TDataValue; virtual;
function VisitCreateSeries(const Node: ICreateSeriesNode): TDataValue; virtual;
function VisitAddSeriesItem(const Node: IAddSeriesItemNode): TDataValue; virtual;
function VisitSeriesLength(const Node: ISeriesLengthNode): TDataValue; virtual;
function VisitRecurNode(const Node: IRecurNode): TDataValue; virtual;
function VisitNop(const Node: INopNode): TDataValue; virtual;
function IsTruthy(const AValue: TDataValue): Boolean; inline;
// Returns a closure that can create the correct type of visitor for a lambda's body.
function CreateVisitorFactory: TEvaluatorFactory; virtual;
property Scope: IExecutionScope read FScope;
public
constructor Create(const AScope: IExecutionScope);
// Executes an AST with proper TCO handling. This is the main entry point.
function Execute(const RootNode: IAstNode): TDataValue;
class procedure HandleTCO(var ResultValue: TDataValue); static;
end;
implementation
uses
System.TypInfo,
System.Generics.Defaults,
Myc.Ast,
Myc.Data.Keyword,
Myc.Data.Decimal,
Myc.Data.Series,
Myc.Data.Scalar.JSON,
Myc.Ast.Types;
// Helper type for TCO via trampolining.
type
TThunk = record
Callee: TDataValue;
Args: TArray<TDataValue>;
Recur: Boolean;
constructor Create(const ACallee: TDataValue; const AArgs: TArray<TDataValue>; ARecur: Boolean);
end;
constructor TThunk.Create(const ACallee: TDataValue; const AArgs: TArray<TDataValue>; ARecur: Boolean);
begin
Callee := ACallee;
Args := AArgs;
Recur := ARecur;
end;
{ TDynamicRecord }
type
// Runtime implementation for generic records using linear search
TDynamicRecord = class(TInterfacedObject, IKeywordMapping<TDataValue>)
private
FFields: TArray<TPair<IKeyword, TDataValue>>;
function GetFields: TArray<TPair<IKeyword, TDataValue>>;
public
constructor Create(const AFields: TArray<TPair<IKeyword, TDataValue>>);
function IndexOf(const Key: IKeyword): Integer;
end;
constructor TDynamicRecord.Create(const AFields: TArray<TPair<IKeyword, TDataValue>>);
begin
inherited Create;
FFields := AFields;
end;
function TDynamicRecord.GetFields: TArray<TPair<IKeyword, TDataValue>>;
begin
Result := FFields;
end;
function TDynamicRecord.IndexOf(const Key: IKeyword): Integer;
begin
// Linear search (O(n)) as requested
for Result := 0 to High(FFields) do
begin
if FFields[Result].Key.Idx = Key.Idx then
exit;
end;
Result := -1;
end;
{ TEvaluatorVisitor }
constructor TEvaluatorVisitor.Create(const AScope: IExecutionScope);
begin
inherited Create;
Assert(Assigned(AScope));
FScope := AScope;
end;
function TEvaluatorVisitor.Execute(const RootNode: IAstNode): TDataValue;
begin
if not Assigned(RootNode) then
exit(TDataValue.Void);
Result := RootNode.Accept(Self);
HandleTCO(Result);
end;
function TEvaluatorVisitor.CreateVisitorFactory: TEvaluatorFactory;
begin
// The production visitor returns a factory that creates another production visitor.
Result := function(const AScope: IExecutionScope): IEvaluatorVisitor begin Result := TEvaluatorVisitor.Create(AScope); end;
end;
class procedure TEvaluatorVisitor.HandleTCO(var ResultValue: TDataValue);
begin
// This is the central trampoline loop for Tail Call Optimization.
// It runs as long as the evaluation returns a thunk.
while ResultValue.Kind = vkGeneric do
begin
var thunk := ResultValue.AsGeneric<TThunk>;
var callee := thunk.Callee.AsMethod();
ResultValue := callee(thunk.Args);
end;
end;
function TEvaluatorVisitor.IsTruthy(const AValue: TDataValue): Boolean;
begin
// Other types (Text, Series, etc.) are considered "false" in a boolean context
if (AValue.Kind <> vkScalar) then
exit(false);
case AValue.AsScalar.Kind of
TScalar.TKind.Ordinal: Result := AValue.AsScalar.Value.AsInt64 <> 0;
TScalar.TKind.Float: Result := AValue.AsScalar.Value.AsDouble <> 0.0;
TScalar.TKind.Keyword: Result := AValue.AsScalar.Value.AsInt64 <> 0;
TScalar.TKind.Boolean: Result := AValue.AsScalar.Value.AsInt64 <> 0;
else
Result := false;
end;
end;
function TEvaluatorVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode): TDataValue;
var
capturedCells: TArray<IValueCell>;
i: Integer;
closureScope: IExecutionScope;
visitorFactory: TEvaluatorFactory;
begin
// 1. Capture Upvalues
// The Node.Upvalues array contains the physical addresses in the *current* scope (FScope)
// that map to the closure's upvalues.
if Node.Upvalues <> nil then
begin
SetLength(capturedCells, Length(Node.Upvalues));
for i := 0 to High(Node.Upvalues) do
capturedCells[i] := FScope.Capture(Node.Upvalues[i]);
end
else
capturedCells := nil;
// 2. Determine Parent Scope for Closure
// Memory optimization: a lambda's scope does not need to be kept alive as a parent
// if it contains no nested lambdas that might need to capture from it later.
// (Assuming HasNestedLambdas flag is set correctly by Binder)
if Node.HasNestedLambdas then
closureScope := FScope
else
closureScope := nil;
// 3. Prepare Visitor Factory
visitorFactory := CreateVisitorFactory();
// 4. Capture Metadata for Closure
var descriptor := Node.Descriptor; // Runtime Descriptor (contains types + layout)
var params := Node.Parameters;
var cNode: ILambdaExpressionNode := Node;
// [unsafe] prevents a reference cycle since the closure captures itself for 'recur'.
var [unsafe] closure: TDataValue.TFunc;
closure :=
function(const ArgValues: TArray<TDataValue>): TDataValue
var
lambdaScope: IExecutionScope;
bodyVisitor: IAstVisitor;
i: Integer;
adr: TResolvedAddress;
begin
if (Length(ArgValues) <> Length(params)) then
raise EArgumentException.CreateFmt('Argument count mismatch: expected %d, got %d', [Length(params), Length(ArgValues)]);
// Create the new execution scope for this function call.
lambdaScope := TScope.CreateScope(closureScope, descriptor, capturedCells);
// Capture the closure itself in slot 0 for 'recur' to find it (if needed).
// Note: The Binder reserves Slot 0 for <self>.
adr.Kind := akLocalOrParent;
adr.ScopeDepth := 0;
adr.SlotIndex := 0;
lambdaScope[adr] := TDataValue(closure); // Explicit cast
// Populate the scope with the actual parameters passed to the function.
for i := 0 to High(ArgValues) do
begin
// Parameters are bound to specific slots by the Binder.
// We access them via the Address stored in the parameter node.
adr := params[i].Address;
// Defensive check: Ensure we are writing to local scope
Assert(adr.ScopeDepth = 0);
lambdaScope[adr] := ArgValues[i];
end;
// Create a visitor with the new scope and execute the lambda's body.
bodyVisitor := visitorFactory(lambdaScope);
Result := cNode.Body.Accept(bodyVisitor);
end;
// The result of visiting a lambda node is the callable closure itself.
Result := TDataValue(closure); // Explicit cast
end;
function TEvaluatorVisitor.VisitMacroDefinition(const Node: IMacroDefinitionNode): TDataValue;
begin
// Macro definitions are compile-time constructs.
// The Evaluator ignores them (treated as Void).
Result := TDataValue.Void;
end;
function TEvaluatorVisitor.VisitQuasiquote(const Node: IQuasiquoteNode): TDataValue;
begin
raise Exception.Create('Quasiquote nodes are a compile-time construct and cannot be evaluated at runtime.');
end;
function TEvaluatorVisitor.VisitUnquote(const Node: IUnquoteNode): TDataValue;
begin
raise Exception.Create('Unquote nodes are a compile-time construct and cannot be evaluated at runtime.');
end;
function TEvaluatorVisitor.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): TDataValue;
begin
raise Exception.Create('Unquote-splicing nodes are a compile-time construct and cannot be evaluated at runtime.');
end;
function TEvaluatorVisitor.VisitFunctionCall(const Node: IFunctionCallNode): TDataValue;
var
calleeValue: TDataValue;
argValues: TArray<TDataValue>;
i: Integer;
argNodes: TArray<IAstNode>;
begin
if Assigned(Node.StaticTarget) then
begin
// --- Static Path (Optimized) ---
// 1. Evaluate arguments
argNodes := Node.Arguments;
SetLength(argValues, Length(argNodes));
for i := 0 to High(argNodes) do
begin
// Assuming arguments passed type check
argValues[i] := argNodes[i].Accept(Self);
end;
// 2. Call the static target directly
Result := Node.StaticTarget(argValues);
// 3. Handle TCO
HandleTCO(Result);
end
else
begin
// --- Dynamic Path (Default) ---
calleeValue := Node.Callee.Accept(Self);
if calleeValue.Kind <> vkMethod then
raise EArgumentException.Create('Expression is not invokable in this context.');
argNodes := Node.Arguments;
SetLength(argValues, Length(argNodes));
for i := 0 to High(argNodes) do
argValues[i] := argNodes[i].Accept(Self);
if Node.IsTailCall then
begin
// This is a tail call. Return a thunk to be processed by the trampoline.
Result := TDataValue.FromGeneric<TThunk>(TThunk.Create(calleeValue, argValues, false));
end
else
begin
// This is a non-tail call. It must execute the call and act as the trampoline.
Result := (calleeValue.AsMethod)(argValues);
HandleTCO(Result);
end;
end;
end;
function TEvaluatorVisitor.VisitMacroExpansionNode(const Node: IMacroExpansionNode): TDataValue;
begin
// The evaluator simply "unwraps" the macro expansion node
// and executes the expanded body it contains.
Result := Node.ExpandedBody.Accept(Self);
end;
function TEvaluatorVisitor.VisitRecurNode(const Node: IRecurNode): TDataValue;
var
argValues: TArray<TDataValue>;
calleeAddress: TResolvedAddress;
calleeValue: TDataValue;
i: Integer;
begin
// The binder ensures this is only in a tail position.
SetLength(argValues, Length(Node.Arguments));
for i := 0 to High(Node.Arguments) do
argValues[i] := Node.Arguments[i].Accept(Self);
// The callee is the current function, stored in slot 0 of the current scope.
calleeAddress.Kind := akLocalOrParent;
calleeAddress.ScopeDepth := 0;
calleeAddress.SlotIndex := 0;
calleeValue := FScope[calleeAddress];
// Recur always returns a thunk for the trampoline.
Result := TDataValue.FromGeneric<TThunk>(TThunk.Create(calleeValue, argValues, true));
end;
function TEvaluatorVisitor.VisitAddSeriesItem(const Node: IAddSeriesItemNode): TDataValue;
var
itemValue, lookbackValue, seriesVar: TDataValue;
lookback: Int64;
begin
seriesVar := FScope[Node.Series.Address];
itemValue := Node.Value.Accept(Self);
lookback := -1;
if Assigned(Node.Lookback) then
begin
lookbackValue := Node.Lookback.Accept(Self);
if (lookbackValue.Kind <> vkScalar) or (lookbackValue.AsScalar.Kind <> TScalar.TKind.Ordinal) then
raise EArgumentException.Create('Lookback parameter must be an integer.');
lookback := lookbackValue.AsScalar.Value.AsInt64;
end;
case seriesVar.Kind of
vkRecordSeries:
begin
if (itemValue.Kind <> vkRecord) then
raise EArgumentException.Create('Can only add record values to a TScalarRecordSeries.');
with seriesVar.AsRecordSeries do
begin
Add(itemValue.AsRecord, lookback);
end;
end;
else
raise EArgumentException.Create('"add" operation is only supported for series types.');
end;
Result := TDataValue.Void;
end;
function TEvaluatorVisitor.VisitAssignment(const Node: IAssignmentNode): TDataValue;
begin
if Node.Target.Kind <> akIdentifier then
raise ETypeException.Create('Runtime Error: Assignment target must be an identifier.');
// Evaluate value
Result := Node.Value.Accept(Self);
// Assign
FScope[Node.Target.AsIdentifier.Address] := Result;
end;
function TEvaluatorVisitor.VisitConstant(const Node: IConstantNode): TDataValue;
begin
Result := Node.Value;
end;
function TEvaluatorVisitor.VisitKeyword(const Node: IKeywordNode): TDataValue;
begin
// Return the keyword as a TScalar value
Result := TDataValue(TScalar.FromKeyword(Node.Value)); // Explicit cast
end;
function TEvaluatorVisitor.VisitCreateSeries(const Node: ICreateSeriesNode): TDataValue;
var
def: string;
begin
def := Node.Definition.Trim;
if def.StartsWith('[') then
begin
var recordDef := TRttiAstHelper.JsonToRecordDefinition(def);
if Length(recordDef.Fields) = 0 then
raise EArgumentException.Create('Failed to parse record definition from JSON array.');
var recordSeries := TScalarRecordSeries.Create(recordDef);
Result := TDataValue.FromRecordSeries(recordSeries);
end
else
begin
var scalarKind := TScalar.StringToKind(def);
Result := TDataValue.FromSeries(TScalarSeries.Create(scalarKind));
end;
end;
function TEvaluatorVisitor.VisitIdentifier(const Node: IIdentifierNode): TDataValue;
begin
// The scope's GetValues implementation handles unboxing and parent lookup.
Result := FScope[Node.Address];
end;
function TEvaluatorVisitor.VisitIndexer(const Node: IIndexerNode): TDataValue;
var
baseValue, indexValue: TDataValue;
index: Int64;
series: ISeries;
recSeries: IRecordSeries;
i, fieldCount: Integer;
values: TArray<TScalar.TValue>;
key: IKeyword;
memberSeries: ISeries;
scalarValue: TScalar;
rec: TScalarRecord;
begin
baseValue := Node.Base.Accept(Self);
indexValue := Node.Index.Accept(Self);
if (indexValue.Kind <> vkScalar) or (indexValue.AsScalar.Kind <> TScalar.TKind.Ordinal) then
raise EArgumentException.Create('Indexer `[]` requires an integer argument.');
index := indexValue.AsScalar.Value.AsInt64;
case baseValue.Kind of
vkSeries:
begin
series := baseValue.AsSeries;
if (index < 0) or (index >= series.TotalCount) then
raise EArgumentException.CreateFmt('Index %d is out of bounds for series with %d elements.', [index, series.TotalCount]);
Result := TDataValue(series.Items[Integer(index)]); // Explicit cast
end;
vkRecordSeries:
begin
recSeries := baseValue.AsRecordSeries;
if (index < 0) or (index >= recSeries.TotalCount) then
raise EArgumentException.CreateFmt('Index %d is out of bounds for series with %d elements.', [index, recSeries.TotalCount]);
// Materialize the TScalarRecord by accessing each member series by index
fieldCount := Length(recSeries.Def.Fields);
SetLength(values, fieldCount);
for i := 0 to fieldCount - 1 do
begin
key := recSeries.Def.Fields[i].Key;
memberSeries := recSeries[key];
scalarValue := memberSeries[index];
values[i] := scalarValue.Value;
end;
rec := TScalarRecord.Create(recSeries.Def, values);
Result := TDataValue.FromRecord(rec);
end;
else
raise EArgumentException.Create('Indexer `[]` is not supported for this value type.');
end;
end;
function TEvaluatorVisitor.VisitMemberAccess(const Node: IMemberAccessNode): TDataValue;
var
baseValue: TDataValue;
begin
baseValue := Node.Base.Accept(Self);
case baseValue.Kind of
vkRecordSeries: Result := TDataValue.FromSeries(baseValue.AsRecordSeries[Node.Member.Value]);
vkRecord: Result := TDataValue(baseValue.AsRecord[Node.Member.Value]); // Explicit cast
vkGenericRecord:
begin
var rec := baseValue.AsGenericRecord;
var fieldIndex := rec.IndexOf(Node.Member.Value);
if fieldIndex < 0 then
raise EArgumentException.CreateFmt('Member ":%s" not found in record.', [Node.Member.Value.Name]);
Result := rec.Fields[fieldIndex].Value;
end;
else
raise EArgumentException.Create('Member access operator `.` is not supported for this value type.');
end;
end;
function TEvaluatorVisitor.VisitRecordLiteral(const Node: IRecordLiteralNode): TDataValue;
var
i: Integer;
begin
// Use the properties populated by the TypeChecker/Binder
if Assigned(Node.GenericDefinition) then
begin
// --- GENERIC RECORD PATH ---
var genFields: TArray<TPair<IKeyword, TDataValue>>;
SetLength(genFields, Length(Node.Fields));
for i := 0 to High(Node.Fields) do
begin
genFields[i] :=
TPair<IKeyword, TDataValue>.Create(
Node.Fields[i].Key.Value,
Node.Fields[i].Value.Accept(Self) // Evaluate expression
);
end;
var dynRec := TDynamicRecord.Create(genFields);
Result := TDataValue.FromGenericRecord(dynRec);
end
else if Assigned(Node.ScalarDefinition) then
begin
// --- SCALAR RECORD PATH ---
var values: TArray<TScalar.TValue>;
SetLength(values, Length(Node.Fields));
for i := 0 to High(Node.Fields) do
begin
var valData := Node.Fields[i].Value.Accept(Self);
values[i] := valData.AsScalar.Value;
end;
var rec := TScalarRecord.Create(Node.ScalarDefinition, values);
Result := TDataValue.FromRecord(rec);
end
else
raise EInvalidOpException.Create('RecordLiteral has no definition (Binder/TypeChecker failure).');
end;
function TEvaluatorVisitor.VisitVariableDeclaration(const Node: IVariableDeclarationNode): TDataValue;
var
address: TResolvedAddress;
begin
if Node.Target.Kind <> akIdentifier then
raise ETypeException.Create('Runtime Error: Variable declaration target must be an identifier.');
// 1. Evaluate Initializer
if Assigned(Node.Initializer) then
Result := Node.Initializer.Accept(Self)
else
Result := TDataValue.Void;
// 2. Get Address (assigned by Binder)
address := Node.Target.AsIdentifier.Address;
// 3. Store Value
if Node.IsBoxed then
begin
// Capture (heap alloc)
Assert(address.ScopeDepth = 0);
FScope.DefineBoxed(address.SlotIndex, Result);
end
else
begin
// Stack alloc
FScope[address] := Result;
end;
end;
function TEvaluatorVisitor.VisitIfExpression(const Node: IIfExpressionNode): TDataValue;
begin
if IsTruthy(Node.Condition.Accept(Self)) then
Result := Node.ThenBranch.Accept(Self)
else if Assigned(Node.ElseBranch) then
Result := Node.ElseBranch.Accept(Self)
else
Result := TDataValue.Void;
end;
function TEvaluatorVisitor.VisitTernaryExpression(const Node: ITernaryExpressionNode): TDataValue;
begin
if IsTruthy(Node.Condition.Accept(Self)) then
Result := Node.ThenBranch.Accept(Self)
else
Result := Node.ElseBranch.Accept(Self);
end;
function TEvaluatorVisitor.VisitBlockExpression(const Node: IBlockExpressionNode): TDataValue;
var
expression: IAstNode;
begin
Result := TDataValue.Void;
for expression in Node.Expressions do
Result := expression.Accept(Self);
end;
function TEvaluatorVisitor.VisitNop(const Node: INopNode): TDataValue;
begin
Result := TDataValue.Void;
end;
function TEvaluatorVisitor.VisitSeriesLength(const Node: ISeriesLengthNode): TDataValue;
var
seriesValue: TDataValue;
len: Int64;
begin
seriesValue := FScope[Node.Series.Address];
case seriesValue.Kind of
vkSeries: len := seriesValue.AsSeries.Count;
vkRecordSeries: len := seriesValue.AsRecordSeries.Count;
else
raise EArgumentException.CreateFmt('Cannot get length of type %s.', [GetEnumName(TypeInfo(TDataValueKind), Ord(seriesValue.Kind))]);
end;
Result := TDataValue(TScalar.FromInt64(len)); // Explicit cast
end;
end.