Add comprehensive spec tests: closures, macros, TCO, defcomp, parser
New test files expose fundamental evaluator issues: - define doesn't create self-referencing closures (13 failures) - let doesn't isolate scope from parent env (2 failures) - set! doesn't walk scope chain for closed-over vars (3 failures) - Component calls return kwargs object instead of evaluating body (10 failures) 485/516 passing (94%). Parser tests: 100% pass. Macro tests: 96% pass. These failures map the exact work needed for tree-walk removal. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
268
spec/tests/test-macros.sx
Normal file
268
spec/tests/test-macros.sx
Normal file
@@ -0,0 +1,268 @@
|
||||
;; ==========================================================================
|
||||
;; 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
|
||||
(defmacro dotimes (binding &rest body)
|
||||
(let ((var (first binding))
|
||||
(n (first (rest binding))))
|
||||
`(let loop ((,var 0))
|
||||
(when (< ,var ,n)
|
||||
,@body
|
||||
(loop (+ ,var 1))))))
|
||||
(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))))))
|
||||
Reference in New Issue
Block a user