Macro hygiene

This commit is contained in:
Michael Schimmel
2025-11-09 18:22:09 +01:00
parent d849f65f2d
commit 7aa406f27b
9 changed files with 418 additions and 95 deletions
File diff suppressed because one or more lines are too long
+41 -27
View File
@@ -29,47 +29,40 @@ object Form1: TForm1
Text = 'Test 2'
OnClick = Test2ButtonClick
end
object PrettyPrintButton: TButton
Position.X = 24.000000000000000000
Position.Y = 479.000000000000000000
TabOrder = 3
Text = 'Print'
OnClick = PrettyPrintButtonClick
end
object RecursionButton: TButton
Position.X = 24.000000000000000000
Position.Y = 92.000000000000000000
Size.Width = 80.000000000000000000
Size.Height = 22.000000000000000000
Size.PlatformDefault = False
TabOrder = 4
TabOrder = 3
Text = 'Recursion'
OnClick = RecursionButtonClick
end
object ShowScopeBox: TCheckBox
Position.X = 24.000000000000000000
Position.Y = 452.000000000000000000
TabOrder = 5
TabOrder = 4
Text = 'Scope'
end
object FibonacciButton: TButton
Position.X = 24.000000000000000000
Position.Y = 122.000000000000000000
TabOrder = 7
TabOrder = 6
Text = 'Fibonacci'
OnClick = FibonacciButtonClick
end
object CrerateTriggerExampleButton: TButton
Position.X = 24.000000000000000000
Position.Y = 168.000000000000000000
TabOrder = 8
TabOrder = 7
Text = 'TriggerTest'
OnClick = CreateTriggerExampleButtonClick
end
object DoTriggerButton: TButton
Position.X = 24.000000000000000000
Position.Y = 198.000000000000000000
TabOrder = 9
TabOrder = 8
Text = 'Trigger!'
OnClick = DoTriggerButtonClick
object DoTrigger2Button: TButton
@@ -85,7 +78,7 @@ object Form1: TForm1
Size.Width = 80.000000000000000000
Size.Height = 22.000000000000000000
Size.PlatformDefault = False
TabOrder = 10
TabOrder = 9
Text = 'Clear'
OnClick = ClearButtonClick
end
@@ -95,84 +88,84 @@ object Form1: TForm1
Size.Width = 80.000000000000000000
Size.Height = 22.000000000000000000
Size.PlatformDefault = False
TabOrder = 12
TabOrder = 11
Text = 'Series'
OnClick = SeriesTestButtonClick
end
object OHLCButton: TButton
Position.X = 24.000000000000000000
Position.Y = 288.000000000000000000
TabOrder = 13
TabOrder = 12
Text = 'OHLC'
OnClick = OHLCButtonClick
end
object DebugBox: TCheckBox
Position.X = 24.000000000000000000
Position.Y = 439.000000000000000000
TabOrder = 14
Position.Y = 436.000000000000000000
TabOrder = 13
Text = 'Debug'
OnChange = DebugBoxChange
end
object FromJSONButton: TButton
Position.X = 24.000000000000000000
Position.Y = 569.000000000000000000
TabOrder = 15
TabOrder = 14
Text = 'From JSON'
OnClick = FromJSONButtonClick
end
object ToJSONButton: TButton
Position.X = 24.000000000000000000
Position.Y = 599.000000000000000000
TabOrder = 16
TabOrder = 15
Text = 'To JSON'
OnClick = ToJSONButtonClick
end
object ExternalFuncButton: TButton
Position.X = 24.000000000000000000
Position.Y = 318.000000000000000000
TabOrder = 17
TabOrder = 16
Text = 'External Func'
OnClick = ExternalFuncButtonClick
end
object InnerLambdaButton: TButton
Position.X = 24.000000000000000000
Position.Y = 348.000000000000000000
TabOrder = 18
TabOrder = 17
Text = 'Inner Lambda'
OnClick = InnerLambdaButtonClick
end
object DumpButton: TButton
Position.X = 24.000000000000000000
Position.Y = 509.000000000000000000
TabOrder = 19
TabOrder = 18
Text = 'Dump'
OnClick = DumpButtonClick
end
object FailingUpvalueButton: TButton
Position.X = 24.000000000000000000
Position.Y = 378.000000000000000000
TabOrder = 21
TabOrder = 20
Text = 'Upvalue'
OnClick = FailingUpvalueButtonClick
end
object TailCallButten: TButton
Position.X = 24.000000000000000000
Position.Y = 406.000000000000000000
TabOrder = 22
TabOrder = 21
Text = 'Tail calls'
OnClick = TailCallButtenClick
end
object SaveUserLibButton: TButton
Position.X = 24.000000000000000000
Position.Y = 629.000000000000000000
TabOrder = 23
TabOrder = 22
Text = 'Save Lib'
OnClick = SaveUserLibButtonClick
end
object LoadUserLibButton: TButton
Position.X = 24.000000000000000000
Position.Y = 659.000000000000000000
TabOrder = 24
TabOrder = 23
Text = 'LoadLib'
OnClick = LoadUserLibButtonClick
end
@@ -187,7 +180,7 @@ object Form1: TForm1
Size.Width = 113.000000000000000000
Size.Height = 184.000000000000000000
Size.PlatformDefault = False
TabOrder = 25
TabOrder = 24
OnChange = RTLListViewChange
end
end
@@ -197,6 +190,22 @@ object Form1: TForm1
Size.Height = 609.000000000000000000
Size.PlatformDefault = False
TabOrder = 3
object CompilerStageBox: TComboBox
Anchors = [akRight, akBottom]
Items.Strings = (
'Unbound'
'Expanded'
'Bound'
'Lowered')
ItemIndex = 0
Position.X = 672.000000000000000000
Position.Y = 576.000000000000000000
Size.Width = 104.000000000000000000
Size.Height = 25.000000000000000000
Size.PlatformDefault = False
TabOrder = 0
OnChange = CompilerStageBoxChange
end
end
object Memo1: TMemo
Touch.InteractiveGestures = [Pan, LongTap, DoubleTap]
@@ -240,6 +249,11 @@ object Form1: TForm1
' (defmacro test [body] `(fn [] ~body ))'
' (defmacro test2 [body] `(fn [] ~body ))'
' (test2 "hi")'
''
' (def debug-mode :true)'
' '
' (stopwatch '
' (if debug-mode (print "Debug an")))'
')')
StyledSettings = [Size, Style, FontColor]
TextSettings.Font.Family = 'Consolas'
+30 -26
View File
@@ -39,7 +39,8 @@ uses
FMX.ListView.Appearances,
FMX.ListView.Adapters.Base,
FMX.ListView,
Myc.Ast.Environment; // Added Environment
Myc.Ast.Environment,
FMX.ListBox; // Added Environment
type
// A test record
@@ -57,7 +58,6 @@ type
Memo1: TMemo;
Test1Button: TButton;
Test2Button: TButton;
PrettyPrintButton: TButton;
RecursionButton: TButton;
ShowScopeBox: TCheckBox;
FibonacciButton: TButton;
@@ -82,8 +82,10 @@ type
LoadUserLibButton: TButton;
SaveUserLibButton: TButton;
RTLListView: TListView;
CompilerStageBox: TComboBox;
procedure InnerLambdaButtonClick(Sender: TObject);
procedure ClearButtonClick(Sender: TObject);
procedure CompilerStageBoxChange(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure CreateTriggerExampleButtonClick(Sender: TObject);
procedure DebugBoxChange(Sender: TObject);
@@ -95,7 +97,6 @@ type
procedure FibonacciButtonClick(Sender: TObject);
procedure FlowOnlyBoxChange(Sender: TObject);
procedure OHLCButtonClick(Sender: TObject);
procedure PrettyPrintButtonClick(Sender: TObject);
procedure RecursionButtonClick(Sender: TObject);
procedure SeriesTestButtonClick(Sender: TObject);
procedure Test1ButtonClick(Sender: TObject);
@@ -205,6 +206,11 @@ begin
FWorkspace.Repaint;
end;
procedure TForm1.CompilerStageBoxChange(Sender: TObject);
begin
ShowVizualization(14, 14);
end;
function TForm1.CreateEnvironment: TAstEnvironment;
begin
if DebugBox.IsChecked then
@@ -361,6 +367,7 @@ begin
.AsMacroDefinition
);
CompilerStageBox.BringToFront;
ClearButtonClick(Self);
end;
@@ -585,12 +592,6 @@ begin
try
var unboundAst := converter.Deserialize(jsonObj);
// Set strategy based on UI
if DebugBox.IsChecked then
FEnvironment.SetDebugMode(Memo1.Lines, ShowScopeBox.IsChecked)
else
FEnvironment.SetStandardMode;
// Run the full pipeline
FCurrExec := FEnvironment.Compile(unboundAst);
@@ -718,21 +719,6 @@ begin
UpdateScript;
end;
procedure TForm1.PrettyPrintButtonClick(Sender: TObject);
begin
Memo1.Lines.Clear;
Memo1.Lines.Add('--- AST Pretty Print ---');
// We print the *compiled* AST
if not Assigned(FCurrExec) then
begin
Memo1.Lines.Add('No AST has been generated yet.');
exit;
end;
Memo1.Lines.Add(TAstScript.Print(FCurrUnboundAst));
end;
procedure TForm1.RecursionButtonClick(Sender: TObject);
var
root: IAstNode;
@@ -1270,9 +1256,10 @@ begin
Memo1.Lines.Clear;
try
FCurrUnboundAst := TAstScript.Parse(ScriptMemo.Lines.Text);
try
// Execute the entire script block when it changes
var result := ExecuteAst(TAstScript.Parse(ScriptMemo.Lines.Text));
var result := ExecuteAst(FCurrUnboundAst);
Memo1.Lines.Add(Format('Script executed. Final result: %s', [result.ToString]));
finally
ShowVizualization(14, 14);
@@ -1295,7 +1282,24 @@ procedure TForm1.ShowVizualization(X, Y: Single);
begin
FWorkspace.DeleteChildren;
FWorkspace.Build(FCurrUnboundAst, TPointF.Create(X, Y));
if FCurrUnboundAst = nil then
exit;
var ast := FCurrUnboundAst;
if CompilerStageBox.ItemIndex > 0 then
ast := FEnvironment.Environment.ExpandMacros(ast);
if CompilerStageBox.ItemIndex > 1 then
begin
var desc: IScopeDescriptor;
ast := FEnvironment.Environment.Bind(ast, desc);
end;
if CompilerStageBox.ItemIndex > 2 then
ast := FEnvironment.Environment.Lower(ast);
FWorkspace.Build(ast, TPointF.Create(X, Y));
//
// if FCurrExec <> nil then
// begin
+16
View File
@@ -0,0 +1,16 @@
### Projektplan: Hybrid-Makro-System
**Datum:** 06.11.2025 14:39
#### Motivation
Die Implementierung von `stopwatch` als RTL-Funktion war unzureichend, da der Zugriff auf andere Laufzeit-Symbole (wie `print`) einen langsamen Laufzeit-Lookup (`FindSymbolAddress`) erfordert hätte. Die Umwandlung in ein Makro löst das Bindungs-Problem, wirft aber das Problem der Makro-Hygiene auf. Explizite Lösungen (wie `gensym` oder `$`-Suffixe) sind syntaktisch zu komplex, unleserlich und/oder unzureichend.
#### Ziel
Entwicklung eines impliziten, automatischen Makro-Systems. Dieses System muss **hygienisch für Definitionen** sein (um Kollisionen bei internen Variablen wie `start-time` zu verhindern), aber **unhygienisch für freie Symbole** (um kontext-abhängige Makros, die z.B. `*debug-mode*` lesen, zu ermöglichen).
Die Komplexität der Hygiene soll vollständig vom Makro-Autor in den Makro-Expander (`TExpansionVisitor`) verlagert werden.
#### Ergebnis
Ein Makro-System, bei dem:
1. **Automatische Hygiene:** Alle *Definitionen* (`def`, `fn`-Parameter etc.) innerhalb eines Makro-Templates (`quasiquote`) automatisch und eindeutig umbenannt werden (z.B. `start-time` -> `start-time_123`). Dies löst das Verschachtelungs- und Kollisionsproblem (`stopwatch` in `stopwatch`).
2. **Unhygienische Freie Symbole:** Alle *freien Symbole* (z.B. `print`, `timestamp`, `*debug-mode*`), die im Makro-Template *verwendet*, aber *nicht definiert* werden, unberührt bleiben. Diese werden (unhygienisch) vom `TAstBinder` im *Aufruf-Scope* des Benutzers aufgelöst.
+119
View File
@@ -0,0 +1,119 @@
# White Paper: A Hybrid Approach to Macro Hygiene
**Date:** 09.11.2025
**Status:** Final
## 1\. Executive Summary
The macro system is a cornerstone of the compiler, enabling powerful syntactic abstraction. However, designing a macro system requires solving the fundamental conflict between **safety** (preventing accidental variable conflicts) and **power** (allowing macros to interact with their calling context).
This document outlines the rationale for the implemented hybrid macro system. This system is designed to provide the best of both worlds without burdening the macro author with manual hygiene management (such as `gensym` or special syntax).
Our system operates on two simple, deterministic rules:
1. **Automatic Hygiene for Definitions:** All symbols *defined* within a macro template are automatically renamed to be unique, preventing conflicts with user code or nested macro calls.
2. **Unhygienic Fallback for Free Symbols:** All *free symbols* (those used but not defined within the template) are left untouched. They are resolved by the binder in the **call-site scope** (the scope where the macro was invoked).
This hybrid model ensures that internal macro variables are always safe, while simultaneously permitting powerful, context-aware macros.
-----
## 2\. The Core Challenge: Safety vs. Context
A macro, by definition, injects code into a foreign scope. This creates two distinct, opposing requirements.
### Scenario A: The Need for Safety (Hygienic Definitions)
This is the classic hygiene problem. A macro must manage its own internal state without interfering with the user's code.
Consider a simple `stopwatch` macro:
```lisp
(* Macro Definition *)
(defmacro stopwatch [body]
`(do
(def start-time (timestamp))
(def result ~body)
(print "Time: " (- (timestamp) start-time))
result))
(* User Code *)
(def start-time "Important User Data")
(stopwatch (expensive-call))
(print start-time)
```
A naive (fully unhygienic) expansion would redefine the user's `start-time` variable, corrupting their program. This is unacceptable. The macro's internal variables (`start-time`, `result`) must be **hygienic**—that is, isolated from the call-site scope.
### Scenario B: The Need for Context (Unhygienic Free Symbols)
Macros derive their power from interacting with the context in which they are called. A macro may need to read variables from the user's scope.
Consider a `debug-print` macro:
```lisp
(* Macro Definition *)
(defmacro debug-print [msg]
`(if *debug-mode*
(print msg)))
(* User Code *)
(do
(def *debug-mode* true) ; User-defined context variable
(debug-print "Test message")
)
```
In this case, the macro *must* access the user's `*debug-mode*` variable from the call-site scope. A "fully hygienic" system (which binds *all* symbols to the macro's *definition-site scope*) would fail, as it would be unable to see the user's local `*debug-mode*` variable.
-----
## 3\. Rejected Alternatives
To solve this conflict, several common designs were considered and rejected for failing one of the two core scenarios.
* **Fully Unhygienic (The `$` Suffix):** This approach makes all symbols unhygienic by default and requires the author to manually mark internal variables (e.g., `start-time$`) for special handling. This inverts the desired default (safety) and fails to solve nested macro conflicts.
* **Fully Hygienic (Strict Academic):** This system binds *all* symbols (defined or free) to the macro's definition-site scope. This perfectly solves Scenario A but makes Scenario B impossible.
* **Explicit Gensym (The Lisp Way):** This requires the macro author to manually generate unique symbols (`(let [start-sym (gensym)] ...)`). This is syntactically complex, error-prone, and places an unnecessary burden on the author.
-----
## 4\. The Implemented Solution: Hybrid Hygiene
Our system resolves the conflict by treating definitions and free variables differently. The logic is handled entirely by the macro expander, requiring no special syntax from the macro author.
### Rule 1: Automatic Renaming of Definitions
When the macro expander processes a template, it performs a pre-pass to identify all symbols being *defined*. This includes `def` forms, `fn` parameters, and `let` bindings.
* For each defined symbol (e.g., `start-time`), the expander generates a unique, internal-only name (e.g., `start-time_G123`).
* It stores this in an expansion-local rename map.
* It then replaces all occurrences of that symbol *within the template* with the new name.
This automatically and transparently solves **Scenario A**. The `stopwatch` macro's `start-time` becomes `start-time_G123` and cannot possibly conflict with the user's `start-time`. This also solves the nested macro problem, as each expansion generates new unique names.
### Rule 2: Unhygienic Fallback for Free Symbols
Any symbol in the macro template that is *not* part of a definition (a "free symbol") is left untouched by the expander.
* In `stopwatch`, the symbols `do`, `timestamp`, `print`, and `-` are free symbols.
* In `debug-print`, the symbols `if`, `*debug-mode*`, and `print` are free symbols.
The expander passes these symbols directly to the next compiler stage (the binder). The binder then resolves them, as it would any normal code, within the **call-site scope**.
This solves **Scenario B**. The binder finds `*debug-mode*` in the user's `do` block, exactly as intended.
-----
## 5\. Conclusion and Trade-Offs
This hybrid design provides "implicit hygiene" for the common case (internal variables) while defaulting to "unhygienic" behavior for external symbols, which provides maximum power and flexibility.
The primary trade-off of this design is that **typographical errors in free symbols are caught late**.
For example, if the `stopwatch` macro misspelled `print` as `prnit`:
1. The expander would see `prnit` as a free symbol (Rule 2) and leave it untouched.
2. The binder would then fail to find `prnit` in the *user's* call-site scope.
The resulting error (`Undefined symbol: prnit`) will point to the user's code where `stopwatch` was *called*, not the macro *definition*. This is a minor, acceptable trade-off for a system that achieves both safety and power without syntactic overhead.
View File
+7
View File
@@ -0,0 +1,7 @@
### Regeln zur Erzeugung eines White Paper
- Format: Markdown
- Sprache: Englisch
- Direkte Bezüge auf den Delphi-Quelltext sind verboten.
- Wir machen keinen Vorschlag zur Implementierung, sondern begründen, warum etwas bereits implementiert wurde.
+157 -4
View File
@@ -30,7 +30,18 @@ type
private
FEvaluate: TEvaluateProc;
FMacroScope: IExecutionScope;
FRenameMap: TDictionary<string, string>; // [*] Hygiene Rename Map (Rule 1)
FDefinitionDescriptor: IScopeDescriptor; // [*] Template Scope (Rule 1)
class var
FGensymCounter: Int64;
function TransformAndSpliceNodes(const ANodes: TArray<IAstNode>): TArray<IAstNode>;
// [*] Gensym helper
function Gensym(const ABaseName: string): string;
// [*] Scope management for template
procedure EnterScope;
procedure ExitScope;
protected
// Override the new IAstNode-returning methods
function VisitUnquote(const Node: IUnquoteNode): IAstNode; override;
@@ -38,8 +49,19 @@ type
function VisitFunctionCall(const Node: IFunctionCallNode): IAstNode; override;
function VisitBlockExpression(const Node: IBlockExpressionNode): IAstNode; override;
function VisitRecordLiteral(const Node: IRecordLiteralNode): IAstNode; override;
// [*] Hygiene implementation (Rule 1)
function VisitVariableDeclaration(const Node: IVariableDeclarationNode): IAstNode; override;
function VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode; override;
// [*] Hygiene implementation (Rule 1, 2, 3)
function VisitIdentifier(const Node: IIdentifierNode): IAstNode; override;
public
// [*] Updated constructor
constructor Create(const AMacroScope: IExecutionScope; const AEvaluate: TEvaluateProc);
destructor Destroy; override; // [*] Added
// [*] Updated static helper
class function Expand(const MacroScope: IExecutionScope; const RootNode: IAstNode; const AEvaluate: TEvaluateProc): IAstNode;
end;
@@ -62,6 +84,8 @@ type
function VisitMacroDefinition(const Node: IMacroDefinitionNode): IAstNode; override;
// Finds and expands macro calls
function VisitFunctionCall(const Node: IFunctionCallNode): IAstNode; override;
// [*] Do not traverse into quasiquotes (they are handled by TExpansionVisitor)
function VisitQuasiquote(const Node: IQuasiquoteNode): IAstNode; override;
function VisitUnquote(const Node: IUnquoteNode): IAstNode; override;
function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode; override;
@@ -93,6 +117,7 @@ implementation
uses
System.Generics.Defaults,
System.SyncObjs,
Myc.Ast.Compiler.Binder,
Myc.Ast.Evaluator, // Required for TEvaluatorVisitor.Execute
Myc.Data.Keyword;
@@ -104,6 +129,43 @@ begin
inherited Create;
FMacroScope := AMacroScope;
FEvaluate := AEvaluate;
// [*] Initialize hygiene tools
FRenameMap := TDictionary<string, string>.Create;
// Create a descriptor for the *template* scope, parented to nil.
// It only tracks template-internal definitions for shadowing.
FDefinitionDescriptor := TScope.CreateDescriptor(nil);
end;
destructor TExpansionVisitor.Destroy;
begin
FRenameMap.Free;
// FDefinitionDescriptor is an interface
inherited;
end;
procedure TExpansionVisitor.EnterScope;
begin
// [*] Create a new nested definition scope
FDefinitionDescriptor := TScope.CreateDescriptor(FDefinitionDescriptor);
end;
procedure TExpansionVisitor.ExitScope;
begin
// [*] Revert to parent definition scope
FDefinitionDescriptor := FDefinitionDescriptor.Parent;
end;
function TExpansionVisitor.Gensym(const ABaseName: string): string;
begin
if not FRenameMap.TryGetValue(ABaseName, Result) then
begin
var count := TInterlocked.Add(FGensymCounter, 1);
inc(count);
Result := ABaseName + '_' + count.ToString;
FRenameMap.Add(ABaseName, Result);
end;
end;
class function TExpansionVisitor.Expand(
@@ -154,7 +216,7 @@ begin
end
else
begin
// Use the IAstNode-returning Accept helper
// [*] Use the IAstNode-returning Accept helper (hygienic transform)
transformedNode := Self.Accept(node);
if Assigned(transformedNode) then
newList.Add(transformedNode);
@@ -166,12 +228,92 @@ begin
end;
end;
function TExpansionVisitor.VisitIdentifier(const Node: IIdentifierNode): IAstNode;
var
newName: string;
begin
// [*] Hygiene Rule 1: Check for local rename
// Is this symbol defined *within the template* (and thus renamed)?
if FRenameMap.TryGetValue(Node.Name, newName) then
begin
// Yes, replace it with the hygienic name (e.g. 'start-time$1')
// We use TTypes.Unknown because the Binder/TypeChecker run *after* this.
Result := TAst.Identifier(newName, TTypes.Unknown);
exit;
end;
// [*] Hygiene Rule 2 & 3: Free Symbol (Hygienic 'print' or Unhygienic '*debug-mode*')
// No, it's a free symbol. Leave it as-is.
// The TAstBinder will resolve it later, either in the
// macro's definition scope (Rule 2) or the caller's scope (Rule 3).
Result := Node;
end;
function TExpansionVisitor.VisitVariableDeclaration(const Node: IVariableDeclarationNode): IAstNode;
var
newName: string;
newIdent: IIdentifierNode;
newInit: IAstNode;
begin
// [*] Hygiene Rule 1: This is a definition within the template.
// 1. Visit initializer *before* defining the var
// (so initializer can't see the var it's defining)
newInit := Accept(Node.Initializer);
// 2. Generate hygienic name
newName := Gensym(Node.Identifier.Name);
FDefinitionDescriptor.Define(newName);
// 3. Create new identifier node (type is unknown)
newIdent := TAst.Identifier(newName, TTypes.Unknown);
// 4. Create new VarDecl node
Result := TAst.VarDecl(newIdent, newInit, TTypes.Unknown);
end;
function TExpansionVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode): IAstNode;
var
newParams: TArray<IIdentifierNode>;
newBody: IAstNode;
newName: string;
i: Integer;
begin
// [*] Hygiene Rule 1: Lambda parameters are definitions.
// 1. Enter a new definition scope for the template
EnterScope;
try
// 2. Rename parameters
SetLength(newParams, Length(Node.Parameters));
for i := 0 to High(Node.Parameters) do
begin
newName := Gensym(Node.Parameters[i].Name);
FDefinitionDescriptor.Define(newName);
newParams[i] := TAst.Identifier(newName, TTypes.Unknown);
end;
// 3. Visit body recursively in the new scope
newBody := Accept(Node.Body);
finally
// 4. Exit definition scope
ExitScope;
end;
// 5. Create new Lambda node (type is unknown)
Result := TAst.LambdaExpr(newParams, newBody, nil, nil, False, TTypes.Unknown);
end;
function TExpansionVisitor.VisitUnquote(const Node: IUnquoteNode): IAstNode;
var
value: TDataValue;
expr: IAstNode;
symbol: TResolvedSymbol;
begin
// [*] This node *stops* the hygienic transformation.
// It reverts to the previous compile-time evaluation logic.
expr := Node.Expression;
// Check if we are unquoting a macro parameter (simple identifier)
@@ -220,7 +362,9 @@ var
newArgs: TArray<IAstNode>;
transformedCallee: IAstNode;
begin
// [*] Callee must be transformed hygienically
transformedCallee := Self.Accept(Node.Callee); // Calls IAstNode helper
// [*] Arguments must be transformed and allow splicing
newArgs := TransformAndSpliceNodes(Node.Arguments);
Result := TAst.FunctionCall(transformedCallee, newArgs);
end;
@@ -229,6 +373,7 @@ function TExpansionVisitor.VisitBlockExpression(const Node: IBlockExpressionNode
var
newExprs: TArray<IAstNode>;
begin
// [*] Expressions must be transformed and allow splicing
newExprs := TransformAndSpliceNodes(Node.Expressions);
Result := TAst.Block(newExprs);
end;
@@ -237,6 +382,7 @@ function TExpansionVisitor.VisitRecordLiteral(const Node: IRecordLiteralNode): I
begin
// Record literals do not support splicing.
// We just use the default TAstTransformer implementation which visits child values.
// [*] This will correctly call VisitIdentifier for keys/values.
Result := inherited VisitRecordLiteral(Node);
end;
@@ -361,6 +507,7 @@ begin
expansionScope.Define(params[i].Name, TDataValue.FromIntf<IAstNode>(Node.Arguments[i])); // Use FromIntf
// 3. Create the evaluation callback (TEvaluateProc)
// It must use the FInitialScope (for globals) and the expansionScope (for args)
var evaluatorProc: TEvaluateProc;
evaluatorProc :=
function(const ANodeToEvaluate: IAstNode): TDataValue
@@ -371,6 +518,7 @@ begin
boundSubAst: IAstNode;
begin
// This is the compile-time evaluation (Binder + Evaluator)
// [*] It must bind against the *expansion* scope, not the initial scope
boundSubAst := TAstBinder.Bind(expansionScope.CreateDescriptor, ANodeToEvaluate, subDescriptor);
// Create eval scope from the new descriptor, parented to the expansion scope
@@ -381,6 +529,7 @@ begin
end;
// 4. Expand the macro body using the TExpansionVisitor
// [*] FInitialScope removed from call
var expandedBody := TExpansionVisitor.Expand(expansionScope, macroDef.Body.AsQuasiquote.Expression, evaluatorProc);
// 5. Wrap the result in a MacroExpansionNode (for debugging/tracing)
@@ -393,17 +542,21 @@ end;
function TMacroExpander.VisitQuasiquote(const Node: IQuasiquoteNode): IAstNode;
begin
Result := Accept(Node.Expression);
// [*] Do not traverse into nested quasiquotes.
// TExpansionVisitor handles them when (if) they are unquoted.
Result := Node;
end;
function TMacroExpander.VisitUnquote(const Node: IUnquoteNode): IAstNode;
begin
Result := Accept(Node.Expression);
// [*] Standalone unquote (outside a quasiquote) is a syntax error.
raise Exception.Create('Unquote (`~`) can only be used inside a quasiquote (` `) template.');
end;
function TMacroExpander.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): IAstNode;
begin
Result := Accept(Node.Expression);
// [*] Standalone unquote-splicing (outside a quasiquote) is a syntax error.
raise Exception.Create('Unquote-splicing (`~@`) can only be used inside a quasiquote (` `) template.');
end;
end.
+43 -33
View File
@@ -42,6 +42,10 @@ type
procedure SetExecutionStrategy(const AStrategy: IExecutionStrategy);
function ExpandMacros(const Node: IAstNode): IAstNode;
function Bind(const Node: IAstNode; out Descriptor: IScopeDescriptor): IAstNode;
function Lower(const Node: IAstNode): IAstNode;
function Compile(const ANode: IAstNode; const Params: TArray<IIdentifierNode> = []): TDataValue.TFunc;
function CreateEnvironment: IEnvironment;
@@ -73,6 +77,7 @@ type
procedure Define(const Name: String; const AScript: IAstNode);
property Environment: IEnvironment read FEnvironment;
property RootScope: IExecutionScope read GetRootScope;
property MacroRegistry: IMacroRegistry read GetMacroRegistry;
end;
@@ -140,8 +145,11 @@ type
procedure SetExecutionStrategy(const AStrategy: IExecutionStrategy);
function Compile(const ANode: IAstNode; out Descriptor: IScopeDescriptor): IAstNode; overload;
function Compile(const ANode: IAstNode; const Params: TArray<IIdentifierNode> = []): TDataValue.TFunc; overload;
function ExpandMacros(const Node: IAstNode): IAstNode;
function Bind(const Node: IAstNode; out Descriptor: IScopeDescriptor): IAstNode;
function Lower(const Node: IAstNode): IAstNode;
function Compile(const Node: IAstNode; const Params: TArray<IIdentifierNode>): TDataValue.TFunc;
end;
// --- Factory Implementations ---
@@ -299,6 +307,14 @@ begin
inherited Destroy;
end;
function TEnvironment.Bind(const Node: IAstNode; out Descriptor: IScopeDescriptor): IAstNode;
begin
var boundAst := TAstBinder.Bind(FRootScope.CreateDescriptor, Node, Descriptor);
var typedAst := TTypeChecker.CheckTypes(boundAst, Descriptor);
Result := typedAst;
end;
function TEnvironment.GetMacroRegistry: IMacroRegistry;
begin
Result := FMacroRegistry;
@@ -319,44 +335,21 @@ begin
Result := TEnvironment.Create(TScope.CreateScope(FRootScope, nil, nil), TMacroRegistryImpl.Create(FMacroRegistry), FExecutionStrategy);
end;
function TEnvironment.Compile(const ANode: IAstNode; out Descriptor: IScopeDescriptor): IAstNode;
begin
// Step 1: Expand macros
var cExecutionStrategy := FExecutionStrategy;
var expandedAst :=
TMacroExpander.ExpandMacros(
FMacroRegistry,
FRootScope,
ANode,
function(const Scope: IExecutionScope): IEvaluatorVisitor begin Result := cExecutionStrategy.CreateVisitor(Scope); end
);
// Step 2: Bind names
var boundAst := TAstBinder.Bind(FRootScope.CreateDescriptor, expandedAst, Descriptor);
// Step 3: Check types
var typedAst := TTypeChecker.CheckTypes(boundAst, Descriptor);
// Step 4: Lowering
var loweredAst := TAstLowerer.Lower(typedAst);
// Step 5: TCO
var compiledAst := TAstTCO.Optimize(loweredAst);
Result := compiledAst;
end;
function TEnvironment.Compile(const ANode: IAstNode; const Params: TArray<IIdentifierNode> = []): TDataValue.TFunc;
function TEnvironment.Compile(const Node: IAstNode; const Params: TArray<IIdentifierNode>): TDataValue.TFunc;
var
desc: IScopeDescriptor;
begin
var prg := TAst.LambdaExpr(Params, ANode);
var prg := TAst.LambdaExpr(Params, Node);
var compiled := Compile(prg, desc);
var expanded := ExpandMacros(prg);
var bound := Bind(expanded, desc);
var lowered := Lower(bound);
var tcoOptimized := TAstTCO.Optimize(lowered);
var visitor := FExecutionStrategy.CreateVisitor(desc.CreateScope(FRootScope));
var func := compiled.Accept(visitor).AsMethod;
var func := tcoOptimized.Accept(visitor).AsMethod;
Result :=
function(const Args: TArray<TDataValue>): TDataValue
@@ -366,4 +359,21 @@ begin
end;
end;
function TEnvironment.ExpandMacros(const Node: IAstNode): IAstNode;
begin
var cExecutionStrategy := FExecutionStrategy;
Result :=
TMacroExpander.ExpandMacros(
FMacroRegistry,
FRootScope,
Node,
function(const Scope: IExecutionScope): IEvaluatorVisitor begin Result := cExecutionStrategy.CreateVisitor(Scope); end
);
end;
function TEnvironment.Lower(const Node: IAstNode): IAstNode;
begin
Result := TAstLowerer.Lower(Node);
end;
end.