All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 22s
Cache the style element reference in _cssx-style-el so flush-cssx-to-dom never creates more than one. Previous code called dom-query on every flush, which could miss the element during rapid successive calls, creating duplicates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
518 lines
26 KiB
Plaintext
518 lines
26 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")))
|
|
(effect (fn ()
|
|
(when (deref running)
|
|
(let ((id (set-interval (fn () (swap! elapsed inc)) 100)))
|
|
(fn () (clear-interval id))))))
|
|
(effect (fn ()
|
|
(let ((e (deref elapsed)))
|
|
(dom-set-text-content time-text
|
|
(str (floor (/ e 10)) "." (mod e 10) "s")))))
|
|
(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)))
|
|
(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)))
|
|
(letrec
|
|
((dom-stack (list))
|
|
(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}))))
|
|
(do-parse (fn ()
|
|
(reset! error-msg nil)
|
|
(reset! step-idx 0)
|
|
(reset! parsed-ok false)
|
|
(set! dom-stack (list))
|
|
(let ((container (dom-query "#render-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! dom-stack (list (dom-query "#render-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"))
|
|
(parent (if (empty? dom-stack) (dom-query "#render-preview") (last dom-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))
|
|
(set! dom-stack (append dom-stack (list el))))
|
|
(= step-type "close")
|
|
(when (> (len dom-stack) 1)
|
|
(set! dom-stack (slice dom-stack 0 (- (len dom-stack) 1))))
|
|
(= 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 ()
|
|
(console-log "do-back: idx=" (deref step-idx))
|
|
(when (and (deref parsed-ok) (> (deref step-idx) 0))
|
|
(let ((target (- (deref step-idx) 1))
|
|
(container (dom-query "#render-preview")))
|
|
(when container (dom-set-prop container "innerHTML" ""))
|
|
(set! dom-stack (list (dom-query "#render-preview")))
|
|
(reset! step-idx 0)
|
|
(let loop ()
|
|
(when (< (deref step-idx) target)
|
|
(do-step)
|
|
(loop))))))))
|
|
(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")
|
|
(div :id "render-preview")))))))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; 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")))))
|