;; ========================================================================== ;; test-cek-reactive.sx — Tests for deref-as-shift reactive rendering ;; ;; Tests that (deref signal) inside a reactive-reset boundary performs ;; continuation capture: the rest of the expression becomes the subscriber. ;; ;; Requires: test-framework.sx, frames.sx, cek.sx, signals.sx loaded first. ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; Basic deref behavior through CEK ;; -------------------------------------------------------------------------- (defsuite "deref pass-through" (deftest "deref non-signal passes through" (let ((result (eval-expr-cek (sx-parse-one "(deref 42)") (test-env)))) (assert-equal 42 result))) (deftest "deref nil passes through" (let ((result (eval-expr-cek (sx-parse-one "(deref nil)") (test-env)))) (assert-nil result))) (deftest "deref string passes through" (let ((result (eval-expr-cek (sx-parse-one "(deref \"hello\")") (test-env)))) (assert-equal "hello" result)))) ;; -------------------------------------------------------------------------- ;; Deref signal without reactive-reset (no shift) ;; -------------------------------------------------------------------------- (defsuite "deref signal without reactive-reset" (deftest "deref signal returns current value" (let ((s (signal 99))) (env-set! (test-env) "test-sig" s) (let ((result (eval-expr-cek (sx-parse-one "(deref test-sig)") (test-env)))) (assert-equal 99 result)))) (deftest "deref signal in expression returns computed value" (let ((s (signal 10))) (env-set! (test-env) "test-sig" s) (let ((result (eval-expr-cek (sx-parse-one "(+ 5 (deref test-sig))") (test-env)))) (assert-equal 15 result))))) ;; -------------------------------------------------------------------------- ;; Reactive reset + deref: continuation capture ;; -------------------------------------------------------------------------- (defsuite "reactive-reset shift" (deftest "deref signal with reactive-reset captures continuation" (let ((s (signal 42)) (captured-val nil)) ;; Run CEK with a ReactiveResetFrame (let ((result (cek-run (make-cek-state (sx-parse-one "(deref test-sig)") (let ((e (env-extend (test-env)))) (env-set! e "test-sig" s) e) (list (make-reactive-reset-frame (test-env) (fn (v) (set! captured-val v)) true)))))) ;; Initial render: returns current value, update-fn NOT called (first-render) (assert-equal 42 result) (assert-nil captured-val)))) (deftest "signal change invokes subscriber with update-fn" (let ((s (signal 10)) (update-calls (list))) ;; Set up reactive-reset with tracking update-fn (scope-push! "sx-island-scope" nil) (let ((e (env-extend (test-env)))) (env-set! e "test-sig" s) (cek-run (make-cek-state (sx-parse-one "(deref test-sig)") e (list (make-reactive-reset-frame e (fn (v) (append! update-calls v)) true))))) ;; Change signal — subscriber should fire (reset! s 20) (assert-equal 1 (len update-calls)) (assert-equal 20 (first update-calls)) ;; Change again (reset! s 30) (assert-equal 2 (len update-calls)) (assert-equal 30 (nth update-calls 1)) (scope-pop! "sx-island-scope"))) (deftest "expression with deref captures rest as continuation" (let ((s (signal 5)) (update-calls (list))) (scope-push! "sx-island-scope" nil) (let ((e (env-extend (test-env)))) (env-set! e "test-sig" s) ;; (str "val=" (deref test-sig)) — continuation captures (str "val=" [HOLE]) (let ((result (cek-run (make-cek-state (sx-parse-one "(str \"val=\" (deref test-sig))") e (list (make-reactive-reset-frame e (fn (v) (append! update-calls v)) true)))))) (assert-equal "val=5" result))) ;; Change signal — should get updated string (reset! s 42) (assert-equal 1 (len update-calls)) (assert-equal "val=42" (first update-calls)) (scope-pop! "sx-island-scope")))) ;; -------------------------------------------------------------------------- ;; Disposal and cleanup ;; -------------------------------------------------------------------------- (defsuite "disposal" (deftest "scope cleanup unsubscribes continuation" (let ((s (signal 1)) (update-calls (list)) (disposers (list))) ;; Create island scope with collector that accumulates disposers (scope-push! "sx-island-scope" (fn (d) (append! disposers d))) (let ((e (env-extend (test-env)))) (env-set! e "test-sig" s) (cek-run (make-cek-state (sx-parse-one "(deref test-sig)") e (list (make-reactive-reset-frame e (fn (v) (append! update-calls v)) true))))) ;; Pop scope — call all disposers (scope-pop! "sx-island-scope") (for-each (fn (d) (cek-call d nil)) disposers) ;; Change signal — no update should fire (reset! s 999) (assert-equal 0 (len update-calls))))) ;; -------------------------------------------------------------------------- ;; cek-call integration — computed/effect use cek-call dispatch ;; -------------------------------------------------------------------------- (defsuite "cek-call dispatch" (deftest "cek-call invokes native function" (let ((log (list))) (cek-call (fn (x) (append! log x)) (list 42)) (assert-equal (list 42) log))) (deftest "cek-call invokes zero-arg lambda" (let ((result (cek-call (fn () (+ 1 2)) nil))) (assert-equal 3 result))) (deftest "cek-call with nil function returns nil" (assert-nil (cek-call nil nil))) (deftest "computed tracks deps via cek-call" (let ((s (signal 10))) (let ((c (computed (fn () (* 2 (deref s)))))) (assert-equal 20 (deref c)) (reset! s 5) (assert-equal 10 (deref c))))) (deftest "effect runs and re-runs via cek-call" (let ((s (signal "a")) (log (list))) (effect (fn () (append! log (deref s)))) (assert-equal (list "a") log) (reset! s "b") (assert-equal (list "a" "b") log))) (deftest "effect cleanup runs on re-trigger" (let ((s (signal 0)) (log (list))) (effect (fn () (let ((val (deref s))) (append! log (str "run:" val)) ;; Return cleanup function (fn () (append! log (str "clean:" val)))))) (assert-equal (list "run:0") log) (reset! s 1) (assert-equal (list "run:0" "clean:0" "run:1") log))) (deftest "batch coalesces via cek-call" (let ((s (signal 0)) (count (signal 0))) (effect (fn () (do (deref s) (swap! count inc)))) (assert-equal 1 (deref count)) (batch (fn () (reset! s 1) (reset! s 2) (reset! s 3))) ;; batch should coalesce — effect runs once, not three times (assert-equal 2 (deref count))))) ;; -------------------------------------------------------------------------- ;; CEK-native higher-order forms ;; -------------------------------------------------------------------------- (defsuite "CEK higher-order forms" (deftest "map through CEK" (let ((result (eval-expr-cek (sx-parse-one "(map (fn (x) (* x 2)) (list 1 2 3))") (test-env)))) (assert-equal (list 2 4 6) result))) (deftest "map-indexed through CEK" (let ((result (eval-expr-cek (sx-parse-one "(map-indexed (fn (i x) (+ i x)) (list 10 20 30))") (test-env)))) (assert-equal (list 10 21 32) result))) (deftest "filter through CEK" (let ((result (eval-expr-cek (sx-parse-one "(filter (fn (x) (> x 2)) (list 1 2 3 4 5))") (test-env)))) (assert-equal (list 3 4 5) result))) (deftest "reduce through CEK" (let ((result (eval-expr-cek (sx-parse-one "(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3))") (test-env)))) (assert-equal 6 result))) (deftest "some through CEK" (let ((result (eval-expr-cek (sx-parse-one "(some (fn (x) (> x 3)) (list 1 2 3 4 5))") (test-env)))) (assert-true result))) (deftest "some returns false when none match" (let ((result (eval-expr-cek (sx-parse-one "(some (fn (x) (> x 10)) (list 1 2 3))") (test-env)))) (assert-false result))) (deftest "every? through CEK" (let ((result (eval-expr-cek (sx-parse-one "(every? (fn (x) (> x 0)) (list 1 2 3))") (test-env)))) (assert-true result))) (deftest "every? returns false on first falsy" (let ((result (eval-expr-cek (sx-parse-one "(every? (fn (x) (> x 2)) (list 1 2 3))") (test-env)))) (assert-false result))) (deftest "for-each through CEK" (let ((log (list))) (env-set! (test-env) "test-log" log) (eval-expr-cek (sx-parse-one "(for-each (fn (x) (append! test-log x)) (list 1 2 3))") (test-env)) (assert-equal (list 1 2 3) log))) (deftest "map on empty list" (let ((result (eval-expr-cek (sx-parse-one "(map (fn (x) x) (list))") (test-env)))) (assert-equal (list) result))))