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; type // Exception specific to macro expansion errors EMacroException = class(EAstException); IAstMacroExpander = interface(IAstVisitor) function Execute(const RootNode: IAstNode): IAstNode; end; // Callback used by TMacroExpander to request full compilation and execution of macro arguments. TMacroEvaluatorProc = reference to function(const Scope: IExecutionScope; const Node: IAstNode): TDataValue; // Handles the expansion of the macro body template (Quasiquotes/Unquotes). TExpansionVisitor = class(TAstTransformer) private FMacroEvaluator: TMacroEvaluatorProc; FMacroScope: IExecutionScope; FRenameMap: TDictionary; class var FGensymCounter: Int64; // Helper to handle unquote-splicing (~@) within lists. // Takes a source list (e.g., Arguments) and returns a flattened array for the new node. function TransformAndSpliceNodes(const ANodes: INodeList): TArray; function Gensym(const ABaseName: string): string; protected function VisitUnquote(const Node: IUnquoteNode): IAstNode; override; function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode; override; // We override these container visits to handle Splicing manually, // bypassing the standard 1:1 list visitor. 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 AMacroEvaluator: TMacroEvaluatorProc); destructor Destroy; override; class function Expand( const MacroScope: IExecutionScope; const RootNode: IAstNode; const MacroEvaluator: TMacroEvaluatorProc ): IAstNode; end; IMacroRegistry = interface {$region 'private'} function GetParent: IMacroRegistry; {$endregion} procedure Define(const Node: IMacroDefinitionNode); function Find(const Name: string): IMacroDefinitionNode; function CreateChildRegistry: IMacroRegistry; property Parent: IMacroRegistry read GetParent; 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; 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 AMacroEvaluator: TMacroEvaluatorProc); begin inherited Create; FMacroScope := AMacroScope; FMacroEvaluator := AMacroEvaluator; FRenameMap := TDictionary.Create; end; destructor TExpansionVisitor.Destroy; begin FRenameMap.Free; inherited; end; function TExpansionVisitor.Gensym(const ABaseName: string): string; begin 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 MacroEvaluator: TMacroEvaluatorProc ): IAstNode; begin var expander := TExpansionVisitor.Create(MacroScope, MacroEvaluator); try Result := expander.Accept(RootNode); finally expander.Free; end; end; function TExpansionVisitor.TransformAndSpliceNodes(const ANodes: INodeList): TArray; var newList: TList; nodeToSplice: IAstNode; transformedNode: IAstNode; node: IAstNode; begin newList := TList.Create; try for node in ANodes do begin if node.Kind = akUnquoteSplicing then begin var spliceExpr := node.AsUnquoteSplicing.Expression; var evaluatedSpliceValue: TDataValue; try evaluatedSpliceValue := FMacroEvaluator(FMacroScope, spliceExpr); except on E: EAstException do raise; on E: Exception do raise EMacroException.Create('Error during macro splice evaluation: ' + E.Message); end; if (not evaluatedSpliceValue.IsVoid) and (evaluatedSpliceValue.Kind = vkInterface) then begin nodeToSplice := evaluatedSpliceValue.AsIntf; // Splicing implies the value must be a list-like AST structure (Block or just a List Node) // If the evaluated value is an IArgumentList, IExpressionList etc., we flatten it. if nodeToSplice.Kind = akExpressionList then begin for var subItem in nodeToSplice.AsExpressionList do newList.Add(subItem); end else if nodeToSplice.Kind = akArgumentList then begin for var subItem in nodeToSplice.AsArgumentList do newList.Add(subItem); end else if nodeToSplice.Kind = akBlockExpression then begin // Splice content of block for var subItem in nodeToSplice.AsBlockExpression.Expressions do newList.Add(subItem); end else begin // Treat as single item newList.Add(nodeToSplice); end; end else if (not evaluatedSpliceValue.IsVoid) then begin // Use Splice Node Identity for the new Constant newList.Add(TAst.Constant(evaluatedSpliceValue, node.Identity.Location)); 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 // Create new Identifier with new Name but preserve Location from original Node Result := TAst.Identifier(newName, Node.Identity.Location); exit; end; Result := Node; end; function TExpansionVisitor.VisitVariableDeclaration(const Node: IVariableDeclarationNode): IAstNode; var newTarget, newInit: IAstNode; newName: string; begin newInit := Accept(Node.Initializer); if Node.Target.Kind = akIdentifier then begin newName := Gensym(Node.Target.AsIdentifier.Name); // Use location from original target newTarget := TAst.Identifier(newName, Node.Target.Identity.Location); end else begin newTarget := Accept(Node.Target); end; Result := TAst.VarDecl(Node.Identity, newTarget, newInit, TTypes.Unknown, False); end; function TExpansionVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode; var newParams: TArray; newBody: IAstNode; newName: string; i: Integer; paramList: IParameterList; begin SetLength(newParams, Node.Parameters.Count); for i := 0 to Node.Parameters.Count - 1 do begin var param := Node.Parameters[i]; newName := Gensym(param.Name); // Use location from original param newParams[i] := TAst.Identifier(newName, param.Identity.Location); end; newBody := Accept(Node.Body); // Rebuild: Wrap array in TParameterList with original Identity paramList := TParameterList.Create(newParams, Node.Parameters.Identity); // Use transformation overload Result := TAst.LambdaExpr(Node.Identity, paramList, newBody); end; function TExpansionVisitor.VisitUnquote(const Node: IUnquoteNode): IAstNode; var value: TDataValue; expr: IAstNode; addr: TResolvedAddress; begin expr := Node.Expression; // Optimization: If unquote refers to a local macro variable directly, try to get it. 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; exit; end; end; end; try value := FMacroEvaluator(FMacroScope, expr); except on E: EAstException do raise; on E: Exception do raise EMacroException.Create('Error during macro evaluation: ' + E.Message); end; if value.Kind = vkInterface then begin Result := value.AsIntf; exit; end; if value.Kind in [vkScalar, vkText, vkVoid] then // Create Constant node, inheriting location from the Unquote node Result := TAst.Constant(value, Node.Identity.Location) else raise EMacroException.CreateFmt('Cannot unquote complex runtime value of type %s at compile time.', [value.Kind.ToString]); end; function TExpansionVisitor.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode; begin raise EMacroException.Create('Unquote-splicing (`~@`) can only appear inside a list form.'); end; function TExpansionVisitor.VisitFunctionCall(const Node: IFunctionCallNode): IAstNode; var newArgs: TArray; transformedCallee: IAstNode; argList: IArgumentList; begin transformedCallee := Self.Accept(Node.Callee); // Use splicing aware transformation for arguments (returns TArray) newArgs := TransformAndSpliceNodes(Node.Arguments); // Wrap in List container to use Transformation overload argList := TArgumentList.Create(newArgs, Node.Arguments.Identity); Result := TAst.FunctionCall(Node.Identity, transformedCallee, argList); end; function TExpansionVisitor.VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode; var newExprs: TArray; exprList: IExpressionList; begin // Use splicing aware transformation for block expressions newExprs := TransformAndSpliceNodes(Node.Expressions); // Wrap in List exprList := TExpressionList.Create(newExprs, Node.Expressions.Identity); Result := TAst.Block(Node.Identity, exprList); end; function TExpansionVisitor.VisitRecordLiteral(const Node: IRecordLiteralNode): IAstNode; begin // For now, standard transformation for records (no splicing key-values yet) 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; 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([], nil); end; function TMacroExpander.VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode; begin 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 FCurrentMacroRegistry.Define(Node); Result := Node; end; function TMacroExpander.VisitFunctionCall(const Node: IFunctionCallNode): IAstNode; var calleeIdentifier: IIdentifierNode; macroDef: IMacroDefinitionNode; i: Integer; begin 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)); var expansionScope := TScope.CreateScope(FInitialScope, nil, nil); var params := macroDef.Parameters; if Node.Arguments.Count <> params.Count then raise EMacroException.CreateFmt('Macro %s expects %d arguments.', [calleeIdentifier.Name, params.Count]); for i := 0 to params.Count - 1 do expansionScope.Define(params[i].Name, TDataValue.FromIntf(Node.Arguments[i])); var expandedBody := TExpansionVisitor.Expand(expansionScope, macroDef.Body.AsQuasiquote.Expression, FMacroEvaluator); // The MacroExpansionNode wraps the expansion, preserving the original call's identity (Source Location) // for better error reporting. var macroNode := TAst.MacroExpansionNode(Node.Identity, Node, expandedBody); Result := Self.Accept(macroNode); end; function TMacroExpander.VisitQuasiquote(const Node: IQuasiquoteNode): IAstNode; begin Result := Node; end; function TMacroExpander.VisitUnquote(const Node: IUnquoteNode): IAstNode; begin raise EMacroException.Create('Unquote (`~`) can only be used inside a quasiquote.'); end; function TMacroExpander.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode; begin raise EMacroException.Create('Unquote-splicing (`~@`) can only be used inside a quasiquote.'); end; { TMacroRegistry } constructor TMacroRegistry.Create(AParent: IMacroRegistry); begin inherited Create; FParent := AParent; FMacros := TDictionary.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.