253 lines
6.9 KiB
ObjectPascal
253 lines
6.9 KiB
ObjectPascal
unit Test.Myc.Ast.Compiler.Macros;
|
|
|
|
interface
|
|
|
|
uses
|
|
DUnitX.TestFramework,
|
|
System.SysUtils,
|
|
System.Classes,
|
|
Myc.Ast,
|
|
Myc.Ast.Nodes,
|
|
Myc.Ast.Script,
|
|
Myc.Ast.Environment,
|
|
Myc.Data.Value,
|
|
Myc.Data.Scalar,
|
|
Myc.Ast.Compiler.Macros;
|
|
|
|
type
|
|
[TestFixture]
|
|
[IgnoreMemoryLeaks]
|
|
TMacroTests = class
|
|
private
|
|
FEnv: TAstEnvironment;
|
|
|
|
function Run(const Script: string): TDataValue;
|
|
|
|
// Returns the raw AST root node after expansion
|
|
function ExpandToNode(const Script: string): IAstNode;
|
|
|
|
// Helper to recursively find the first variable declaration in an AST subtree
|
|
function FindFirstVarName(const Node: IAstNode): string;
|
|
|
|
public
|
|
[Setup]
|
|
procedure Setup;
|
|
[TearDown]
|
|
procedure TearDown;
|
|
|
|
// --- Basic Functionality ---
|
|
|
|
[Test]
|
|
[TestCase('Identity', '(defmacro id [x] `~x), (id 42), 42')]
|
|
[TestCase('Constant', '(defmacro c [] `100), (c), 100')]
|
|
[TestCase('SimpleAdd', '(defmacro add [a b] `(+ ~a ~b)), (add 10 20), 30')]
|
|
procedure Test_Basic_Expansion(const MacroDef, Call, ExpectedResult: string);
|
|
|
|
// --- Quasiquoting & Unquoting ---
|
|
|
|
[Test]
|
|
procedure Test_Quasiquote_Literal;
|
|
|
|
[Test]
|
|
procedure Test_Unquote_Complex_Expression;
|
|
|
|
// --- Hygiene (The Critical Checks) ---
|
|
|
|
[Test]
|
|
procedure Test_Hygiene_GlobalSymbol_Preserved;
|
|
|
|
[Test]
|
|
procedure Test_Hygiene_LocalVariable_Renamed;
|
|
|
|
[Test]
|
|
procedure Test_Hygiene_Parameter_Renamed;
|
|
|
|
// --- Corner Cases & Errors ---
|
|
|
|
[Test]
|
|
procedure Test_Error_Unquote_Outside_Quasiquote;
|
|
|
|
[Test]
|
|
procedure Test_Macro_Argument_Count_Mismatch;
|
|
end;
|
|
|
|
implementation
|
|
|
|
{ TMacroTests }
|
|
|
|
procedure TMacroTests.Setup;
|
|
begin
|
|
FEnv := TAstEnvironment.Construct(nil);
|
|
end;
|
|
|
|
procedure TMacroTests.TearDown;
|
|
begin
|
|
FEnv := Default(TAstEnvironment);
|
|
end;
|
|
|
|
function TMacroTests.Run(const Script: string): TDataValue;
|
|
var
|
|
node: IAstNode;
|
|
begin
|
|
node := TAstScript.Parse(Script);
|
|
Result := FEnv.Run(node);
|
|
end;
|
|
|
|
function TMacroTests.ExpandToNode(const Script: string): IAstNode;
|
|
var
|
|
node: IAstNode;
|
|
begin
|
|
node := TAstScript.Parse(Script);
|
|
Result := FEnv.Environment.ExpandMacros(node);
|
|
end;
|
|
|
|
function TMacroTests.FindFirstVarName(const Node: IAstNode): string;
|
|
var
|
|
block: IBlockExpressionNode;
|
|
child: IAstNode;
|
|
begin
|
|
Result := '';
|
|
|
|
if not Assigned(Node) then
|
|
Exit;
|
|
|
|
// Direct match
|
|
if Node.Kind = akVariableDeclaration then
|
|
Exit(Node.AsVariableDeclaration.Target.AsIdentifier.Name);
|
|
|
|
// Recursive search in Blocks
|
|
if Node.Kind = akBlockExpression then
|
|
begin
|
|
block := Node.AsBlockExpression;
|
|
for child in block.Expressions do
|
|
begin
|
|
Result := FindFirstVarName(child);
|
|
if Result <> '' then
|
|
Exit;
|
|
end;
|
|
end;
|
|
|
|
// Recursive search in MacroExpansion wrapper
|
|
if Node.Kind = akMacroExpansion then
|
|
begin
|
|
Result := FindFirstVarName(Node.AsMacroExpansion.ExpandedBody);
|
|
if Result <> '' then
|
|
Exit;
|
|
end;
|
|
end;
|
|
|
|
procedure TMacroTests.Test_Basic_Expansion(const MacroDef, Call, ExpectedResult: string);
|
|
var
|
|
script: string;
|
|
res: TDataValue;
|
|
begin
|
|
script := Format('(do %s %s)', [MacroDef, Call]);
|
|
res := Run(script);
|
|
Assert.AreEqual(Trim(ExpectedResult), res.ToString);
|
|
end;
|
|
|
|
procedure TMacroTests.Test_Quasiquote_Literal;
|
|
var
|
|
script: string;
|
|
res: TDataValue;
|
|
begin
|
|
// We execute this to verify it returns a valid AST structure (represented as TDataValue)
|
|
// Since we can't easily inspect TDataValue(IAstNode) without casting in the test,
|
|
// we just ensure it runs without error and returns something that is NOT void.
|
|
script :=
|
|
'''
|
|
(do
|
|
(def print (fn [msg] msg))
|
|
(defmacro get-code [] `(print "hello"))
|
|
(get-code))
|
|
''';
|
|
|
|
res := Run(script);
|
|
Assert.IsTrue(res.Kind = vkText, 'Quasiquote expansion should return a value');
|
|
end;
|
|
|
|
procedure TMacroTests.Test_Unquote_Complex_Expression;
|
|
var
|
|
script: string;
|
|
res: TDataValue;
|
|
begin
|
|
script := '(do ' + ' (defmacro calc [op a b] `(~op ~a ~b)) ' + ' (calc + 5 (* 2 3)))';
|
|
|
|
res := Run(script);
|
|
Assert.AreEqual(Int64(11), res.AsScalar.Value.AsInt64);
|
|
end;
|
|
|
|
procedure TMacroTests.Test_Hygiene_GlobalSymbol_Preserved;
|
|
var
|
|
script: string;
|
|
res: TDataValue;
|
|
begin
|
|
// '+' is global. It MUST NOT be renamed.
|
|
script := '(do ' + ' (defmacro inc [x] `(+ ~x 1)) ' + ' (inc 41))';
|
|
|
|
try
|
|
res := Run(script);
|
|
Assert.AreEqual(Int64(42), res.AsScalar.Value.AsInt64);
|
|
except
|
|
on E: Exception do
|
|
Assert.Fail('Hygiene check failed. Global symbol likely renamed. Error: ' + E.Message);
|
|
end;
|
|
end;
|
|
|
|
procedure TMacroTests.Test_Hygiene_LocalVariable_Renamed;
|
|
var
|
|
script: string;
|
|
expandedNode: IAstNode;
|
|
varName: string;
|
|
begin
|
|
// Local variables defined in a macro MUST be renamed.
|
|
script := '(do ' + ' (defmacro define-x [] `(do (def x 99) x)) ' + ' (define-x))';
|
|
|
|
// 1. Run first to ensure code validity (Execution logic)
|
|
var res := Run(script);
|
|
Assert.AreEqual(Int64(99), res.AsScalar.Value.AsInt64, 'Execution failed');
|
|
|
|
// 2. Inspect AST Object Graph (Structural logic)
|
|
expandedNode := ExpandToNode(script);
|
|
|
|
// We search the AST for the VariableDeclaration of "x"
|
|
varName := FindFirstVarName(expandedNode);
|
|
|
|
Assert.IsNotEmpty(varName, 'Variable declaration not found in AST');
|
|
Assert.AreNotEqual('x', varName, 'Hygiene Failure: Variable "x" was NOT renamed.');
|
|
Assert.IsTrue(varName.StartsWith('x#'), Format('Variable should be renamed to x#... but was "%s"', [varName]));
|
|
end;
|
|
|
|
procedure TMacroTests.Test_Hygiene_Parameter_Renamed;
|
|
var
|
|
script: string;
|
|
res: TDataValue;
|
|
begin
|
|
// 'temp' inside macro should be renamed to 'temp#1' to not clash with global 'temp'.
|
|
script :=
|
|
'''
|
|
(do
|
|
(defmacro swap-bad [a b]
|
|
`(do (def temp ~a) (assign ~a ~b) (assign ~b temp)))
|
|
(def temp 10)
|
|
(def other 20)
|
|
(swap-bad temp other) ; Call with 'temp' passed as 'a'
|
|
temp) ; Should hold the swapped value (20)
|
|
''';
|
|
|
|
res := Run(script);
|
|
Assert.AreEqual(Int64(20), res.AsScalar.Value.AsInt64);
|
|
end;
|
|
|
|
procedure TMacroTests.Test_Error_Unquote_Outside_Quasiquote;
|
|
begin
|
|
Assert.WillRaise(procedure begin Run('(print ~1)'); end, EMacroException, 'Unquote outside quasiquote should raise exception');
|
|
end;
|
|
|
|
procedure TMacroTests.Test_Macro_Argument_Count_Mismatch;
|
|
begin
|
|
Assert.WillRaise(procedure begin Run('(do (defmacro two [a b] `(+ ~a ~b)) (two 1))'); end, EMacroException);
|
|
end;
|
|
|
|
end.
|