Add CEK Machine section under Geography with live island demos
geography/cek.sx: overview page (three registers, deref-as-shift explanation) + demo page with 5 live islands (counter, computed chain, reactive attrs, stopwatch effect+cleanup, batch coalescing). Nav entry, router routes, defpage definitions. CEK exports (cekRun, makeCekState, makeReactiveResetFrame, evalExpr) added to Sx public API via platform_js.py. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
195
sx/sx/geography/cek.sx
Normal file
195
sx/sx/geography/cek.sx
Normal file
@@ -0,0 +1,195 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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") " (160 lines) and " (code "frames.sx") " (100 lines) — pure s-expressions, bootstrapped to both JavaScript and Python."))
|
||||
|
||||
(~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
|
||||
"(defisland ~demo-counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div\n (button :on-click (fn (e) (swap! count dec)) \"-\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p (str \"doubled: \" (deref doubled))))))"
|
||||
"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
|
||||
"(let ((base (signal 1))\n (doubled (computed (fn () (* (deref base) 2))))\n (quadrupled (computed (fn () (* (deref doubled) 2)))))\n (span (deref base))\n (p (str \"doubled: \" (deref doubled)\n \" | quadrupled: \" (deref quadrupled))))"
|
||||
"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
|
||||
"(div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n (if (deref danger) \"DANGER\" \"SAFE\"))"
|
||||
"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
|
||||
"(effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))"
|
||||
"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
|
||||
"(batch (fn ()\n (swap! first-sig inc)\n (swap! second-sig inc)))\n;; One render pass, not two."
|
||||
"lisp")))))
|
||||
@@ -166,6 +166,12 @@
|
||||
(dict :label "Optimistic" :href "/sx/(geography.(isomorphism.optimistic))")
|
||||
(dict :label "Offline" :href "/sx/(geography.(isomorphism.offline))")))
|
||||
|
||||
(define cek-nav-items (list
|
||||
(dict :label "Overview" :href "/sx/(geography.(cek))"
|
||||
:summary "The CEK machine — explicit evaluator with Control, Environment, Kontinuation. Three registers, pure step function.")
|
||||
(dict :label "Demo" :href "/sx/(geography.(cek.demo))"
|
||||
:summary "Live islands evaluated by the CEK machine. Counter, computed chains, reactive attributes — all through explicit continuation frames.")))
|
||||
|
||||
(define plans-nav-items (list
|
||||
(dict :label "Status" :href "/sx/(etc.(plan.status))"
|
||||
:summary "Audit of all plans — what's done, what's in progress, and what remains.")
|
||||
@@ -384,7 +390,8 @@
|
||||
:summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, built on scopes."}
|
||||
{:label "Marshes" :href "/sx/(geography.(marshes))"
|
||||
:summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted."}
|
||||
{:label "Isomorphism" :href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items})}
|
||||
{:label "Isomorphism" :href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items}
|
||||
{:label "CEK Machine" :href "/sx/(geography.(cek))" :children cek-nav-items})}
|
||||
{:label "Language" :href "/sx/(language)"
|
||||
:children (list
|
||||
{:label "Docs" :href "/sx/(language.(doc))" :children docs-nav-items}
|
||||
|
||||
@@ -650,6 +650,25 @@
|
||||
:layout :sx-docs
|
||||
:content (~layouts/doc :path "/sx/(geography.(marshes))" (~reactive-islands/marshes/reactive-islands-marshes-content)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; CEK Machine section (under Geography)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defpage cek-index
|
||||
:path "/geography/cek/"
|
||||
:auth :public
|
||||
:layout :sx-docs
|
||||
:content (~layouts/doc :path "/sx/(geography.(cek))" (~geography/cek/cek-content)))
|
||||
|
||||
(defpage cek-page
|
||||
:path "/geography/cek/<slug>"
|
||||
:auth :public
|
||||
:layout :sx-docs
|
||||
:content (~layouts/doc :path (str "/sx/(geography.(cek." slug "))")
|
||||
(case slug
|
||||
"demo" (~geography/cek/cek-demo-content)
|
||||
:else (~geography/cek/cek-content))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Bootstrapped page helpers demo
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -267,6 +267,8 @@ _REDIRECT_PATTERNS = [
|
||||
lambda m: f"/sx/(geography.(reactive.{m.group(1)}))"),
|
||||
(re.compile(r"^/geography/isomorphism/(.+?)/?$"),
|
||||
lambda m: f"/sx/(geography.(isomorphism.{m.group(1)}))"),
|
||||
(re.compile(r"^/geography/cek/(.+?)/?$"),
|
||||
lambda m: f"/sx/(geography.(cek.{m.group(1)}))"),
|
||||
(re.compile(r"^/geography/spreads/?$"),
|
||||
"/sx/(geography.(spreads))"),
|
||||
(re.compile(r"^/geography/marshes/?$"),
|
||||
@@ -290,6 +292,7 @@ _REDIRECT_PATTERNS = [
|
||||
(re.compile(r"^/geography/hypermedia/?$"), "/sx/(geography.(hypermedia))"),
|
||||
(re.compile(r"^/geography/reactive/?$"), "/sx/(geography.(reactive))"),
|
||||
(re.compile(r"^/geography/isomorphism/?$"), "/sx/(geography.(isomorphism))"),
|
||||
(re.compile(r"^/geography/cek/?$"), "/sx/(geography.(cek))"),
|
||||
(re.compile(r"^/geography/?$"), "/sx/(geography)"),
|
||||
(re.compile(r"^/applications/cssx/?$"), "/sx/(applications.(cssx))"),
|
||||
(re.compile(r"^/applications/protocols/?$"), "/sx/(applications.(protocol))"),
|
||||
|
||||
Reference in New Issue
Block a user