All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m34s
- Fix deftype tests: use (list ...) instead of bare (...) for type bodies in dict literals. CEK evaluates dict values, so bare lists are treated as function calls. Tree-walk was more permissive. - Fix dotimes macro: use for-each+range instead of named-let+set! (named-let + set! has a scope chain issue under CEK env-merge) - Remaining 6 failures are CEK multi-shot continuation limitations: k invoked multiple times, scope/provide across shift boundaries. These need frame copying for multi-shot support (future work). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
267 lines
9.8 KiB
Plaintext
267 lines
9.8 KiB
Plaintext
;; ==========================================================================
|
|
;; 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))))))
|