Named freeze scopes for serializable reactive state
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m20s

Replace raw CEK state serialization with named freeze scopes.
A freeze scope collects signals registered within it. On freeze,
signal values are serialized to SX. On thaw, values are restored.

- freeze-scope: scoped effect delimiter for signal collection
- freeze-signal: register a signal with a name in the current scope
- cek-freeze-scope / cek-thaw-scope: freeze/thaw by scope name
- freeze-to-sx / thaw-from-sx: full SX text round-trip
- cek-freeze-all / cek-thaw-all: batch operations

Also: register boolean?, symbol?, keyword? predicates in both
Python and JS platforms with proper var aliases.

Demo: counter + name input with Freeze/Thaw buttons.
Frozen SX: {:name "demo" :signals {:count 5 :name "world"}}

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 22:42:21 +00:00
parent a759f4da3b
commit cb4f4b85e5
4 changed files with 287 additions and 376 deletions

View File

@@ -542,79 +542,96 @@
(defisland ~geography/cek/freeze-demo ()
(let ((source (signal "(+ 1 (* 2 3))"))
(state-sig (signal nil))
(step-count (signal 0))
(frozen-sx (signal ""))
(thaw-result (signal "")))
(letrec
((do-parse (fn ()
(reset! frozen-sx "")
(reset! thaw-result "")
(reset! step-count 0)
(let ((parsed (sx-parse (deref source))))
(when (not (empty? parsed))
(reset! state-sig (make-cek-state (first parsed) (make-env) (list)))))))
(do-step (fn ()
(when (and (deref state-sig) (not (cek-terminal? (deref state-sig))))
(reset! state-sig (cek-step (deref state-sig)))
(swap! step-count inc))))
(do-freeze (fn ()
(when (deref state-sig)
(let ((frozen (cek-freeze (deref state-sig))))
(reset! frozen-sx (sx-serialize frozen))))))
(do-thaw (fn ()
(when (not (empty? (deref frozen-sx)))
(let ((parsed (sx-parse (deref frozen-sx))))
(when (not (empty? parsed))
(let ((thawed (cek-thaw (first parsed))))
(reset! thaw-result
(str "Resumed from step " (deref step-count) ": "
(cek-run thawed)))))))))
(do-run (fn ()
(when (deref state-sig)
(let run-loop ()
(when (not (cek-terminal? (deref state-sig)))
(do-step)
(run-loop)))))))
;; Auto-parse
(effect (fn () (do-parse)))
(div :class "space-y-4"
;; Input
(div :class "flex gap-2 items-end"
(div :class "flex-1"
(label :class "text-xs text-stone-400 block mb-1" "Expression")
(input :type "text" :bind source
:class "w-full px-3 py-1.5 rounded border border-stone-300 font-mono text-sm focus:outline-none focus:border-violet-400"
:on-change (fn (e) (do-parse))))
(div :class "flex gap-1"
(button :on-click (fn (e) (do-step))
:class "px-3 py-1.5 rounded bg-violet-500 text-white text-sm hover:bg-violet-600"
"Step")
(button :on-click (fn (e) (do-run))
:class "px-3 py-1.5 rounded bg-violet-700 text-white text-sm hover:bg-violet-800"
"Run")))
;; Step count + freeze button
(div :class "flex items-center gap-3"
(span :class "text-sm text-stone-500 font-mono"
"Step " (deref step-count))
(when (deref state-sig)
(button :on-click (fn (e) (do-freeze))
:class "px-3 py-1.5 rounded bg-amber-500 text-white text-sm hover:bg-amber-600"
"Freeze")))
;; Frozen SX output
(when (not (empty? (deref frozen-sx)))
(div :class "space-y-2"
(label :class "text-xs text-stone-400 block" "Frozen CEK state")
(pre :class "text-xs font-mono bg-stone-50 rounded p-3 overflow-x-auto whitespace-pre-wrap text-stone-700"
(deref frozen-sx))
(button :on-click (fn (e) (do-thaw))
:class "px-3 py-1.5 rounded bg-emerald-600 text-white text-sm hover:bg-emerald-700"
"Thaw \u2192 Resume")))
;; Thaw result
(when (not (empty? (deref thaw-result)))
(div :class "rounded bg-emerald-50 border border-emerald-200 p-3 text-emerald-800 font-mono text-sm"
(deref thaw-result)))))))
(let ((bg (signal "violet"))
(size (signal "text-2xl"))
(weight (signal "font-bold"))
(text (signal "the joy of sx"))
(saved (signal (list))))
;; Register in freeze scope
(freeze-scope "widget" (fn ()
(freeze-signal "bg" bg)
(freeze-signal "size" size)
(freeze-signal "weight" weight)
(freeze-signal "text" text)))
;; Preload all dynamic colour variants
(span (~cssx/tw :tokens "hidden bg-violet-50 bg-rose-50 bg-emerald-50 bg-amber-50 bg-sky-50 bg-stone-50 border-violet-200 border-rose-200 border-emerald-200 border-amber-200 border-sky-200 border-stone-200 text-violet-700 text-rose-700 text-emerald-700 text-amber-700 text-sky-700 text-stone-700"))
(div (~cssx/tw :tokens "space-y-4")
;; Live preview
(div (~cssx/tw :tokens "p-6 rounded-lg text-center transition-all border")
:class (str "bg-" (deref bg) "-50 border-" (deref bg) "-200")
(span :class (str (deref size) " " (deref weight) " text-" (deref bg) "-700")
(deref text)))
;; Controls
(div (~cssx/tw :tokens "grid grid-cols-2 gap-3")
(div
(label (~cssx/tw :tokens "text-xs text-stone-400 block mb-1") "Colour")
(div (~cssx/tw :tokens "flex gap-1")
(button :on-click (fn (e) (reset! bg "violet"))
(~cssx/tw :tokens "w-8 h-8 rounded-full")
:class (str "bg-violet-400" (if (= (deref bg) "violet") " ring-2 ring-offset-1 ring-stone-400" ""))
"")
(button :on-click (fn (e) (reset! bg "rose"))
(~cssx/tw :tokens "w-8 h-8 rounded-full")
:class (str "bg-rose-400" (if (= (deref bg) "rose") " ring-2 ring-offset-1 ring-stone-400" ""))
"")
(button :on-click (fn (e) (reset! bg "emerald"))
(~cssx/tw :tokens "w-8 h-8 rounded-full")
:class (str "bg-emerald-400" (if (= (deref bg) "emerald") " ring-2 ring-offset-1 ring-stone-400" ""))
"")
(button :on-click (fn (e) (reset! bg "amber"))
(~cssx/tw :tokens "w-8 h-8 rounded-full")
:class (str "bg-amber-400" (if (= (deref bg) "amber") " ring-2 ring-offset-1 ring-stone-400" ""))
"")
(button :on-click (fn (e) (reset! bg "sky"))
(~cssx/tw :tokens "w-8 h-8 rounded-full")
:class (str "bg-sky-400" (if (= (deref bg) "sky") " ring-2 ring-offset-1 ring-stone-400" ""))
"")
(button :on-click (fn (e) (reset! bg "stone"))
(~cssx/tw :tokens "w-8 h-8 rounded-full")
:class (str "bg-stone-400" (if (= (deref bg) "stone") " ring-2 ring-offset-1 ring-stone-400" ""))
"")))
(div
(label (~cssx/tw :tokens "text-xs text-stone-400 block mb-1") "Size")
(div (~cssx/tw :tokens "flex gap-1")
(map (fn (s)
(button :on-click (fn (e) (reset! size s))
(~cssx/tw :tokens "px-2 py-1 rounded text-xs")
:class (if (= (deref size) s)
"bg-stone-700 text-white" "bg-stone-100 text-stone-600")
s))
(list "text-sm" "text-lg" "text-2xl" "text-4xl"))))
(div
(label (~cssx/tw :tokens "text-xs text-stone-400 block mb-1") "Weight")
(div (~cssx/tw :tokens "flex gap-1")
(map (fn (w)
(button :on-click (fn (e) (reset! weight w))
(~cssx/tw :tokens "px-2 py-1 rounded text-xs")
:class (if (= (deref weight) w)
"bg-stone-700 text-white" "bg-stone-100 text-stone-600")
w))
(list "font-normal" "font-bold" "font-semibold"))))
(div
(label (~cssx/tw :tokens "text-xs text-stone-400 block mb-1") "Text")
(input :type "text" :bind text
(~cssx/tw :tokens "w-full px-2 py-1 rounded border border-stone-300 text-sm"))))
;; Freeze / Thaw
(div (~cssx/tw :tokens "flex gap-2 items-center")
(button :on-click (fn (e)
(let ((sx (freeze-to-sx "widget")))
(swap! saved (fn (l) (append l (list sx))))))
(~cssx/tw :tokens "px-3 py-1.5 rounded bg-amber-500 text-white text-sm hover:bg-amber-600")
"Save config")
(span (~cssx/tw :tokens "text-xs text-stone-400")
(str (len (deref saved)) " saved")))
;; Saved configs
(when (not (empty? (deref saved)))
(div (~cssx/tw :tokens "space-y-1")
(label (~cssx/tw :tokens "text-xs text-stone-400 block") "Saved configs")
(map-indexed (fn (i sx)
(button :on-click (fn (e) (thaw-from-sx sx))
(~cssx/tw :tokens "block w-full text-left px-2 py-1 rounded bg-stone-50 hover:bg-stone-100 font-mono text-xs text-stone-600 truncate")
(str "Config " (+ i 1))))
(deref saved)))))))
;; ---------------------------------------------------------------------------