Add signal test suite (17/17) and Island type to evaluator
test-signals.sx: 17 tests covering signal basics (create, deref, reset!, swap!), computed (derive, update, chain), effects (run, re-run, dispose, cleanup), batch (deferred deduped notifications), and defisland (create, call, children). types.py: Island dataclass mirroring Component but for reactive boundaries. evaluator.py: sf_defisland special form, Island in call dispatch. run.py: Signal platform primitives (make-signal, tracking context, etc) and native effect/computed/batch implementations that bridge Lambda calls across the Python↔SX boundary. signals.sx: Updated batch to deduplicate subscribers across signals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -194,9 +194,20 @@
|
||||
(when (= *batch-depth* 0)
|
||||
(let ((queue *batch-queue*))
|
||||
(set! *batch-queue* (list))
|
||||
(for-each
|
||||
(fn (s) (flush-subscribers s))
|
||||
queue)))))
|
||||
;; Collect unique subscribers across all queued signals,
|
||||
;; then notify each exactly once.
|
||||
(let ((seen (list))
|
||||
(pending (list)))
|
||||
(for-each
|
||||
(fn (s)
|
||||
(for-each
|
||||
(fn (sub)
|
||||
(when (not (contains? seen sub))
|
||||
(append! seen sub)
|
||||
(append! pending sub)))
|
||||
(signal-subscribers s)))
|
||||
queue)
|
||||
(for-each (fn (sub) (sub)) pending))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
173
shared/sx/ref/test-signals.sx
Normal file
173
shared/sx/ref/test-signals.sx
Normal file
@@ -0,0 +1,173 @@
|
||||
;; ==========================================================================
|
||||
;; test-signals.sx — Tests for signals and reactive islands
|
||||
;;
|
||||
;; Requires: test-framework.sx loaded first.
|
||||
;; Modules tested: signals.sx, eval.sx (defisland)
|
||||
;;
|
||||
;; Note: Multi-expression lambda bodies are wrapped in (do ...) for
|
||||
;; compatibility with the hand-written evaluator which only supports
|
||||
;; single-expression lambda bodies.
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Signal creation and basic read/write
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "signal basics"
|
||||
(deftest "signal creates a reactive container"
|
||||
(let ((s (signal 42)))
|
||||
(assert-true (signal? s))
|
||||
(assert-equal 42 (deref s))))
|
||||
|
||||
(deftest "deref on non-signal passes through"
|
||||
(assert-equal 5 (deref 5))
|
||||
(assert-equal "hello" (deref "hello"))
|
||||
(assert-nil (deref nil)))
|
||||
|
||||
(deftest "reset! changes value"
|
||||
(let ((s (signal 0)))
|
||||
(reset! s 10)
|
||||
(assert-equal 10 (deref s))))
|
||||
|
||||
(deftest "reset! does not notify when value unchanged"
|
||||
(let ((s (signal 5))
|
||||
(count (signal 0)))
|
||||
(effect (fn () (do (deref s) (swap! count inc))))
|
||||
;; Effect runs once on creation → count=1
|
||||
(let ((c1 (deref count)))
|
||||
(reset! s 5) ;; same value — no notification
|
||||
(assert-equal c1 (deref count)))))
|
||||
|
||||
(deftest "swap! applies function to current value"
|
||||
(let ((s (signal 10)))
|
||||
(swap! s inc)
|
||||
(assert-equal 11 (deref s))))
|
||||
|
||||
(deftest "swap! passes extra args"
|
||||
(let ((s (signal 10)))
|
||||
(swap! s + 5)
|
||||
(assert-equal 15 (deref s)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Computed signals
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "computed"
|
||||
(deftest "computed derives initial value"
|
||||
(let ((a (signal 3))
|
||||
(b (signal 4))
|
||||
(sum (computed (fn () (+ (deref a) (deref b))))))
|
||||
(assert-equal 7 (deref sum))))
|
||||
|
||||
(deftest "computed updates when dependency changes"
|
||||
(let ((a (signal 2))
|
||||
(doubled (computed (fn () (* 2 (deref a))))))
|
||||
(assert-equal 4 (deref doubled))
|
||||
(reset! a 5)
|
||||
(assert-equal 10 (deref doubled))))
|
||||
|
||||
(deftest "computed chains"
|
||||
(let ((base (signal 1))
|
||||
(doubled (computed (fn () (* 2 (deref base)))))
|
||||
(quadrupled (computed (fn () (* 2 (deref doubled))))))
|
||||
(assert-equal 4 (deref quadrupled))
|
||||
(reset! base 3)
|
||||
(assert-equal 12 (deref quadrupled)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Effects
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "effects"
|
||||
(deftest "effect runs immediately"
|
||||
(let ((ran (signal false)))
|
||||
(effect (fn () (reset! ran true)))
|
||||
(assert-true (deref ran))))
|
||||
|
||||
(deftest "effect re-runs when dependency changes"
|
||||
(let ((source (signal "a"))
|
||||
(log (signal (list))))
|
||||
(effect (fn ()
|
||||
(swap! log (fn (l) (append l (deref source))))))
|
||||
;; Initial run logs "a"
|
||||
(assert-equal (list "a") (deref log))
|
||||
;; Change triggers re-run
|
||||
(reset! source "b")
|
||||
(assert-equal (list "a" "b") (deref log))))
|
||||
|
||||
(deftest "effect dispose stops tracking"
|
||||
(let ((source (signal 0))
|
||||
(count (signal 0)))
|
||||
(let ((dispose (effect (fn () (do
|
||||
(deref source)
|
||||
(swap! count inc))))))
|
||||
;; Effect ran once
|
||||
(assert-equal 1 (deref count))
|
||||
;; Trigger
|
||||
(reset! source 1)
|
||||
(assert-equal 2 (deref count))
|
||||
;; Dispose
|
||||
(dispose)
|
||||
;; Should NOT trigger
|
||||
(reset! source 2)
|
||||
(assert-equal 2 (deref count)))))
|
||||
|
||||
(deftest "effect cleanup runs before re-run"
|
||||
(let ((source (signal 0))
|
||||
(cleanups (signal 0)))
|
||||
(effect (fn () (do
|
||||
(deref source)
|
||||
(fn () (swap! cleanups inc))))) ;; return cleanup fn
|
||||
;; No cleanup yet (first run)
|
||||
(assert-equal 0 (deref cleanups))
|
||||
;; Change triggers cleanup of previous run
|
||||
(reset! source 1)
|
||||
(assert-equal 1 (deref cleanups)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Batch
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "batch"
|
||||
(deftest "batch defers notifications"
|
||||
(let ((a (signal 0))
|
||||
(b (signal 0))
|
||||
(run-count (signal 0)))
|
||||
(effect (fn () (do
|
||||
(deref a) (deref b)
|
||||
(swap! run-count inc))))
|
||||
;; Initial run
|
||||
(assert-equal 1 (deref run-count))
|
||||
;; Without batch: 2 writes → 2 effect runs
|
||||
;; With batch: 2 writes → 1 effect run
|
||||
(batch (fn () (do
|
||||
(reset! a 1)
|
||||
(reset! b 2))))
|
||||
;; Should be 2 (initial + 1 batched), not 3
|
||||
(assert-equal 2 (deref run-count)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; defisland
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(defsuite "defisland"
|
||||
(deftest "defisland creates an island"
|
||||
(defisland ~test-island (&key value)
|
||||
(list "island" value))
|
||||
(assert-true (island? ~test-island)))
|
||||
|
||||
(deftest "island is callable like component"
|
||||
(defisland ~greeting (&key name)
|
||||
(str "Hello, " name "!"))
|
||||
(assert-equal "Hello, World!" (~greeting :name "World")))
|
||||
|
||||
(deftest "island accepts children"
|
||||
(defisland ~wrapper (&rest children)
|
||||
(list "wrap" children))
|
||||
(assert-equal (list "wrap" (list "a" "b"))
|
||||
(~wrapper "a" "b"))))
|
||||
Reference in New Issue
Block a user