unit Myc.Ast.MacroExpander; interface uses System.SysUtils, System.Classes, System.Generics.Collections, Myc.Data.Value, Myc.Ast.Nodes, Myc.Ast.Scope, Myc.Ast.Visitor; type // Defines the strategy for compiling and evaluating an AST node at compile time. TCompileTimeExecutor = reference to function(const ANode: IAstNode): TDataValue; IMacroExpander = interface(IAstVisitor) function Execute(const RootNode: IAstNode): IAstNode; end; // The macro expander is now a pure AST transformation engine. TMacroExpander = class(TAstTransformer, IMacroExpander) private FMacros: TDictionary; FExecutor: TCompileTimeExecutor; function ExpandNode(const ANode: IAstNode): IAstNode; protected function VisitMacroDefinition(const Node: IMacroDefinitionNode): TDataValue; override; function VisitFunctionCall(const Node: IFunctionCallNode): TDataValue; override; public constructor Create(const AExecutor: TCompileTimeExecutor); destructor Destroy; override; function Execute(const RootNode: IAstNode): IAstNode; end; implementation uses Myc.Ast; type TExpansionHelper = class(TAstTransformer) private FExpansionScope: IExecutionScope; FExpander: TMacroExpander; FExecutor: TCompileTimeExecutor; protected function VisitUnquote(const Node: IUnquoteNode): TDataValue; override; function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): TDataValue; override; function VisitFunctionCall(const Node: IFunctionCallNode): TDataValue; override; public constructor Create(AExpansionScope: IExecutionScope; AExpander: TMacroExpander; AExecutor: TCompileTimeExecutor); class function Expand( Node: IAstNode; ExpansionScope: IExecutionScope; Expander: TMacroExpander; Executor: TCompileTimeExecutor ): IAstNode; static; end; TSubstitutor = class(TAstTransformer) private FParamScope: IExecutionScope; protected function VisitIdentifier(const Node: IIdentifierNode): TDataValue; override; public constructor Create(AParamScope: IExecutionScope); end; { TMacroExpander } constructor TMacroExpander.Create(const AExecutor: TCompileTimeExecutor); begin inherited Create; Assert(Assigned(AExecutor), 'A TCompileTimeExecutor must be provided.'); FMacros := TDictionary.Create; FExecutor := AExecutor; end; destructor TMacroExpander.Destroy; begin FMacros.Free; inherited; end; function TMacroExpander.ExpandNode(const ANode: IAstNode): IAstNode; var lastNode: IAstNode; begin Result := ANode; if not Assigned(Result) then exit; repeat lastNode := Result; Result := Self.Accept(lastNode).AsIntf; until Result = lastNode; end; function TMacroExpander.Execute(const RootNode: IAstNode): IAstNode; begin FMacros.Clear; Result := ExpandNode(RootNode); end; function TMacroExpander.VisitFunctionCall(const Node: IFunctionCallNode): TDataValue; var macroDef: IMacroDefinitionNode; expandedNode: IAstNode; i: Integer; begin if not (Node.Callee is TIdentifierNode) then begin Result := inherited VisitFunctionCall(Node); exit; end; var calleeIdentifier := Node.Callee as TIdentifierNode; if FMacros.TryGetValue(calleeIdentifier.Name, macroDef) then begin var expansionScope := TAst.CreateScope(nil); // Temporary scope for parameters, no parent needed. if Length(Node.Arguments) <> Length(macroDef.Parameters) then raise Exception.CreateFmt( 'Macro %s expects %d arguments, but got %d', [macroDef.Name.Name, Length(macroDef.Parameters), Length(Node.Arguments)]); for i := 0 to High(macroDef.Parameters) do expansionScope.Define(macroDef.Parameters[i].Name, TDataValue.FromIntf(Node.Arguments[i])); if not (macroDef.Body is TQuasiquoteNode) then raise Exception.CreateFmt('Macro body for "%s" must be a quasiquoted expression.', [macroDef.Name.Name]); expandedNode := TExpansionHelper.Expand(IQuasiquoteNode(macroDef.Body).Expression, expansionScope, Self, FExecutor); Result := Self.Accept(expandedNode); end else begin Result := inherited VisitFunctionCall(Node); end; end; function TMacroExpander.VisitMacroDefinition(const Node: IMacroDefinitionNode): TDataValue; begin FMacros.AddOrSetValue(Node.Name.Name, Node); Result := TDataValue.FromIntf(TBlockExpressionNode.Create([])); end; { TExpansionHelper } constructor TExpansionHelper.Create(AExpansionScope: IExecutionScope; AExpander: TMacroExpander; AExecutor: TCompileTimeExecutor); begin inherited Create; FExpansionScope := AExpansionScope; FExpander := AExpander; FExecutor := AExecutor; end; class function TExpansionHelper.Expand( Node: IAstNode; ExpansionScope: IExecutionScope; Expander: TMacroExpander; Executor: TCompileTimeExecutor ): IAstNode; var helper: TExpansionHelper; begin helper := TExpansionHelper.Create(ExpansionScope, Expander, Executor); try Result := helper.Accept(Node).AsIntf; finally helper.Free; end; end; function TExpansionHelper.VisitFunctionCall(const Node: IFunctionCallNode): TDataValue; var newArgs: TList; transformedArgValue: TDataValue; transformedArgNode: IAstNode; transformedCallee: IAstNode; begin newArgs := TList.Create; try for var arg in Node.Arguments do begin transformedArgValue := Self.Accept(arg); if transformedArgValue.IsVoid then continue; transformedArgNode := transformedArgValue.AsIntf; if (transformedArgNode is TBlockExpressionNode) then newArgs.AddRange((transformedArgNode as TBlockExpressionNode).Expressions) else newArgs.Add(transformedArgNode); end; transformedCallee := Self.Accept(Node.Callee).AsIntf; Result := TDataValue.FromIntf(TAst.FunctionCall(transformedCallee, newArgs.ToArray)); finally newArgs.Free; end; end; function TExpansionHelper.VisitUnquote(const Node: IUnquoteNode): TDataValue; var substitutor: TSubstitutor; substitutedAst, expandedAst: IAstNode; value: TDataValue; begin // Step 1: Substitute macro parameters with their AST node values. substitutor := TSubstitutor.Create(FExpansionScope); try substitutedAst := substitutor.Execute(Node.Expression); finally substitutor.Free; end; // Step 2: Recursively expand any macros within the substituted expression. expandedAst := FExpander.ExpandNode(substitutedAst); // Step 3: Call the injected executor strategy to bind and evaluate the AST. value := FExecutor(expandedAst); // Step 4: Wrap the resulting value for insertion into the template. if value.Kind in [vkScalar, vkText] then Result := TDataValue.FromIntf(TAst.Constant(value)) else Result := value; end; function TExpansionHelper.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): TDataValue; var unquotedValue: TDataValue; unquotedNode: IAstNode; begin unquotedValue := Self.VisitUnquote(TAst.Unquote(Node.Expression)); if unquotedValue.Kind = vkInterface then unquotedNode := unquotedValue.AsIntf else unquotedNode := nil; if not (unquotedNode is TBlockExpressionNode) then raise Exception.Create('Expression inside unquote-splicing (`~@`) must evaluate to a list of nodes (a block).'); Result := unquotedValue; end; { TSubstitutor } constructor TSubstitutor.Create(AParamScope: IExecutionScope); begin inherited Create; FParamScope := AParamScope; end; function TSubstitutor.VisitIdentifier(const Node: IIdentifierNode): TDataValue; var addr: TResolvedAddress; nodeValue: TDataValue; begin addr := FParamScope.CreateDescriptor.FindSymbol(Node.Name); if (addr.Kind = akLocalOrParent) and (addr.ScopeDepth = 0) then begin nodeValue := FParamScope.Values[addr]; Result := nodeValue; exit; end; Result := inherited VisitIdentifier(Node); end; end.