Files
rose-ash/sx/sx/geography/cek.sx
giles 71c2003a60 OCaml evaluator for page dispatch + handler aser, 83/83 Playwright tests
Major architectural change: page function dispatch and handler execution
now go through the OCaml kernel instead of the Python bootstrapped evaluator.

OCaml integration:
- Page dispatch: bridge.eval() evaluates SX URL expressions (geography, marshes, etc.)
- Handler aser: bridge.aser() serializes handler responses as SX wire format
- _ensure_components loads all .sx files into OCaml kernel (spec, web adapter, handlers)
- defhandler/defpage registered as no-op special forms so handler files load
- helper IO primitive dispatches to Python page helpers + IO handlers
- ok-raw response format for SX wire format (no double-escaping)
- Natural list serialization in eval (no (list ...) wrapper)
- Clean pipe: _read_until_ok always sends io-response on error

SX adapter (aser):
- scope-emit!/scope-peek aliases to avoid CEK special form conflict
- aser-fragment/aser-call: strings starting with "(" pass through unserialized
- Registered cond-scheme?, is-else-clause?, primitive?, get-primitive in kernel
- random-int, parse-int as kernel primitives; json-encode, into via IO bridge

Handler migration:
- All IO calls converted to (helper "name" args...) pattern
- request-arg, request-form, state-get, state-set!, now, component-source etc.
- Fixed bare (effect ...) in island bodies leaking disposer functions as text
- Fixed lower-case → lower, ~search-results → ~examples/search-results

Reactive islands:
- sx-hydrate-islands called after client-side navigation swap
- force-dispose-islands-in for outerHTML swaps (clears hydration markers)
- clear-processed! platform primitive for re-hydration

Content restructuring:
- Design, event bridge, named stores, phase 2 consolidated into reactive overview
- Marshes split into overview + 5 example sub-pages
- Nav links use sx-get/sx-target for client-side navigation

Playwright test suite (sx/tests/test_demos.py):
- 83 tests covering hypermedia demos, reactive islands, marshes, spec explorer
- Server-side rendering, handler interactions, island hydration, navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:22:51 +00:00

819 lines
42 KiB
Plaintext

;; ---------------------------------------------------------------------------
;; CEK Machine — Geography section
;; ---------------------------------------------------------------------------
;; ---------------------------------------------------------------------------
;; Island demos
;; ---------------------------------------------------------------------------
;; Counter: signal + deref in text position
(defisland ~geography/cek/demo-counter (&key initial)
(let ((count (signal (or initial 0)))
(doubled (computed (fn () (* 2 (deref count))))))
(div :class "rounded-lg border border-stone-200 p-4 space-y-2"
(div :class "flex items-center gap-3"
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
:on-click (fn (e) (swap! count dec)) "-")
(span :class "text-2xl font-bold text-violet-700 min-w-[3ch] text-center"
(deref count))
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
:on-click (fn (e) (swap! count inc)) "+"))
(p :class "text-sm text-stone-500"
(str "doubled: " (deref doubled))))))
;; Computed chain: base -> doubled -> quadrupled
(defisland ~geography/cek/demo-chain ()
(let ((base (signal 1))
(doubled (computed (fn () (* (deref base) 2))))
(quadrupled (computed (fn () (* (deref doubled) 2)))))
(div :class "rounded-lg border border-stone-200 p-4 space-y-2"
(div :class "flex items-center gap-3"
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
:on-click (fn (e) (swap! base dec)) "-")
(span :class "text-2xl font-bold text-violet-700 min-w-[3ch] text-center"
(deref base))
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
:on-click (fn (e) (swap! base inc)) "+"))
(p :class "text-sm text-stone-500"
(str "doubled: " (deref doubled) " | quadrupled: " (deref quadrupled))))))
;; Reactive attribute: (deref sig) in :class position
(defisland ~geography/cek/demo-reactive-attr ()
(let ((danger (signal false)))
(div :class "rounded-lg border border-stone-200 p-4 space-y-3"
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
:on-click (fn (e) (swap! danger not))
(if (deref danger) "Safe mode" "Danger mode"))
(div :class (str "p-3 rounded font-medium transition-colors "
(if (deref danger)
"bg-red-100 text-red-800"
"bg-green-100 text-green-800"))
(if (deref danger)
"DANGER: reactive class binding via CEK"
"SAFE: reactive class binding via CEK")))))
;; Stopwatch: effect + cleanup
(defisland ~geography/cek/demo-stopwatch ()
(let ((running (signal false))
(elapsed (signal 0))
(time-text (create-text-node "0.0s"))
(btn-text (create-text-node "Start"))
(_e1 (effect (fn ()
(when (deref running)
(let ((id (set-interval (fn () (swap! elapsed inc)) 100)))
(fn () (clear-interval id)))))))
(_e2 (effect (fn ()
(let ((e (deref elapsed)))
(dom-set-text-content time-text
(str (floor (/ e 10)) "." (mod e 10) "s"))))))
(_e3 (effect (fn ()
(dom-set-text-content btn-text
(if (deref running) "Stop" "Start"))))))
(div :class "rounded-lg border border-stone-200 p-4"
(div :class "flex items-center gap-3"
(span :class "text-2xl font-bold text-violet-700 font-mono min-w-[5ch]" time-text)
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
:on-click (fn (e) (swap! running not)) btn-text)
(button :class "px-3 py-1 rounded bg-stone-400 text-white text-sm"
:on-click (fn (e) (reset! running false) (reset! elapsed 0)) "Reset")))))
;; Batch: two signals, one notification
(defisland ~geography/cek/demo-batch ()
(let ((first-sig (signal 0))
(second-sig (signal 0))
(renders (signal 0))
(_eff (effect (fn ()
(deref first-sig) (deref second-sig)
(swap! renders inc)))))
(div :class "rounded-lg border border-stone-200 p-4 space-y-2"
(div :class "flex items-center gap-4 text-sm"
(span (str "first: " (deref first-sig)))
(span (str "second: " (deref second-sig)))
(span :class "px-2 py-0.5 rounded bg-green-100 text-green-800 text-xs font-semibold"
(str "renders: " (deref renders))))
(div :class "flex items-center gap-2"
(button :class "px-3 py-1 rounded bg-violet-600 text-white text-sm"
:on-click (fn (e)
(batch (fn ()
(swap! first-sig inc)
(swap! second-sig inc))))
"Batch +1")
(button :class "px-3 py-1 rounded bg-stone-400 text-white text-sm"
:on-click (fn (e)
(swap! first-sig inc)
(swap! second-sig inc))
"No-batch +1")))))
;; ---------------------------------------------------------------------------
;; Overview page content
;; ---------------------------------------------------------------------------
(defcomp ~geography/cek/cek-content ()
(~docs/page :title "CEK Machine"
(~docs/section :title "Three registers" :id "registers"
(p "The CEK machine makes evaluation explicit. Every step is a pure function from state to state:")
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
(li (strong "C") "ontrol — the expression being evaluated")
(li (strong "E") "nvironment — the bindings in scope")
(li (strong "K") "ontinuation — what to do with the result"))
(p "The tree-walk evaluator uses the same three things, but hides them in the call stack. The CEK makes them " (em "data") " — inspectable, serializable, capturable."))
(~docs/section :title "Why it matters" :id "why"
(p "Making the continuation explicit enables:")
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
(li (strong "Stepping") " — pause evaluation, inspect state, resume")
(li (strong "Serialization") " — save a computation mid-flight, restore later")
(li (strong "Delimited continuations") " — " (code "shift") "/" (code "reset") " capture \"the rest of this expression\" as a value")
(li (strong "Deref-as-shift") " — " (code "(deref sig)") " inside a reactive boundary captures the continuation as the subscriber")))
(~docs/section :title "Default evaluator" :id "default"
(p "CEK is the default evaluator on both client (JS) and server (Python). Every " (code "eval-expr") " call goes through " (code "cek-run") ". The tree-walk evaluator is preserved as " (code "_tree_walk_eval_expr") " for test runners that interpret " (code ".sx") " files.")
(p "The CEK is defined in two spec files:")
(ul :class "space-y-1 text-stone-600 list-disc pl-5"
(li (code "frames.sx") " — frame types (IfFrame, ArgFrame, ResetFrame, ReactiveResetFrame, ...)")
(li (code "cek.sx") " — step function, run loop, special form handlers, continuation operations")))
(~docs/section :title "Deref as shift" :id "deref-as-shift"
(p "The reactive payoff. When " (code "(deref sig)") " encounters a signal inside a " (code "reactive-reset") " boundary:")
(ol :class "space-y-1 text-stone-600 list-decimal pl-5"
(li (strong "Shift") " — capture all frames between here and the reactive-reset")
(li (strong "Subscribe") " — register the captured continuation as a signal subscriber")
(li (strong "Return") " — flow the current signal value through the rest of the expression"))
(p "When the signal changes, the captured continuation is re-invoked with the new value. The " (code "update-fn") " on the ReactiveResetFrame mutates the DOM. No explicit " (code "effect()") " wrapping needed.")
(~docs/code :code (highlight
";; User writes:\n(div :class (str \"count-\" (deref counter))\n (str \"Value: \" (deref counter)))\n\n;; CEK sees (deref counter) → signal? → reactive-reset on stack?\n;; Yes: capture (str \"count-\" [HOLE]) as continuation\n;; Register as subscriber. Return current value.\n;; When counter changes: re-invoke continuation → update DOM."
"lisp")))))
;; ---------------------------------------------------------------------------
;; CEK stepper: interactive stepping debugger
;; ---------------------------------------------------------------------------
(defisland ~geography/cek/demo-stepper (&key initial-expr)
(let ((source (signal (or initial-expr "(+ 1 (* 2 3))")))
(state (signal nil))
(steps (signal 0))
(history (signal (list)))
(error-msg (signal nil)))
;; Parse and create initial CEK state
(define start-eval
(fn ()
(reset! error-msg nil)
(reset! history (list))
(reset! steps 0)
(let ((parsed (sx-parse (deref source))))
(if (empty? parsed)
(reset! error-msg "Parse error: empty expression")
(reset! state (make-cek-state (first parsed) (make-env) (list)))))))
;; Single step
(define do-step
(fn ()
(when (and (deref state) (not (cek-terminal? (deref state))))
(let ((prev (deref state)))
(swap! history (fn (h) (append h (list prev))))
(swap! steps inc)
(reset! state (cek-step prev))))))
;; Run to completion
(define do-run
(fn ()
(when (deref state)
(let run-loop ((n 0))
(when (and (not (cek-terminal? (deref state))) (< n 200))
(do-step)
(run-loop (+ n 1)))))))
;; Reset
(define do-reset
(fn ()
(reset! state nil)
(reset! steps 0)
(reset! history (list))
(reset! error-msg nil)))
;; Format control for display
(define fmt-control
(fn (s)
(if (nil? s) "\u2014"
(let ((c (get s "control")))
(if (nil? c) "\u2014"
(sx-serialize c))))))
;; Format value
(define fmt-value
(fn (s)
(if (nil? s) "\u2014"
(let ((v (get s "value")))
(cond
(nil? v) "nil"
(callable? v) (str "\u03bb:" (or (lambda-name v) "fn"))
:else (sx-serialize v))))))
;; Format kont
(define fmt-kont
(fn (s)
(if (nil? s) "\u2014"
(let ((k (get s "kont")))
(if (empty? k) "[]"
(str "[" (join " " (map (fn (f) (get f "type")) k)) "]"))))))
;; Initialize on first render
(start-eval)
(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) (start-eval))))
(div :class "flex gap-1"
(button :on-click (fn (e) (start-eval))
:class "px-3 py-1.5 rounded bg-stone-200 text-stone-700 text-sm hover:bg-stone-300" "Reset")
(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")))
;; Error
(when (deref error-msg)
(div :class "text-red-600 text-sm" (deref error-msg)))
;; Current state
(when (deref state)
(div :class "rounded border border-stone-200 bg-white p-3 font-mono text-sm space-y-1"
(div :class "flex gap-4"
(span :class "text-stone-400 w-16" "Step")
(span :class "font-bold" (deref steps)))
(div :class "flex gap-4"
(span :class "text-stone-400 w-16" "Phase")
(span :class (str "font-bold " (if (= (get (deref state) "phase") "eval") "text-blue-600" "text-green-600"))
(get (deref state) "phase")))
(div :class "flex gap-4"
(span :class "text-violet-500 w-16" "C")
(span (fmt-control (deref state))))
(div :class "flex gap-4"
(span :class "text-amber-600 w-16" "V")
(span (fmt-value (deref state))))
(div :class "flex gap-4"
(span :class "text-emerald-600 w-16" "K")
(span (fmt-kont (deref state))))
(when (cek-terminal? (deref state))
(div :class "mt-2 pt-2 border-t border-stone-200 text-stone-800 font-bold"
(str "Result: " (sx-serialize (cek-value (deref state))))))))
;; Step history
(when (not (empty? (deref history)))
(div :class "rounded border border-stone-100 bg-stone-50 p-2"
(div :class "text-xs text-stone-400 mb-1" "History")
(div :class "space-y-0.5 font-mono text-xs max-h-48 overflow-y-auto"
(map-indexed (fn (i s)
(div :class "flex gap-2 text-stone-500"
(span :class "text-stone-300 w-6 text-right" (+ i 1))
(span :class (if (= (get s "phase") "eval") "text-blue-400" "text-green-400") (get s "phase"))
(span :class "text-violet-400 truncate" (fmt-control s))
(span :class "text-amber-400" (fmt-value s))
(span :class "text-emerald-400" (fmt-kont s))))
(deref history))))))))
;; ---------------------------------------------------------------------------
;; Render stepper: watch a component render itself, tag by tag
;;
;; Walks the SX AST depth-first. At each step, renders ONE subtree
;; via render-to-html and appends to the accumulating output.
;; The preview pane shows partial HTML building up.
;; ---------------------------------------------------------------------------
(defisland ~geography/cek/demo-render-stepper (&key initial-expr)
(let ((source (signal (or initial-expr
"(div (~cssx/tw :tokens \"p-6 rounded-lg border border-stone-200 bg-white text-center\")\n (h1 (~cssx/tw :tokens \"text-3xl font-bold mb-2\")\n (span (~cssx/tw :tokens \"text-rose-500\") \"the \")\n (span (~cssx/tw :tokens \"text-amber-500\") \"joy \")\n (span (~cssx/tw :tokens \"text-emerald-500\") \"of \")\n (span (~cssx/tw :tokens \"text-violet-600 text-4xl\") \"sx\")))")))
(steps (signal (list)))
(step-idx (signal 0))
(parsed-ok (signal false))
(error-msg (signal nil))
(dom-stack-sig (signal (list))))
(letrec
((split-tag (fn (expr result)
(cond
(not (list? expr))
(append! result {"type" "leaf" "expr" expr})
(empty? expr) nil
(not (= (type-of (first expr)) "symbol"))
(append! result {"type" "leaf" "expr" expr})
(is-html-tag? (symbol-name (first expr)))
(let ((ctag (symbol-name (first expr)))
(cargs (rest expr))
(cch (list))
(cat (list))
(spreads (list))
(ckw false))
(for-each (fn (a)
(cond
(= (type-of a) "keyword") (do (set! ckw true) (append! cat a))
ckw (do (set! ckw false) (append! cat a))
(and (list? a) (not (empty? a))
(= (type-of (first a)) "symbol")
(starts-with? (symbol-name (first a)) "~"))
(do (set! ckw false) (append! spreads a))
:else (do (set! ckw false) (append! cch a))))
cargs)
(append! result {"type" "open" "tag" ctag "attrs" cat "spreads" spreads})
(for-each (fn (c) (split-tag c result)) cch)
(append! result {"type" "close" "tag" ctag}))
:else
(append! result {"type" "expr" "expr" expr}))))
(get-preview (fn () (dom-query "[data-sx-lake=\"preview\"]")))
(get-stack (fn () (deref dom-stack-sig)))
(set-stack (fn (v) (reset! dom-stack-sig v)))
(push-stack (fn (el) (reset! dom-stack-sig (append (deref dom-stack-sig) (list el)))))
(pop-stack (fn ()
(let ((s (deref dom-stack-sig)))
(when (> (len s) 1)
(reset! dom-stack-sig (slice s 0 (- (len s) 1)))))))
(do-parse (fn ()
(reset! error-msg nil)
(reset! step-idx 0)
(reset! parsed-ok false)
(set-stack (list))
(let ((container (get-preview)))
(when container (dom-set-prop container "innerHTML" "")))
(let ((parsed (sx-parse (deref source))))
(if (empty? parsed)
(do (reset! error-msg "Parse error") (reset! steps (list)))
(let ((result (list)))
(split-tag (first parsed) result)
(reset! steps result)
(reset! parsed-ok true)
(set-stack (list (get-preview))))))))
(do-step (fn ()
(when (and (deref parsed-ok) (< (deref step-idx) (len (deref steps))))
(let ((step (nth (deref steps) (deref step-idx)))
(step-type (get step "type"))
(stack (get-stack))
(parent (if (empty? (get-stack)) (get-preview) (last (get-stack)))))
(cond
(= step-type "open")
(let ((el (dom-create-element (get step "tag") nil))
(attrs (get step "attrs"))
(spreads (or (get step "spreads") (list))))
(let loop ((i 0))
(when (< i (len attrs))
(dom-set-attr el (keyword-name (nth attrs i)) (nth attrs (+ i 1)))
(loop (+ i 2))))
(for-each (fn (sp)
(let ((result (eval-expr sp (make-env))))
(when (and result (spread? result))
(let ((sattrs (spread-attrs result)))
(for-each (fn (k)
(if (= k "class")
(dom-set-attr el "class"
(str (or (dom-get-attr el "class") "") " " (get sattrs k)))
(dom-set-attr el k (get sattrs k))))
(keys sattrs))))))
spreads)
(when parent (dom-append parent el))
(push-stack el))
(= step-type "close")
(pop-stack)
(= step-type "leaf")
(when parent
(let ((val (get step "expr")))
(dom-append parent (create-text-node (if (string? val) val (str val))))))
(= step-type "expr")
(let ((rendered (render-to-dom (get step "expr") (make-env) nil)))
(when (and parent rendered)
(dom-append parent rendered)))))
(swap! step-idx inc))))
(do-run (fn ()
(let loop ()
(when (< (deref step-idx) (len (deref steps)))
(do-step)
(loop)))))
(do-back (fn ()
(when (and (deref parsed-ok) (> (deref step-idx) 0))
(let ((target (- (deref step-idx) 1))
(container (get-preview)))
(when container (dom-set-prop container "innerHTML" ""))
(set-stack (list (get-preview)))
(reset! step-idx 0)
(for-each (fn (_) (do-step)) (slice (deref steps) 0 target)))))))
(div :class "space-y-4"
(div
(label :class "text-xs text-stone-400 block mb-1" "Component expression")
(textarea :bind source :rows 4
:class "w-full px-3 py-2 rounded border border-stone-300 font-mono text-xs focus:outline-none focus:border-violet-400"))
(div :class "flex gap-1"
(button :on-click (fn (e) (do-parse))
:class "px-3 py-1.5 rounded bg-stone-700 text-white text-sm hover:bg-stone-800" "Parse")
(button :on-click (fn (e) (do-back))
:class (str "px-3 py-1.5 rounded text-sm "
(if (and (deref parsed-ok) (> (deref step-idx) 0))
"bg-stone-200 text-stone-700 hover:bg-stone-300"
"bg-stone-100 text-stone-300 cursor-not-allowed"))
"\u25c0")
(button :on-click (fn (e) (do-step))
:class (str "px-3 py-1.5 rounded text-sm "
(if (and (deref parsed-ok) (< (deref step-idx) (len (deref steps))))
"bg-violet-500 text-white hover:bg-violet-600"
"bg-violet-200 text-violet-400 cursor-not-allowed"))
"Step \u25b6")
(button :on-click (fn (e) (do-run))
:class (str "px-3 py-1.5 rounded text-sm "
(if (deref parsed-ok)
"bg-violet-700 text-white hover:bg-violet-800"
"bg-violet-200 text-violet-400 cursor-not-allowed"))
"Run \u25b6\u25b6"))
(when (deref error-msg)
(div :class "text-red-600 text-sm" (deref error-msg)))
(when (and (deref parsed-ok) (= (deref step-idx) 0))
(div :class "text-sm text-stone-500 bg-stone-50 rounded p-2"
(str "Parsed " (len (deref steps)) " render steps. Click Step to begin.")))
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(when (deref parsed-ok)
(div :class "rounded border border-stone-200 bg-white p-3 min-h-24"
(div :class "text-xs text-stone-400 mb-2"
(str (deref step-idx) " / " (len (deref steps))
(if (= (deref step-idx) (len (deref steps))) " \u2014 complete" "")))
(div :class "space-y-0.5 font-mono text-xs max-h-64 overflow-y-auto"
(map-indexed (fn (i step)
(div :class (str "flex gap-2 px-1 rounded "
(cond
(= i (deref step-idx)) "bg-violet-100 text-violet-700 font-bold"
(< i (deref step-idx)) "text-stone-400"
:else "text-stone-300"))
(span :class "w-4 text-right"
(if (< i (deref step-idx)) "\u2713" (str (+ i 1))))
(span :class "truncate"
(let ((lbl (get step "label")))
(if lbl
(if (> (len lbl) 60) (str (slice lbl 0 57) "...") lbl)
(let ((tp (get step "type")))
(cond
(= tp "open") (str "<" (get step "tag") ">")
(= tp "close") (str "</" (get step "tag") ">")
:else (sx-serialize (get step "expr")))))))))
(deref steps)))))
(div :class "rounded border border-stone-200 p-3 min-h-24"
(div :class "text-xs text-stone-400 mb-2" "Live DOM")
(lake :id "preview")))))))
;; ---------------------------------------------------------------------------
;; CEK Freeze / Thaw — serializable computation
;; ---------------------------------------------------------------------------
(defisland ~geography/cek/content-address-demo ()
(let ((count (signal 0))
(name (signal "world"))
(cid-display (signal ""))
(cid-input (signal ""))
(cid-history (signal (list)))
(status (signal "")))
;; Register in freeze scope
(freeze-scope "ca-demo" (fn ()
(freeze-signal "count" count)
(freeze-signal "name" name)))
(div (~cssx/tw :tokens "space-y-4")
;; Interactive widget
(div (~cssx/tw :tokens "flex gap-4 items-center")
(div (~cssx/tw :tokens "flex items-center gap-2")
(button :on-click (fn (e) (swap! count dec))
(~cssx/tw :tokens "px-2 py-1 rounded bg-stone-200 text-stone-700 hover:bg-stone-300") "-")
(span (~cssx/tw :tokens "font-mono text-lg font-bold w-8 text-center") (deref count))
(button :on-click (fn (e) (swap! count inc))
(~cssx/tw :tokens "px-2 py-1 rounded bg-stone-200 text-stone-700 hover:bg-stone-300") "+"))
(input :type "text" :bind name
(~cssx/tw :tokens "px-3 py-1 rounded border border-stone-300 font-mono text-sm")))
;; Live output
(div (~cssx/tw :tokens "rounded bg-violet-50 border border-violet-200 p-3 text-violet-800")
"Hello, " (deref name) "! Count = " (deref count))
;; Content address button
(div (~cssx/tw :tokens "flex gap-2")
(button :on-click (fn (e)
(let ((cid (freeze-to-cid "ca-demo")))
(reset! cid-display cid)
(reset! status (str "Stored as " cid))
(swap! cid-history (fn (h) (append h (list cid))))))
(~cssx/tw :tokens "px-3 py-1.5 rounded bg-stone-700 text-white text-sm hover:bg-stone-800")
"Content-address"))
;; CID display
(when (not (empty? (deref cid-display)))
(div (~cssx/tw :tokens "font-mono text-sm bg-stone-50 rounded p-2 flex items-center gap-2")
(span (~cssx/tw :tokens "text-stone-400") "CID:")
(span (~cssx/tw :tokens "text-violet-700 font-bold") (deref cid-display))))
;; Status
(when (not (empty? (deref status)))
(div (~cssx/tw :tokens "text-xs text-emerald-600") (deref status)))
;; Restore from CID
(div (~cssx/tw :tokens "flex gap-2 items-end")
(div (~cssx/tw :tokens "flex-1")
(label (~cssx/tw :tokens "text-xs text-stone-400 block mb-1") "Restore from CID")
(input :type "text" :bind cid-input
(~cssx/tw :tokens "w-full px-3 py-1 rounded border border-stone-300 font-mono text-sm")))
(button :on-click (fn (e)
(if (thaw-from-cid (deref cid-input))
(reset! status (str "Restored from " (deref cid-input)))
(reset! status (str "CID not found: " (deref cid-input)))))
(~cssx/tw :tokens "px-3 py-1.5 rounded bg-emerald-600 text-white text-sm hover:bg-emerald-700")
"Restore"))
;; CID history
(when (not (empty? (deref cid-history)))
(div (~cssx/tw :tokens "space-y-1")
(label (~cssx/tw :tokens "text-xs text-stone-400 block") "History")
(map (fn (cid)
(button :on-click (fn (e)
(if (thaw-from-cid cid)
(reset! status (str "Restored from " cid))
(reset! status (str "CID not found: " cid))))
(~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")
cid))
(deref cid-history)))))))
(defcomp ~geography/cek/cek-freeze-content ()
(~docs/page :title "Freeze / Thaw"
(p :class "text-stone-500 text-sm italic mb-8"
"A computation is a value. Freeze it to an s-expression. "
"Store it, transmit it, content-address it. Thaw and resume anywhere.")
(~docs/section :title "The idea" :id "idea"
(p "The CEK machine makes evaluation explicit: every step is a pure function from state to state. "
"The state is a dict with four fields:")
(ul :class "list-disc pl-6 mb-4 space-y-1 text-stone-600"
(li (code "control") " \u2014 the expression being evaluated")
(li (code "env") " \u2014 the bindings in scope")
(li (code "kont") " \u2014 the continuation (what to do with the result)")
(li (code "phase") " \u2014 eval or continue"))
(p "Since the state is data, it can be serialized. "
(code "cek-freeze") " converts a live CEK state to pure s-expressions. "
(code "cek-thaw") " reconstructs a live state from frozen SX. "
(code "cek-run") " resumes from where it left off."))
(~docs/section :title "Freeze" :id "freeze"
(p "Take a computation mid-flight and freeze it:")
(~docs/code :code (highlight
"(let ((expr (sx-parse \"(+ 1 (* 2 3))\"))\n (state (make-cek-state (first expr) (make-env) (list))))\n ;; Step 4 times\n (set! state (cek-step (cek-step (cek-step (cek-step state)))))\n ;; Freeze to SX\n (cek-freeze state))"
"lisp"))
(p "The frozen state is pure SX:")
(~docs/code :code (highlight
"{:phase \"continue\"\n :control nil\n :value 1\n :env {}\n :kont ({:type \"arg\"\n :f (primitive \"+\")\n :evaled ()\n :remaining ((* 2 3))\n :env {}})}"
"lisp"))
(p "Everything is data. The continuation frame says: \u201cI was adding 1 to something, "
"and I still need to evaluate " (code "(* 2 3)") ".\u201d"))
(~docs/section :title "Thaw and resume" :id "thaw"
(p "Parse the frozen SX back. Thaw it. Resume:")
(~docs/code :code (highlight
"(let ((frozen (sx-parse frozen-text))\n (state (cek-thaw (first frozen))))\n (cek-run state))\n;; => 7"
"lisp"))
(p "Native functions like " (code "+") " serialize as " (code "(primitive \"+\")")
" and are looked up in the primitive registry on thaw. "
"Lambdas serialize as their source AST \u2014 " (code "(lambda (x) (* x 2))")
" \u2014 and reconstruct as callable functions."))
(~docs/section :title "Live demo" :id "demo"
(p "Type an expression, step to any point, freeze the state. "
"The frozen SX appears below. Click Thaw to resume from the frozen state.")
(~geography/cek/freeze-demo))
(~docs/section :title "Content addressing" :id "content-addressing"
(p "Hash the frozen SX " (code "\u2192") " content identifier. "
"Same state always produces the same CID. Store by CID, retrieve by CID, verify by CID.")
(~docs/code :code (highlight
"(freeze-to-cid \"widget\")\n;; => \"d9eea67b\"\n\n(thaw-from-cid \"d9eea67b\")\n;; Signals restored. Same CID = same state."
"lisp"))
(p "Try it: change the values, click Content-address. Copy the CID. "
"Change the values again. Paste the CID and Restore.")
(~geography/cek/content-address-demo))
(~docs/section :title "What this enables" :id "enables"
(ul :class "list-disc pl-6 mb-4 space-y-2 text-stone-600"
(li (strong "Persistence") " \u2014 save reactive island state to localStorage, "
"resume on page reload")
(li (strong "Migration") " \u2014 freeze a computation on one machine, "
"thaw on another. Same result, deterministically.")
(li (strong "Content addressing") " \u2014 hash the frozen SX \u2192 CID. "
"A pointer to a computation in progress, not just a value.")
(li (strong "Time travel") " \u2014 freeze at each step, store the history. "
"Jump to any point. Undo. Branch.")
(li (strong "Verification") " \u2014 re-run from a frozen state, "
"check the result matches. Reproducible computation."))
(p "The Platonic argument made concrete: a computation IS a value. "
"The Form persists. The instance resumes."))))
(defisland ~geography/cek/freeze-demo ()
(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)))))))
(defcomp ~geography/cek/cek-content-address-content ()
(~docs/page :title "Content-Addressed Computation"
(p :class "text-stone-500 text-sm italic mb-8"
"A computation is a value. A value has a hash. The hash is the address. "
"Same state, same address, forever.")
(~docs/section :title "The idea" :id "idea"
(p "Freeze a scope to SX. Hash the SX text. The hash is a content identifier (CID). "
"Store the frozen SX keyed by CID. Later, look up the CID, thaw, resume.")
(p "The critical property: "
(strong "same state always produces the same CID") ". "
"Two machines freezing identical signal values get identical CIDs. "
"The address IS the content."))
(~docs/section :title "How it works" :id "how"
(~docs/code :code (highlight
";; Freeze a scope \u2192 hash \u2192 CID\n(freeze-to-cid \"widget\")\n;; => \"d9eea67b\"\n\n;; The frozen SX is stored by CID\n(content-get \"d9eea67b\")\n;; => {:name \"widget\" :signals {:count 42 :name \"hello\"}}\n\n;; Thaw from CID \u2192 signals restored\n(thaw-from-cid \"d9eea67b\")\n;; Signals reset to frozen values"
"lisp"))
(p "The hash is djb2 for now \u2014 deterministic and fast. "
"Real deployment uses SHA-256 / multihash for IPFS compatibility."))
(~docs/section :title "Why it matters" :id "why"
(ul :class "list-disc pl-6 mb-4 space-y-2 text-stone-600"
(li (strong "Sharing") " \u2014 send a CID, not a blob. "
"The receiver looks up the CID and gets the exact state.")
(li (strong "Deduplication") " \u2014 same state = same CID. "
"Store once, reference many times.")
(li (strong "Verification") " \u2014 re-freeze, compare CIDs. "
"If they match, the state is identical. No diffing needed.")
(li (strong "History") " \u2014 each CID is a snapshot. "
"A sequence of CIDs is a complete undo history.")
(li (strong "Distribution") " \u2014 CIDs on IPFS are global. "
"Pin a widget state, anyone can thaw it. "
"No server, no API, no account.")))
(~docs/section :title "Live demo" :id "demo"
(p "Change the counter and name. Click " (strong "Content-address") " to freeze and hash. "
"The CID appears below. Change the values, then click any CID in the history "
"or paste one into the input to restore.")
(~geography/cek/content-address-demo))
(~docs/section :title "The path to IPFS" :id "ipfs"
(p "The content store is currently in-memory. The next steps:")
(ul :class "list-disc pl-6 mb-4 space-y-1 text-stone-600"
(li "Replace djb2 with SHA-256 (browser SubtleCrypto)")
(li "Wrap in multihash + CIDv1 format")
(li "Store to IPFS via the Art DAG L2 registry")
(li "Pin CIDs attributed to " (code "sx-web.org"))
(li "Anyone can pin, fork, extend"))
(p "The frozen SX is the content. The CID is the address. "
"IPFS is the network. The widget state becomes a permanent, "
"verifiable, shareable artifact \u2014 not trapped in a database, "
"not behind an API, not owned by anyone."))))
;; ---------------------------------------------------------------------------
;; Demo page content
;; ---------------------------------------------------------------------------
(defcomp ~geography/cek/cek-demo-content ()
(~docs/page :title "CEK Demo"
(~docs/section :title "What this demonstrates" :id "what"
(p "These are " (strong "live islands") " evaluated by the CEK machine. Every " (code "eval-expr") " goes through " (code "cek-run") ". Every " (code "(deref sig)") " in an island creates a reactive DOM binding via continuation frames.")
(p "The CEK machine is defined in " (code "cek.sx") " and " (code "frames.sx") " — pure s-expressions, bootstrapped to both JavaScript and Python."))
(~docs/section :title "Stepper" :id "stepper"
(p "The CEK machine is pure data\u2192data. Each step takes a state dict and returns a new one. "
"Type an expression, click Step to advance one CEK transition.")
(~geography/cek/demo-stepper :initial-expr "(let ((x 10)) (+ x (* 2 3)))")
(~docs/code :code (highlight (component-source "~geography/cek/demo-stepper") "lisp")))
(~docs/section :title "Render stepper" :id "render-stepper"
(p "Watch a component render itself. The CEK evaluates the expression — "
"when it encounters " (code "(div ...)") ", the render adapter produces HTML in one step. "
"Click Run to see the rendered output appear in the preview.")
(~geography/cek/demo-render-stepper)
(~docs/code :code (highlight (component-source "~geography/cek/demo-render-stepper") "lisp")))
(~docs/section :title "1. Counter" :id "demo-counter"
(p (code "(deref count)") " in text position creates a reactive text node. " (code "(deref doubled)") " is a computed that updates when count changes.")
(~geography/cek/demo-counter :initial 0)
(~docs/code :code (highlight (component-source "~geography/cek/demo-counter") "lisp")))
(~docs/section :title "2. Computed chain" :id "demo-chain"
(p "Three levels of computed: base -> doubled -> quadrupled. Change base, all propagate.")
(~geography/cek/demo-chain)
(~docs/code :code (highlight (component-source "~geography/cek/demo-chain") "lisp")))
(~docs/section :title "3. Reactive attributes" :id "demo-attr"
(p (code "(deref sig)") " in " (code ":class") " position. The CEK evaluates the " (code "str") " expression, and when the signal changes, the continuation re-evaluates and updates the attribute.")
(~geography/cek/demo-reactive-attr)
(~docs/code :code (highlight (component-source "~geography/cek/demo-reactive-attr") "lisp")))
(~docs/section :title "4. Effect + cleanup" :id "demo-stopwatch"
(p "Effects still work through CEK. This stopwatch uses " (code "effect") " with cleanup — toggling the signal clears the interval.")
(~geography/cek/demo-stopwatch)
(~docs/code :code (highlight (component-source "~geography/cek/demo-stopwatch") "lisp")))
(~docs/section :title "5. Batch coalescing" :id "demo-batch"
(p "Two signals updated in " (code "batch") " — one notification cycle. Compare render counts between batch and no-batch.")
(~geography/cek/demo-batch)
(~docs/code :code (highlight (component-source "~geography/cek/demo-batch") "lisp")))))