All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m49s
Three-layer architecture:
spec/ — Core language (19 files): evaluator, parser, primitives,
CEK machine, types, continuations. Host-independent.
web/ — Web framework (20 files): signals, adapters, engine,
orchestration, boot, router, CSSX. Built on core spec.
sx/ — Application (sx-docs website). Built on web framework.
Split boundary.sx into boundary-core.sx (type-of, make-env, identical?)
and boundary-web.sx (IO primitives, signals, spreads, page helpers).
Bootstrappers search spec/ → web/ → shared/sx/ref/ for .sx files.
Original files remain in shared/sx/ref/ as fallback during transition.
All 63 tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
280 lines
10 KiB
Plaintext
280 lines
10 KiB
Plaintext
;; ==========================================================================
|
|
;; 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))))
|