Files
MycLib/Src/AST/Myc.Ast.Compiler.Macros.pas
T

523 lines
18 KiB
ObjectPascal

unit Myc.Ast.Compiler.Macros;
interface
uses
System.SysUtils,
System.Classes,
System.Generics.Collections,
Myc.Data.Scalar,
Myc.Data.Value,
Myc.Ast.Nodes,
Myc.Ast.Visitor,
Myc.Ast.Scope,
Myc.Ast.Types,
Myc.Ast,
Myc.Ast.Environment;
type
IAstMacroExpander = interface(IAstVisitor)
function Execute(const RootNode: IAstNode): IAstNode;
end;
// Callback used by TMacroExpander to request full compilation and execution of macro arguments.
// This abstracts away the specific Binder, TypeChecker, and Evaluator implementations.
TMacroEvaluatorProc = reference to function(const Scope: IExecutionScope; const Node: IAstNode): TDataValue;
// Handles the expansion of the macro body template (Quasiquotes/Unquotes).
TExpansionVisitor = class(TAstTransformer)
type
TEvaluateProc = reference to function(const Node: IAstNode): TDataValue;
private
FEvaluate: TEvaluateProc;
FMacroScope: IExecutionScope;
FRenameMap: TDictionary<string, string>;
class var
FGensymCounter: Int64;
function TransformAndSpliceNodes(const ANodes: TArray<IAstNode>): TArray<IAstNode>;
function Gensym(const ABaseName: string): string;
protected
function VisitUnquote(const Node: IUnquoteNode): IAstNode; override;
function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode; override;
function VisitFunctionCall(const Node: IFunctionCallNode): IAstNode; override;
function VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode; override;
function VisitRecordLiteral(const Node: IRecordLiteralNode): IAstNode; override;
function VisitVariableDeclaration(const Node: IVariableDeclarationNode): IAstNode; override;
function VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode; override;
function VisitIdentifier(const Node: IIdentifierNode): IAstNode; override;
public
constructor Create(const AMacroScope: IExecutionScope; const AEvaluate: TEvaluateProc);
destructor Destroy; override;
class function Expand(const MacroScope: IExecutionScope; const RootNode: IAstNode; const AEvaluate: TEvaluateProc): IAstNode;
end;
// Handles the expansion of macro calls within the AST.
TMacroExpander = class(TAstTransformer, IAstMacroExpander)
private
FInitialScope: IExecutionScope;
FCurrentMacroRegistry: IMacroRegistry;
FMacroEvaluator: TMacroEvaluatorProc;
procedure EnterMacroScope;
procedure ExitMacroScope;
protected
function VisitMacroDefinition(const Node: IMacroDefinitionNode): IAstNode; override;
function VisitFunctionCall(const Node: IFunctionCallNode): IAstNode; override;
function VisitQuasiquote(const Node: IQuasiquoteNode): IAstNode; override;
function VisitUnquote(const Node: IUnquoteNode): IAstNode; override;
function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode; override;
function VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode; override;
function VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode; override;
public
constructor Create(
const ARootRegistry: IMacroRegistry;
const AInitialScope: IExecutionScope;
const AMacroEvaluator: TMacroEvaluatorProc
);
destructor Destroy; override;
function Execute(const RootNode: IAstNode): IAstNode;
class function ExpandMacros(
const ARootRegistry: IMacroRegistry;
const AInitialScope: IExecutionScope;
const RootNode: IAstNode;
const MacroEvaluator: TMacroEvaluatorProc
): IAstNode; static;
end;
// Implementation of the macro registry interface defined in Myc.Ast.Environment.
TMacroRegistry = class(TInterfacedObject, IMacroRegistry)
private
FParent: IMacroRegistry;
FMacros: TDictionary<string, IMacroDefinitionNode>;
function GetParent: IMacroRegistry;
public
constructor Create(AParent: IMacroRegistry);
destructor Destroy; override;
procedure Define(const Node: IMacroDefinitionNode);
function Find(const Name: string): IMacroDefinitionNode;
function CreateChildRegistry: IMacroRegistry;
end;
implementation
uses
System.Generics.Defaults,
System.SyncObjs,
Myc.Data.Keyword;
{ TExpansionVisitor }
constructor TExpansionVisitor.Create(const AMacroScope: IExecutionScope; const AEvaluate: TEvaluateProc);
begin
inherited Create;
FMacroScope := AMacroScope;
FEvaluate := AEvaluate;
FRenameMap := TDictionary<string, string>.Create;
end;
destructor TExpansionVisitor.Destroy;
begin
FRenameMap.Free;
inherited;
end;
function TExpansionVisitor.Gensym(const ABaseName: string): string;
begin
// Hygienic macros: Generate unique names for identifiers introduced by the macro
// that were not captured from the arguments.
if not FRenameMap.TryGetValue(ABaseName, Result) then
begin
var count := TInterlocked.Add(FGensymCounter, 1);
if count < 0 then
count := -count;
Result := ABaseName + '#' + count.ToString;
FRenameMap.Add(ABaseName, Result);
end;
end;
class function TExpansionVisitor.Expand(
const MacroScope: IExecutionScope;
const RootNode: IAstNode;
const AEvaluate: TEvaluateProc
): IAstNode;
begin
var expander := TExpansionVisitor.Create(MacroScope, AEvaluate);
try
Result := expander.Accept(RootNode);
finally
expander.Free;
end;
end;
function TExpansionVisitor.TransformAndSpliceNodes(const ANodes: TArray<IAstNode>): TArray<IAstNode>;
var
newList: TList<IAstNode>;
nodeToSplice: IAstNode;
transformedNode: IAstNode;
begin
newList := TList<IAstNode>.Create;
try
for var node in ANodes do
begin
if node.Kind = akUnquoteSplicing then
begin
var spliceExpr := node.AsUnquoteSplicing.Expression;
var evaluatedSpliceValue := FEvaluate(spliceExpr);
if (not evaluatedSpliceValue.IsVoid) and (evaluatedSpliceValue.Kind = vkInterface) then
begin
nodeToSplice := evaluatedSpliceValue.AsIntf<IAstNode>;
// If the result is a block, flatten it into the current list
if nodeToSplice.Kind = akBlockExpression then
newList.AddRange(nodeToSplice.AsBlockExpression.Expressions)
else
newList.Add(nodeToSplice);
end
else if (not evaluatedSpliceValue.IsVoid) then
begin
// Treat scalar values as constants in the AST
newList.Add(TAst.Constant(evaluatedSpliceValue));
end;
end
else
begin
transformedNode := Self.Accept(node);
if Assigned(transformedNode) then
newList.Add(transformedNode);
end;
end;
Result := newList.ToArray;
finally
newList.Free;
end;
end;
function TExpansionVisitor.VisitIdentifier(const Node: IIdentifierNode): IAstNode;
var
newName: string;
begin
// Apply hygienic renaming
if FRenameMap.TryGetValue(Node.Name, newName) then
begin
Result := TAst.Identifier(newName, TTypes.Unknown);
exit;
end;
Result := Node;
end;
function TExpansionVisitor.VisitVariableDeclaration(const Node: IVariableDeclarationNode): IAstNode;
var
newTarget, newInit: IAstNode;
newName: string;
begin
newInit := Accept(Node.Initializer);
// Check if target is identifier before trying to rename
if Node.Target.Kind = akIdentifier then
begin
newName := Gensym(Node.Target.AsIdentifier.Name);
newTarget := TAst.Identifier(newName, TTypes.Unknown);
end
else
begin
// Recursively expand unquotes in target position (advanced usage)
newTarget := Accept(Node.Target);
end;
Result := TAst.VarDecl(newTarget, newInit, TTypes.Unknown);
end;
function TExpansionVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode;
var
newParams: TArray<IIdentifierNode>;
newBody: IAstNode;
newName: string;
i: Integer;
begin
// Parameters in a macro body must also be renamed hygienically
SetLength(newParams, Length(Node.Parameters));
for i := 0 to High(Node.Parameters) do
begin
newName := Gensym(Node.Parameters[i].Name);
newParams[i] := TAst.Identifier(newName, TTypes.Unknown);
end;
newBody := Accept(Node.Body);
Result := TAst.LambdaExpr(newParams, newBody, nil, nil, nil, False, Node.IsPure, TTypes.Unknown);
end;
function TExpansionVisitor.VisitUnquote(const Node: IUnquoteNode): IAstNode;
var
value: TDataValue;
expr: IAstNode;
addr: TResolvedAddress;
begin
expr := Node.Expression;
// Optimization: If unquoting a simple identifier that exists in the macro scope,
// try to resolve it directly to avoid unnecessary evaluation overhead.
if expr.Kind = akIdentifier then
begin
addr := FMacroScope.Resolve(expr.AsIdentifier.Name);
if (addr.Kind = akLocalOrParent) and (addr.ScopeDepth = 0) then
begin
var argValue := FMacroScope.Values[addr];
if argValue.Kind = vkInterface then
begin
Result := argValue.AsIntf<IAstNode>;
exit;
end;
end;
end;
// Evaluate the expression at compile time
value := FEvaluate(expr);
if value.Kind = vkInterface then
begin
// It returned an AST node -> Inject it
Result := value.AsIntf<IAstNode>;
exit;
end;
// It returned a value -> Wrap as Constant
if value.Kind in [vkScalar, vkText, vkVoid] then
Result := TAst.Constant(value)
else
raise Exception.CreateFmt('Cannot unquote complex runtime value of type %s at compile time.', [value.Kind.ToString]);
end;
function TExpansionVisitor.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode;
begin
// ~@ can only be handled within a list context (TransformAndSpliceNodes)
raise Exception.Create('Unquote-splicing (`~@`) can only appear inside a list form.');
end;
function TExpansionVisitor.VisitFunctionCall(const Node: IFunctionCallNode): IAstNode;
var
newArgs: TArray<IAstNode>;
transformedCallee: IAstNode;
begin
transformedCallee := Self.Accept(Node.Callee);
// Use special splicing logic for argument lists
newArgs := TransformAndSpliceNodes(Node.Arguments);
Result := TAst.FunctionCall(transformedCallee, newArgs);
end;
function TExpansionVisitor.VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode;
var
newExprs: TArray<IAstNode>;
begin
// Use special splicing logic for block expressions
newExprs := TransformAndSpliceNodes(Node.Expressions);
Result := TAst.Block(newExprs);
end;
function TExpansionVisitor.VisitRecordLiteral(const Node: IRecordLiteralNode): IAstNode;
begin
// Standard recursion for records
Result := inherited VisitRecordLiteral(Node);
end;
{ TMacroExpander }
constructor TMacroExpander.Create(
const ARootRegistry: IMacroRegistry;
const AInitialScope: IExecutionScope;
const AMacroEvaluator: TMacroEvaluatorProc
);
begin
inherited Create;
FInitialScope := AInitialScope;
FMacroEvaluator := AMacroEvaluator;
// Create a local child registry to handle scoped macro definitions (defmacro inside do)
FCurrentMacroRegistry := ARootRegistry.CreateChildRegistry;
end;
destructor TMacroExpander.Destroy;
begin
inherited Destroy;
end;
procedure TMacroExpander.EnterMacroScope;
begin
FCurrentMacroRegistry := FCurrentMacroRegistry.CreateChildRegistry;
end;
procedure TMacroExpander.ExitMacroScope;
begin
FCurrentMacroRegistry := FCurrentMacroRegistry.Parent;
end;
class function TMacroExpander.ExpandMacros(
const ARootRegistry: IMacroRegistry;
const AInitialScope: IExecutionScope;
const RootNode: IAstNode;
const MacroEvaluator: TMacroEvaluatorProc
): IAstNode;
begin
var expander := TMacroExpander.Create(ARootRegistry, AInitialScope, MacroEvaluator) as IAstMacroExpander;
Result := expander.Execute(RootNode);
end;
function TMacroExpander.Execute(const RootNode: IAstNode): IAstNode;
begin
Result := Accept(RootNode);
if not Assigned(Result) then
Result := TAst.Block([]);
end;
function TMacroExpander.VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode;
begin
// Macros defined inside a block should only be visible within that block
EnterMacroScope;
try
Result := inherited VisitBlockExpression(Node);
finally
ExitMacroScope;
end;
end;
function TMacroExpander.VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode;
begin
EnterMacroScope;
try
Result := inherited VisitLambdaExpression(Node);
finally
ExitMacroScope;
end;
end;
function TMacroExpander.VisitMacroDefinition(const Node: IMacroDefinitionNode): IAstNode;
begin
// Register the macro in the current scope
FCurrentMacroRegistry.Define(Node);
// Remove the definition from the runtime AST (it's compile-time only)
// However, for now we return the node so it can be serialized/inspected,
// but the Evaluator usually ignores it (returns Void).
Result := Node;
end;
function TMacroExpander.VisitFunctionCall(const Node: IFunctionCallNode): IAstNode;
var
calleeIdentifier: IIdentifierNode;
macroDef: IMacroDefinitionNode;
i: Integer;
begin
// Check if it is a macro call
if Node.Callee.Kind <> akIdentifier then
exit(inherited VisitFunctionCall(Node));
calleeIdentifier := Node.Callee.AsIdentifier;
macroDef := FCurrentMacroRegistry.Find(calleeIdentifier.Name);
if macroDef = nil then
exit(inherited VisitFunctionCall(Node));
// --- Macro Expansion Logic ---
// 1. Create temporary scope for macro arguments (parented to Initial/Global Scope)
// This ensures macro execution is hygienic regarding the environment it runs in.
var expansionScope := TScope.CreateScope(FInitialScope, nil, nil);
var params := macroDef.Parameters;
if Length(Node.Arguments) <> Length(params) then
raise Exception.CreateFmt('Macro %s expects %d arguments.', [calleeIdentifier.Name, Length(params)]);
// 2. Populate scope with UN-EVALUATED AST nodes (Code as Data)
for i := 0 to High(params) do
expansionScope.Define(params[i].Name, TDataValue.FromIntf<IAstNode>(Node.Arguments[i]));
// 3. Create simple callback for TExpansionVisitor
var evaluatorProc: TExpansionVisitor.TEvaluateProc :=
function(const ANodeToEvaluate: IAstNode): TDataValue
begin
// Delegate complex binding/execution logic to the environment via FMacroEvaluator
Result := FMacroEvaluator(expansionScope, ANodeToEvaluate);
end;
// 4. Expand the Macro Body
// We assume the macro body is a Quasiquote. We expand it using the visitor.
var expandedBody := TExpansionVisitor.Expand(expansionScope, macroDef.Body.AsQuasiquote.Expression, evaluatorProc);
// 5. Create a MacroExpansionNode to preserve source mapping
var macroNode := TAst.MacroExpansionNode(Node, expandedBody);
// 6. Recursively expand the result (the output of a macro might contain more macro calls)
Result := Self.Accept(macroNode);
end;
function TMacroExpander.VisitQuasiquote(const Node: IQuasiquoteNode): IAstNode;
begin
// Quasiquotes in user code are not expanded by the MacroExpander,
// they are literals.
Result := Node;
end;
function TMacroExpander.VisitUnquote(const Node: IUnquoteNode): IAstNode;
begin
// Unquotes outside of a macro definition/expansion phase are invalid syntax
raise Exception.Create('Unquote (`~`) can only be used inside a quasiquote.');
end;
function TMacroExpander.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode;
begin
raise Exception.Create('Unquote-splicing (`~@`) can only be used inside a quasiquote.');
end;
{ TMacroRegistry }
constructor TMacroRegistry.Create(AParent: IMacroRegistry);
begin
inherited Create;
FParent := AParent;
FMacros := TDictionary<string, IMacroDefinitionNode>.Create;
end;
destructor TMacroRegistry.Destroy;
begin
FMacros.Free;
inherited Destroy;
end;
function TMacroRegistry.GetParent: IMacroRegistry;
begin
Result := FParent;
end;
procedure TMacroRegistry.Define(const Node: IMacroDefinitionNode);
begin
FMacros.AddOrSetValue(Node.Name.Name, Node);
end;
function TMacroRegistry.Find(const Name: string): IMacroDefinitionNode;
var
current: IMacroRegistry;
begin
current := Self;
while Assigned(current) do
begin
if (current as TMacroRegistry).FMacros.TryGetValue(Name, Result) then
exit;
current := current.Parent;
end;
Result := nil;
end;
function TMacroRegistry.CreateChildRegistry: IMacroRegistry;
begin
Result := TMacroRegistry.Create(Self);
end;
end.