RECUR keyword added

This commit is contained in:
Michael Schimmel
2025-09-20 12:06:51 +02:00
parent e03155179a
commit 81dd69bf49
13 changed files with 352 additions and 98 deletions
+45 -1
View File
@@ -32,6 +32,7 @@ type
protected
function Accept(const Node: IAstNode): TDataValue; override;
function IsValidIdentifier(const Name: string): Boolean;
public
constructor Create(const AInitialScope: IExecutionScope);
@@ -47,6 +48,7 @@ type
function VisitIfExpression(const Node: IIfExpressionNode): TDataValue; override;
function VisitTernaryExpression(const Node: ITernaryExpressionNode): TDataValue; override;
function VisitFunctionCall(const Node: IFunctionCallNode): TDataValue; override;
function VisitRecurNode(const Node: IRecurNode): TDataValue; override;
function VisitBinaryExpression(const Node: IBinaryExpressionNode): TDataValue; override;
function VisitUnaryExpression(const Node: IUnaryExpressionNode): TDataValue; override;
function VisitAssignment(const Node: IAssignmentNode): TDataValue; override;
@@ -58,6 +60,7 @@ implementation
uses
System.Generics.Defaults,
System.Character,
Myc.Ast;
type
@@ -214,6 +217,17 @@ begin
inherited;
end;
function TAstBinder.VisitRecurNode(const Node: IRecurNode): TDataValue;
begin
// Check if the current context is a tail position.
if not FIsTailStack.Peek then
raise Exception.Create('''recur'' can only be used in a tail position.');
// Arguments to recur are not in a tail position.
FNextIsTail := False;
inherited;
end;
function TAstBinder.VisitIdentifier(const Node: IIdentifierNode): TDataValue;
var
depth, idx: Integer;
@@ -278,7 +292,9 @@ begin
try
EnterScope;
try
FCurrentDescriptor.Define('Self');
// Reserve slot 0 for the closure itself (for 'recur'),
// using a name that cannot be accessed from source code.
FCurrentDescriptor.Define('<self>');
for param in Node.Parameters do
FCurrentDescriptor.Define(param.Name);
@@ -341,6 +357,30 @@ begin
inherited;
end;
function TAstBinder.IsValidIdentifier(const Name: string): Boolean;
var
i: Integer;
c: Char;
begin
if Name.IsEmpty then
exit(False);
// First character must be a letter or underscore.
c := Name[1];
if not (c.IsLetter or (c = '_')) then
exit(False);
// Subsequent characters can be letters, numbers, underscore, or hyphen.
for i := 2 to Length(Name) do
begin
c := Name[i];
if not (c.IsLetterOrDigit or (c = '_') or (c = '-')) then
exit(False);
end;
Result := True;
end;
function TAstBinder.VisitVariableDeclaration(const Node: IVariableDeclarationNode): TDataValue;
var
slotIndex: Integer;
@@ -350,6 +390,10 @@ begin
if Assigned(Node.Initializer) then
Accept(Node.Initializer);
// Reject identifiers that contain special characters or reserved operator characters.
if not IsValidIdentifier(Node.Identifier.Name) then
raise Exception.CreateFmt('Invalid identifier name: "%s".', [Node.Identifier.Name]);
slotIndex := FCurrentDescriptor.Define(Node.Identifier.Name);
(Node.Identifier as TIdentifierNode).Address := TResolvedAddress.Create(akLocalOrParent, 0, slotIndex);
+16
View File
@@ -39,6 +39,7 @@ type
function VisitTernaryExpression(const Node: ITernaryExpressionNode): TDataValue; override;
function VisitLambdaExpression(const Node: ILambdaExpressionNode): TDataValue; override;
function VisitFunctionCall(const Node: IFunctionCallNode): TDataValue; override;
function VisitRecurNode(const Node: IRecurNode): TDataValue; override;
function VisitBlockExpression(const Node: IBlockExpressionNode): TDataValue; override;
function VisitVariableDeclaration(const Node: IVariableDeclarationNode): TDataValue; override;
function VisitAssignment(const Node: IAssignmentNode): TDataValue; override;
@@ -230,6 +231,21 @@ begin
Result := TDataValue.Void;
end;
function TAstDumper.VisitRecurNode(const Node: IRecurNode): TDataValue;
var
arg: IAstNode;
begin
LogFmt('Recur (IsTailCall: %s)', [Node.IsTailCall.ToString(TUseBoolStrs.True)]);
Indent;
LogFmt('Arguments (%d):', [Length(Node.Arguments)]);
Indent;
for arg in Node.Arguments do
Accept(arg);
Unindent;
Unindent;
Result := TDataValue.Void;
end;
function TAstDumper.VisitBlockExpression(const Node: IBlockExpressionNode): TDataValue;
var
expr: IAstNode;
+30 -1
View File
@@ -50,6 +50,7 @@ type
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;
end;
// Registers native Delphi functions into a scope.
@@ -204,7 +205,8 @@ begin
adr.Kind := akLocalOrParent;
adr.ScopeDepth := 0;
// Capture 'Self' (slot 0) for recursion using the captured variable.
// Capture the closure itself in slot 0 for 'recur' to find it.
// The name 'Self' is no longer exposed to the user by the binder.
adr.SlotIndex := 0;
lambdaScope[adr] := TDataValue(closure);
@@ -256,6 +258,33 @@ begin
end;
end;
function TEvaluatorVisitor.VisitRecurNode(const Node: IRecurNode): TDataValue;
var
argValues: TArray<TDataValue>;
calleeAddress: TResolvedAddress;
calleeValue: TDataValue;
i: Integer;
begin
if not Node.IsTailCall then
raise EInvalidOperation.Create('Recur has to be a tail call');
// Evaluate all arguments for the recursive call.
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, which is stored by the lambda
// expression visitor in slot 0 of the current scope.
calleeAddress.Kind := akLocalOrParent;
calleeAddress.ScopeDepth := 0;
calleeAddress.SlotIndex := 0;
calleeValue := FScope[calleeAddress];
// Recur must be in a tail position, so we always return a thunk.
// The binder is responsible for enforcing the tail position rule.
Result := TDataValue.FromGeneric<TThunk>(TThunk.Create(calleeValue, argValues));
end;
function TEvaluatorVisitor.VisitAddSeriesItem(const Node: IAddSeriesItemNode): TDataValue;
var
itemValue, lookbackValue, seriesVar: TDataValue;
+44
View File
@@ -41,6 +41,7 @@ type
function JsonToTernaryExprNode(const AObj: TJSONObject): ITernaryExpressionNode;
function JsonToLambdaExprNode(const AObj: TJSONObject): ILambdaExpressionNode;
function JsonToFunctionCallNode(const AObj: TJSONObject): IFunctionCallNode;
function JsonToRecurNode(const AObj: TJSONObject): IRecurNode;
function JsonToBlockNode(const AObj: TJSONObject): IBlockExpressionNode;
function JsonToVarDeclNode(const AObj: TJSONObject): IVariableDeclarationNode;
function JsonToAssignmentNode(const AObj: TJSONObject): IAssignmentNode;
@@ -59,6 +60,7 @@ type
function VisitTernaryExpression(const Node: ITernaryExpressionNode): TDataValue;
function VisitLambdaExpression(const Node: ILambdaExpressionNode): TDataValue;
function VisitFunctionCall(const Node: IFunctionCallNode): TDataValue;
function VisitRecurNode(const Node: IRecurNode): TDataValue;
function VisitBlockExpression(const Node: IBlockExpressionNode): TDataValue;
function VisitVariableDeclaration(const Node: IVariableDeclarationNode): TDataValue;
function VisitAssignment(const Node: IAssignmentNode): TDataValue;
@@ -321,6 +323,32 @@ begin
Result := TDataValue.Void;
end;
function TJsonAstConverter.VisitRecurNode(const Node: IRecurNode): TDataValue;
var
obj: TJSONObject;
argsArray: TJSONArray;
tempArgs: TArray<TJSONObject>;
arg: IAstNode;
i: Integer;
begin
for arg in Node.Arguments do
arg.Accept(Self);
SetLength(tempArgs, Length(Node.Arguments));
for i := High(tempArgs) downto 0 do
tempArgs[i] := FJsonObjectStack.Pop;
argsArray := TJSONArray.Create;
for i := 0 to High(tempArgs) do
argsArray.Add(tempArgs[i]);
obj := TJSONObject.Create;
obj.AddPair('NodeType', TJSONString.Create('Recur'));
obj.AddPair('Arguments', argsArray);
FJsonObjectStack.Push(obj);
Result := TDataValue.Void;
end;
function TJsonAstConverter.VisitBlockExpression(const Node: IBlockExpressionNode): TDataValue;
var
obj: TJSONObject;
@@ -604,6 +632,20 @@ begin
Result := TAst.FunctionCall(callee, args);
end;
function TJsonAstConverter.JsonToRecurNode(const AObj: TJSONObject): IRecurNode;
var
args: TArray<IAstNode>;
argsArray: TJSONArray;
i: Integer;
begin
argsArray := AObj.GetValue<TJSONArray>('Arguments');
SetLength(args, argsArray.Count);
for i := 0 to argsArray.Count - 1 do
args[i] := JsonToNode(argsArray.Items[i]);
Result := TAst.Recur(args);
end;
function TJsonAstConverter.JsonToBlockNode(const AObj: TJSONObject): IBlockExpressionNode;
var
expressions: TArray<IAstNode>;
@@ -702,6 +744,8 @@ begin
Result := JsonToLambdaExprNode(obj)
else if nodeType = 'FunctionCall' then
Result := JsonToFunctionCallNode(obj)
else if nodeType = 'Recur' then
Result := JsonToRecurNode(obj)
else if nodeType = 'Block' then
Result := JsonToBlockNode(obj)
else if nodeType = 'VarDecl' then
+12
View File
@@ -28,6 +28,7 @@ type
ICreateSeriesNode = interface;
IAddSeriesItemNode = interface;
ISeriesLengthNode = interface;
IRecurNode = interface;
IExecutionScope = interface;
IScopeDescriptor = interface;
@@ -103,6 +104,7 @@ type
function VisitCreateSeries(const Node: ICreateSeriesNode): TDataValue;
function VisitAddSeriesItem(const Node: IAddSeriesItemNode): TDataValue;
function VisitSeriesLength(const Node: ISeriesLengthNode): TDataValue;
function VisitRecurNode(const Node: IRecurNode): TDataValue;
end;
IAstNode = interface(IInterface)
@@ -193,6 +195,16 @@ type
property IsTailCall: Boolean read GetIsTailCall;
end;
// A node representing a tail-recursive call.
IRecurNode = interface(IAstNode)
{$region 'private'}
function GetArguments: TArray<IAstNode>;
function GetIsTailCall: Boolean;
{$endregion}
property Arguments: TArray<IAstNode> read GetArguments;
property IsTailCall: Boolean read GetIsTailCall;
end;
IBlockExpressionNode = interface(IAstNode)
{$region 'private'}
function GetExpressions: TList<IAstNode>;
+16
View File
@@ -40,6 +40,7 @@ type
function VisitCreateSeries(const Node: ICreateSeriesNode): TDataValue;
function VisitAddSeriesItem(const Node: IAddSeriesItemNode): TDataValue;
function VisitSeriesLength(const Node: ISeriesLengthNode): TDataValue;
function VisitRecurNode(const Node: IRecurNode): TDataValue;
end;
implementation
@@ -206,6 +207,21 @@ begin
Result := TDataValue.Void;
end;
function TPrettyPrintVisitor.VisitRecurNode(const Node: IRecurNode): TDataValue;
var
arg: IAstNode;
begin
AppendLine('Recur');
Indent;
AppendLine('Arguments:');
Indent;
for arg in Node.Arguments do
arg.Accept(Self);
Unindent;
Unindent;
Result := TDataValue.Void;
end;
function TPrettyPrintVisitor.VisitBlockExpression(const Node: IBlockExpressionNode): TDataValue;
var
expr: IAstNode;
+13
View File
@@ -35,6 +35,7 @@ type
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;
end;
// Generic traverser for managing state during AST walks.
@@ -123,6 +124,18 @@ begin
end;
end;
function TAstTraverser.VisitRecurNode(const Node: IRecurNode): TDataValue;
var
arg: IAstNode;
begin
for arg in Node.Arguments do
begin
if FDone then
break;
Accept(arg);
end;
end;
function TAstTraverser.VisitIdentifier(const Node: IIdentifierNode): TDataValue;
begin
end;
+48
View File
@@ -36,6 +36,7 @@ type
class function TernaryExpr(const ACondition: IAstNode; const AThenBranch, AElseBranch: IAstNode): ITernaryExpressionNode; static;
class function LambdaExpr(const AParameters: TArray<IIdentifierNode>; const ABody: IAstNode): ILambdaExpressionNode; static;
class function FunctionCall(const ACallee: IAstNode; const AArguments: TArray<IAstNode>): IFunctionCallNode; static;
class function Recur(const AArguments: array of IAstNode): IRecurNode; static;
class function Block(const AExpressions: array of IAstNode): IBlockExpressionNode; static;
class function VarDecl(const AIdentifier: IIdentifierNode; AInitializer: IAstNode = nil): IVariableDeclarationNode; static;
class function Assign(const AIdentifier: IIdentifierNode; const AValue: IAstNode): IAssignmentNode; static;
@@ -164,6 +165,18 @@ type
property IsTailCall: Boolean read FIsTailCall write FIsTailCall;
end;
TRecurNode = class(TAstNode, IRecurNode)
private
FArguments: TArray<IAstNode>;
FIsTailCall: Boolean;
function GetArguments: TArray<IAstNode>;
function GetIsTailCall: Boolean;
public
constructor Create(const AArguments: TArray<IAstNode>);
function Accept(const Visitor: IAstVisitor): TDataValue; override;
property IsTailCall: Boolean read FIsTailCall write FIsTailCall;
end;
TBlockExpressionNode = class(TAstNode, IBlockExpressionNode)
private
FExpressions: TList<IAstNode>;
@@ -479,6 +492,30 @@ begin
Result := FIsTailCall;
end;
{ TRecurNode }
constructor TRecurNode.Create(const AArguments: TArray<IAstNode>);
begin
inherited Create;
FArguments := AArguments;
FIsTailCall := True;
end;
function TRecurNode.Accept(const Visitor: IAstVisitor): TDataValue;
begin
Result := Visitor.VisitRecurNode(Self);
end;
function TRecurNode.GetArguments: TArray<IAstNode>;
begin
Result := FArguments;
end;
function TRecurNode.GetIsTailCall: Boolean;
begin
Result := FIsTailCall;
end;
{ TBlockExpressionNode }
constructor TBlockExpressionNode.Create(const AExpressions: array of IAstNode);
@@ -760,6 +797,17 @@ begin
Result := TMemberAccessNode.Create(ABase, AMember);
end;
class function TAst.Recur(const AArguments: array of IAstNode): IRecurNode;
var
args: TArray<IAstNode>;
i: Integer;
begin
SetLength(args, Length(AArguments));
for i := 0 to High(AArguments) do
args[i] := AArguments[i];
Result := TRecurNode.Create(args);
end;
class function TAst.SeriesLength(const ASeries: IIdentifierNode): ISeriesLengthNode;
begin
Result := TSeriesLengthNode.Create(ASeries);