;; ========================================================================== ;; test-closures.sx — Comprehensive tests for closures and lexical scoping ;; ;; Requires: test-framework.sx loaded first. ;; Modules tested: eval.sx (lambda, let, define, set!) ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; Closure basics ;; -------------------------------------------------------------------------- (defsuite "closure-basics" (deftest "lambda captures variable from enclosing scope" (let ((x 10)) (let ((f (fn () x))) (assert-equal 10 (f))))) (deftest "lambda captures multiple variables" (let ((a 3) (b 4)) (let ((hyp (fn () (+ (* a a) (* b b))))) (assert-equal 25 (hyp))))) (deftest "returned lambda retains captured values" (define make-greeter (fn (greeting) (fn (name) (str greeting ", " name "!")))) (let ((hello (make-greeter "Hello"))) (assert-equal "Hello, Alice!" (hello "Alice")) (assert-equal "Hello, Bob!" (hello "Bob")))) (deftest "factory function returns independent closures" (define make-adder (fn (n) (fn (x) (+ n x)))) (let ((add5 (make-adder 5)) (add10 (make-adder 10))) (assert-equal 8 (add5 3)) (assert-equal 13 (add10 3)) (assert-equal 15 (add5 10)))) (deftest "counter via closure" (define make-counter (fn () (let ((count 0)) (fn () (set! count (+ count 1)) count)))) (let ((counter (make-counter))) (assert-equal 1 (counter)) (assert-equal 2 (counter)) (assert-equal 3 (counter)))) (deftest "closure captures value at time of creation" ;; Create closure when x=1, then rebind x to 99. ;; The closure should still see 1, not 99. (let ((x 1)) (let ((f (fn () x))) (let ((x 99)) (assert-equal 1 (f))))))) ;; -------------------------------------------------------------------------- ;; Lexical scope ;; -------------------------------------------------------------------------- (defsuite "lexical-scope" (deftest "inner binding shadows outer" (let ((x 1)) (let ((x 2)) (assert-equal 2 x)))) (deftest "shadow does not affect outer scope" (let ((x 1)) (let ((x 2)) (assert-equal 2 x)) (assert-equal 1 x))) (deftest "nested let scoping" (let ((x 1) (y 10)) (let ((x 2) (z 100)) (assert-equal 2 x) (assert-equal 10 y) (assert-equal 100 z)) (assert-equal 1 x))) (deftest "lambda body sees its own let bindings" (let ((f (fn (x) (let ((y (* x 2))) (+ x y))))) (assert-equal 9 (f 3)) (assert-equal 15 (f 5)))) (deftest "deeply nested scope chain" (let ((a 1)) (let ((b 2)) (let ((c 3)) (let ((d 4)) (assert-equal 10 (+ a b c d))))))) (deftest "lambda param shadows enclosing binding" (let ((x 99)) (let ((f (fn (x) (* x 2)))) (assert-equal 10 (f 5)) ;; outer x still visible after call (assert-equal 99 x)))) (deftest "sibling let bindings are independent" ;; Bindings in the same let do not see each other. (let ((a 1) (b 2)) (assert-equal 1 a) (assert-equal 2 b)))) ;; -------------------------------------------------------------------------- ;; Closure mutation ;; -------------------------------------------------------------------------- (defsuite "closure-mutation" (deftest "set! inside closure affects closed-over variable" (let ((x 0)) (let ((inc-x (fn () (set! x (+ x 1))))) (inc-x) (inc-x) (assert-equal 2 x)))) (deftest "multiple closures sharing same mutable variable" (let ((count 0)) (let ((inc! (fn () (set! count (+ count 1)))) (dec! (fn () (set! count (- count 1)))) (get (fn () count))) (inc!) (inc!) (inc!) (dec!) (assert-equal 2 (get))))) (deftest "set! in let binding visible to later expressions" (let ((x 1)) (set! x 42) (assert-equal 42 x))) (deftest "set! visible across multiple later expressions" (let ((result 0)) (set! result 5) (set! result (* result 2)) (assert-equal 10 result))) (deftest "map creates closures each seeing its own iteration value" ;; Each fn passed to map closes over x for that invocation. ;; The resulting list of thunks should each return the value they ;; were called with at map time. (let ((thunks (map (fn (x) (fn () x)) (list 1 2 3 4 5)))) (assert-equal 1 ((nth thunks 0))) (assert-equal 2 ((nth thunks 1))) (assert-equal 3 ((nth thunks 2))) (assert-equal 4 ((nth thunks 3))) (assert-equal 5 ((nth thunks 4)))))) ;; -------------------------------------------------------------------------- ;; Higher-order closures ;; -------------------------------------------------------------------------- (defsuite "higher-order-closures" (deftest "compose two functions" (define compose (fn (f g) (fn (x) (f (g x))))) (let ((double (fn (x) (* x 2))) (inc (fn (x) (+ x 1)))) (let ((double-then-inc (compose inc double)) (inc-then-double (compose double inc))) (assert-equal 7 (double-then-inc 3)) (assert-equal 8 (inc-then-double 3))))) (deftest "partial application via closure" ;; Manual partial — captures first arg, returns fn taking second (define partial2 (fn (f a) (fn (b) (f a b)))) (let ((add (fn (a b) (+ a b))) (mul (fn (a b) (* a b)))) (let ((add10 (partial2 add 10)) (triple (partial2 mul 3))) (assert-equal 15 (add10 5)) (assert-equal 21 (triple 7))))) (deftest "map with closure that captures outer variable" (let ((offset 100)) (let ((result (map (fn (x) (+ x offset)) (list 1 2 3)))) (assert-equal (list 101 102 103) result)))) (deftest "reduce with closure" (let ((multiplier 3)) (let ((result (reduce (fn (acc x) (+ acc (* x multiplier))) 0 (list 1 2 3 4)))) ;; (1*3 + 2*3 + 3*3 + 4*3) = 30 (assert-equal 30 result)))) (deftest "filter with closure over threshold" (let ((threshold 5)) (let ((big (filter (fn (x) (> x threshold)) (list 3 5 7 9 1 6)))) (assert-equal (list 7 9 6) big)))) (deftest "closure returned from higher-order function composes correctly" (define make-multiplier (fn (factor) (fn (x) (* x factor)))) (define pipeline (fn (fns x) (reduce (fn (acc f) (f acc)) x fns))) (let ((double (make-multiplier 2)) (triple (make-multiplier 3))) ;; 5 -> *2 -> 10 -> *3 -> 30 (assert-equal 30 (pipeline (list double triple) 5)))))