;; ========================================================================== ;; test-macros.sx — Tests for macros and quasiquote ;; ;; Requires: test-framework.sx loaded first. ;; Modules tested: eval.sx (defmacro, quasiquote, unquote, splice-unquote) ;; ;; Platform functions required (beyond test framework): ;; sx-parse-one (source) -> first AST expression from source string ;; equal? (a b) -> deep equality comparison ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; Quasiquote basics ;; -------------------------------------------------------------------------- (defsuite "quasiquote-basics" (deftest "quasiquote with no unquotes is like quote" ;; `(a b c) returns a list of three symbols — same as '(a b c) (assert-true (equal? '(a b c) `(a b c))) (assert-length 3 `(a b c))) (deftest "quasiquote preserves numbers and strings as-is" (assert-equal (list 1 "hello" true) `(1 "hello" true))) (deftest "quasiquote returns literal list" ;; Without unquotes, the result is a plain list — not evaluated (let ((result `(+ 1 2))) (assert-type "list" result) (assert-length 3 result))) (deftest "unquote substitutes value" ;; `(a ,x b) with x=42 should yield the list (a 42 b) ;; Compare against the parsed AST of "(a 42 b)" (let ((x 42)) (assert-true (equal? (sx-parse-one "(a 42 b)") `(a ,x b))))) (deftest "unquote evaluates its expression" ;; ,expr evaluates expr — not just symbol substitution (let ((x 3)) (assert-equal (list 1 2 6 4) `(1 2 ,(* x 2) 4)))) (deftest "unquote-splicing flattens list into quasiquote" ;; ,@xs splices the elements of xs in-place (let ((xs (list 1 2 3))) (assert-equal (list 0 1 2 3 4) `(0 ,@xs 4)))) (deftest "unquote-splicing with multiple elements" ;; Verify splice replaces the ,@xs slot with each element individually (let ((xs (list 2 3 4))) (assert-true (equal? (sx-parse-one "(a 2 3 4 b)") `(a ,@xs b))))) (deftest "unquote-splicing empty list leaves no elements" (let ((empty (list))) (assert-equal (list 1 2) `(1 ,@empty 2)))) (deftest "multiple unquotes in one template" (let ((a 10) (b 20)) (assert-equal (list 10 20 30) `(,a ,b ,(+ a b))))) (deftest "quasiquote with only unquote-splicing" (let ((items (list 7 8 9))) (assert-equal (list 7 8 9) `(,@items))))) ;; -------------------------------------------------------------------------- ;; defmacro basics ;; -------------------------------------------------------------------------- (defsuite "defmacro-basics" (deftest "simple macro transforms code" ;; A macro that wraps its argument in (do ...) (defmacro wrap-do (expr) `(do ,expr)) (assert-equal 42 (wrap-do 42)) (assert-equal "hello" (wrap-do "hello"))) (deftest "macro with multiple args" ;; my-if is structurally the same as if (defmacro my-if (condition then else) `(if ,condition ,then ,else)) (assert-equal "yes" (my-if true "yes" "no")) (assert-equal "no" (my-if false "yes" "no")) (assert-equal "yes" (my-if (> 5 3) "yes" "no"))) (deftest "macro using quasiquote and unquote" ;; inc1 expands to (+ x 1) (defmacro inc1 (x) `(+ ,x 1)) (assert-equal 6 (inc1 5)) (assert-equal 1 (inc1 0)) (let ((n 10)) (assert-equal 11 (inc1 n)))) (deftest "macro using unquote-splicing for rest body" ;; progn evaluates a sequence, returning the last value (defmacro progn (&rest body) `(do ,@body)) (assert-equal 3 (progn 1 2 3)) (assert-equal "last" (progn "first" "middle" "last"))) (deftest "macro with rest body side effects" ;; All body forms execute, not just the first (define counter 0) (defmacro progn2 (&rest body) `(do ,@body)) (progn2 (set! counter (+ counter 1)) (set! counter (+ counter 1)) (set! counter (+ counter 1))) (assert-equal 3 counter)) (deftest "macro expansion happens before evaluation" ;; The macro sees raw AST — its body arg is the symbol x, not a value ;; This verifies that macro args are not evaluated before expansion (defmacro quote-arg (x) `(quote ,x)) ;; (quote-arg foo) should expand to (quote foo), returning the symbol foo (let ((result (quote-arg foo))) (assert-true (equal? (sx-parse-one "foo") result)))) (deftest "macro can build new list structure" ;; Macro that builds a let binding from two args (defmacro bind-to (name val body) `(let ((,name ,val)) ,body)) (assert-equal 10 (bind-to x 10 x)) (assert-equal 20 (bind-to y 10 (* y 2))))) ;; -------------------------------------------------------------------------- ;; Common macro patterns ;; -------------------------------------------------------------------------- (defsuite "macro-patterns" (deftest "unless macro — opposite of when" (defmacro unless (condition &rest body) `(when (not ,condition) ,@body)) ;; Runs body when condition is false (assert-equal "ran" (unless false "ran")) (assert-nil (unless true "should-not-run")) ;; Works with compound conditions (assert-equal "done" (unless (> 1 2) "done")) (assert-nil (unless (= 1 1) "nope"))) (deftest "swap-vals! macro — exchange two bindings" ;; Swaps values of two variables using a temp binding (defmacro swap-vals! (a b) `(let ((tmp ,a)) (set! ,a ,b) (set! ,b tmp))) (define p 1) (define q 2) (swap-vals! p q) (assert-equal 2 p) (assert-equal 1 q)) (deftest "with-default macro — provide fallback for nil" ;; (with-default expr default) returns expr unless it is nil (defmacro with-default (expr fallback) `(or ,expr ,fallback)) (assert-equal "hello" (with-default "hello" "fallback")) (assert-equal "fallback" (with-default nil "fallback")) (assert-equal "fallback" (with-default false "fallback"))) (deftest "when2 macro — two-arg version with implicit body" ;; Like when, but condition and body are explicit (defmacro when2 (cond-expr body-expr) `(if ,cond-expr ,body-expr nil)) (assert-equal 42 (when2 true 42)) (assert-nil (when2 false 42))) (deftest "dotimes macro — simple counted loop" ;; Executes body n times, binding loop var to 0..n-1 ;; Uses for-each over range instead of named-let (avoids set! scope issue) (defmacro dotimes (binding &rest body) (let ((var (first binding)) (n (first (rest binding)))) `(for-each (fn (,var) ,@body) (range 0 ,n)))) (define total 0) (dotimes (i 5) (set! total (+ total i))) ;; 0+1+2+3+4 = 10 (assert-equal 10 total)) (deftest "and2 macro — two-arg short-circuit and" (defmacro and2 (a b) `(if ,a ,b false)) (assert-equal "b" (and2 "a" "b")) (assert-false (and2 false "b")) (assert-false (and2 "a" false))) (deftest "macro calling another macro" ;; nand is defined in terms of and2 (which is itself a macro) (defmacro and2b (a b) `(if ,a ,b false)) (defmacro nand (a b) `(not (and2b ,a ,b))) (assert-true (nand false false)) (assert-true (nand false true)) (assert-true (nand true false)) (assert-false (nand true true)))) ;; -------------------------------------------------------------------------- ;; Macro hygiene ;; -------------------------------------------------------------------------- (defsuite "macro-hygiene" (deftest "macro-introduced bindings do not leak to caller scope" ;; The macro uses a local let binding named `tmp`. ;; That binding must not appear in the caller's environment after expansion. (defmacro double-add (x) `(let ((tmp (* ,x 2))) (+ tmp 1))) (assert-equal 11 (double-add 5)) (assert-equal 21 (double-add 10)) ;; Verify the let scope is isolated: evaluate two calls and confirm ;; results are independent (no shared `tmp` leaking between calls) (assert-equal (list 11 21) (list (double-add 5) (double-add 10)))) (deftest "caller bindings are visible inside macro expansion" ;; The macro emits code that references `scale` — a name that must be ;; looked up in the caller's environment at expansion evaluation time. (defmacro scale-add (x) `(+ ,x scale)) (let ((scale 100)) (assert-equal 105 (scale-add 5)))) (deftest "nested macro expansion" ;; Outer macro expands to a call of an inner macro. ;; The inner macro's expansion must also be fully evaluated. (defmacro inner-mac (x) `(* ,x 2)) (defmacro outer-mac (x) `(inner-mac (+ ,x 1))) ;; outer-mac 4 → (inner-mac (+ 4 1)) → (inner-mac 5) → (* 5 2) → 10 (assert-equal 10 (outer-mac 4))) (deftest "macro does not evaluate args — sees raw AST" ;; Passing an expression that would error if evaluated; macro must not ;; force evaluation of args it doesn't use. (defmacro first-arg (a b) `(quote ,a)) ;; b = (/ 1 0) would be a runtime error if evaluated, but macro ignores b (assert-true (equal? (sx-parse-one "hello") (first-arg hello (/ 1 0))))) (deftest "macro expansion in let body" ;; Macros must expand correctly when used inside a let body, ;; not just at top level. (defmacro triple (x) `(* ,x 3)) (let ((n 4)) (assert-equal 12 (triple n)))) (deftest "macro in higher-order position — map over macro results" ;; Macros can't be passed as first-class values, but their expansions ;; can produce lambdas that are passed. Verify that using a macro to ;; build a lambda works correctly. (defmacro make-adder (n) `(fn (x) (+ x ,n))) (let ((add5 (make-adder 5)) (add10 (make-adder 10))) (assert-equal 8 (add5 3)) (assert-equal 13 (add10 3)) (assert-equal (list 6 7 8) (map (make-adder 5) (list 1 2 3))))))