Add live CEK stepper island — interactive stepping debugger
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 26s

A defisland that lets users type an SX expression, step through CEK
evaluation one transition at a time, and see C/E/K registers update
live. Demonstrates that cek-step is pure data->data.

- cek.sx geography: add ~geography/cek/demo-stepper island with
  source input, step/run/reset buttons, state display, step history
- platform_js.py: register CEK stepping primitives (make-cek-state,
  cek-step, cek-terminal?, cek-value, make-env, sx-serialize) so
  island code can access them

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 13:31:52 +00:00
parent 0047757af8
commit c24a672c36
3 changed files with 354 additions and 17 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-14T10:44:25Z";
var SX_VERSION = "2026-03-14T14:12:59Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -6547,6 +6547,21 @@ return (function() {
return cekValue(state);
};
// CEK stepping primitives — for debugger islands
PRIMITIVES["make-cek-state"] = makeCekState;
PRIMITIVES["cek-step"] = cekStep;
PRIMITIVES["cek-terminal?"] = cekTerminal_p;
PRIMITIVES["cek-value"] = cekValue;
PRIMITIVES["make-env"] = function() { return merge(PRIMITIVES); };
PRIMITIVES["sx-serialize"] = sxSerialize;
PRIMITIVES["lambda-name"] = lambdaName;
PRIMITIVES["callable?"] = isCallable;
PRIMITIVES["is-html-tag?"] = function(name) { return HTML_TAGS.indexOf(name) >= 0; };
PRIMITIVES["make-symbol"] = function(name) { return new Symbol(name); };
PRIMITIVES["keyword-name"] = keywordName;
PRIMITIVES["type-of"] = typeOf;
PRIMITIVES["symbol-name"] = symbolName;
// =========================================================================
// Async IO: Promise-aware rendering for client-side IO primitives

View File

@@ -1508,6 +1508,21 @@ CEK_FIXUPS_JS = '''
while (!cekTerminal_p(state)) { state = cekStep(state); }
return cekValue(state);
};
// CEK stepping primitives — for debugger islands
PRIMITIVES["make-cek-state"] = makeCekState;
PRIMITIVES["cek-step"] = cekStep;
PRIMITIVES["cek-terminal?"] = cekTerminal_p;
PRIMITIVES["cek-value"] = cekValue;
PRIMITIVES["make-env"] = function() { return merge(PRIMITIVES); };
PRIMITIVES["sx-serialize"] = sxSerialize;
PRIMITIVES["lambda-name"] = lambdaName;
PRIMITIVES["callable?"] = isCallable;
PRIMITIVES["is-html-tag?"] = function(name) { return HTML_TAGS.indexOf(name) >= 0; };
PRIMITIVES["make-symbol"] = function(name) { return new Symbol(name); };
PRIMITIVES["keyword-name"] = keywordName;
PRIMITIVES["type-of"] = typeOf;
PRIMITIVES["symbol-name"] = symbolName;
'''

View File

@@ -148,6 +148,310 @@
"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 :class \"p-6 rounded-lg border border-stone-200 bg-white text-center\"\n (h1 :class \"text-3xl font-bold mb-2\"\n (span :class \"text-rose-500\" \"the \")\n (span :class \"text-amber-500\" \"joy \")\n (span :class \"text-emerald-500\" \"of \")\n (span :class \"text-violet-600 text-4xl\" \"sx\")))")))
(steps (signal (list)))
(step-idx (signal 0))
(parsed-ok (signal false))
(error-msg (signal nil)))
(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"
;; Parse button
(button :on-click (fn (e)
(reset! error-msg nil)
(reset! step-idx 0)
(reset! parsed-ok false)
(let ((parsed (sx-parse (deref source))))
(if (empty? parsed)
(do (reset! error-msg "Parse error") (reset! steps (list)))
(let ((expr (first parsed)))
(if (and (list? expr) (not (empty? expr))
(= (type-of (first expr)) "symbol")
(is-html-tag? (symbol-name (first expr))))
;; HTML tag: split into open, children, close
(let ((tag (symbol-name (first expr)))
(args (rest expr))
(children (list))
(attrs (list))
(in-kw false)
(result (list)))
(for-each (fn (a)
(cond
(= (type-of a) "keyword") (do (set! in-kw true) (append! attrs a))
in-kw (do (set! in-kw false) (append! attrs a))
:else (do (set! in-kw false) (append! children a))))
args)
(let ((attr-parts (list)))
(let loop ((i 0))
(when (< i (len attrs))
(append! attr-parts
(str " " (keyword-name (nth attrs i)) "=\""
(nth attrs (+ i 1)) "\""))
(loop (+ i 2))))
(let ((attr-str (join "" attr-parts))
(open-html (str "<" tag attr-str ">")))
(append! result {"label" (str "<" tag (if (empty? attr-str) "" " ...") ">") "html" open-html})
(let ((add-steps nil))
(set! add-steps
(fn (expr)
(if (and (list? expr) (not (empty? expr))
(= (type-of (first expr)) "symbol")
(is-html-tag? (symbol-name (first expr))))
;; Recurse into HTML tag
(let ((ctag (symbol-name (first expr)))
(cargs (rest expr))
(cchildren (list))
(cattrs (list))
(ckw false))
(for-each (fn (a)
(cond
(= (type-of a) "keyword") (do (set! ckw true) (append! cattrs a))
ckw (do (set! ckw false) (append! cattrs a))
:else (do (set! ckw false) (append! cchildren a))))
cargs)
(let ((cap (list)))
(let aloop ((j 0))
(when (< j (len cattrs))
(append! cap
(str " " (keyword-name (nth cattrs j)) "=\""
(nth cattrs (+ j 1)) "\""))
(aloop (+ j 2))))
(let ((cas (join "" cap)))
(append! result {"label" (str "<" ctag (if (empty? cas) "" " ...") ">") "html" (str "<" ctag cas ">")})
(for-each add-steps cchildren)
(append! result {"label" (str "</" ctag ">") "html" (str "</" ctag ">")}))))
;; Leaf: render in one step
(let ((html (render-to-html expr (make-env)))
(sx-str (sx-serialize expr)))
(append! result {"label" sx-str "html" html})))))
(for-each add-steps children))
(append! result {"label" (str "</" tag ">") "html" (str "</" tag ">")})
(reset! steps result)
(reset! parsed-ok true))))
;; Not a tag
(let ((html (render-to-html expr (make-env))))
(reset! steps (list {"label" (sx-serialize expr) "html" html}))
(reset! parsed-ok true)))))))
:class "px-3 py-1.5 rounded bg-stone-700 text-white text-sm hover:bg-stone-800" "Parse")
;; Back button
(button :on-click (fn (e)
(when (> (deref step-idx) 0)
(swap! step-idx dec)))
: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")
;; Step button
(button :on-click (fn (e)
(when (and (deref parsed-ok) (< (deref step-idx) (len (deref steps))))
(swap! step-idx inc)))
: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")
;; Run button
(button :on-click (fn (e)
(when (deref parsed-ok)
(reset! step-idx (len (deref steps)))))
: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)))
;; Status
(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.")))
;; Two-pane
(when (deref parsed-ok)
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
;; Left: step list
(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 (> (len lbl) 60) (str (slice lbl 0 57) "...") lbl)))))
(deref steps))))
;; Right: live preview — rebuild from steps[0..step-idx]
(div :class "rounded border border-stone-200 bg-white p-3 min-h-24"
(div :class "text-xs text-stone-400 mb-2" "Live preview")
(if (= (deref step-idx) 0)
(div :class "text-stone-300 text-sm italic" "Click Step to start rendering...")
(raw! (join "" (map (fn (s) (get s "html"))
(slice (deref steps) 0 (deref step-idx))))))))))) )
;; ---------------------------------------------------------------------------
;; Demo page content
;; ---------------------------------------------------------------------------
@@ -157,39 +461,42 @@
(~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."))
(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
"(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/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
"(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/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
"(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/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
"(effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))"
"lisp")))
(~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
"(batch (fn ()\n (swap! first-sig inc)\n (swap! second-sig inc)))\n;; One render pass, not two."
"lisp")))))
(~docs/code :code (highlight (component-source "~geography/cek/demo-batch") "lisp")))))