diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 23a9e74..c3b8379 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-14T01:00:32Z"; + var SX_VERSION = "2026-03-14T01:23:35Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -1391,7 +1391,7 @@ var d2 = hexDigitValue(nth(source, pos)); var _ = (pos = (pos + 1)); var d3 = hexDigitValue(nth(source, pos)); var _ = (pos = (pos + 1)); -buf = (String(buf) + String(charFromCode(((((d0 * 4096) + (d1 * 256)) + (d2 * 16)) + d3)))); +buf = (String(buf) + String(charFromCode((d0 * 4096)))); continue; } } else { buf = (String(buf) + String((isSxTruthy((esc == "n")) ? "\n" : (isSxTruthy((esc == "t")) ? "\t" : (isSxTruthy((esc == "r")) ? "\r" : esc))))); pos = (pos + 1); continue; } } } else { buf = (String(buf) + String(ch)); @@ -6442,87 +6442,6 @@ return forEach(function(d) { return invoke(d); }, subDisposers); }); }; - // ========================================================================= - // Extension: Delimited continuations (shift/reset) - // ========================================================================= - - function Continuation(fn) { this.fn = fn; } - Continuation.prototype._continuation = true; - Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); }; - - function ShiftSignal(kName, body, env) { - this.kName = kName; - this.body = body; - this.env = env; - } - - PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; }; - - var _resetResume = []; - - function sfReset(args, env) { - var body = args[0]; - try { - return trampoline(evalExpr(body, env)); - } catch (e) { - if (e instanceof ShiftSignal) { - var sig = e; - var cont = new Continuation(function(value) { - if (value === undefined) value = NIL; - _resetResume.push(value); - try { - return trampoline(evalExpr(body, env)); - } finally { - _resetResume.pop(); - } - }); - var sigEnv = merge(sig.env); - sigEnv[sig.kName] = cont; - return trampoline(evalExpr(sig.body, sigEnv)); - } - throw e; - } - } - - function sfShift(args, env) { - if (_resetResume.length > 0) { - return _resetResume[_resetResume.length - 1]; - } - var kName = symbolName(args[0]); - var body = args[1]; - throw new ShiftSignal(kName, body, env); - } - - // Wrap evalList to intercept reset/shift - var _baseEvalList = evalList; - evalList = function(expr, env) { - var head = expr[0]; - if (isSym(head)) { - var name = head.name; - if (name === "reset") return sfReset(expr.slice(1), env); - if (name === "shift") return sfShift(expr.slice(1), env); - } - return _baseEvalList(expr, env); - }; - - // Wrap aserSpecial to handle reset/shift in SX wire mode - if (typeof aserSpecial === "function") { - var _baseAserSpecial = aserSpecial; - aserSpecial = function(name, expr, env) { - if (name === "reset") return sfReset(expr.slice(1), env); - if (name === "shift") return sfShift(expr.slice(1), env); - return _baseAserSpecial(name, expr, env); - }; - } - - // Wrap typeOf to recognize continuations - var _baseTypeOf = typeOf; - typeOf = function(x) { - if (x != null && x._continuation) return "continuation"; - return _baseTypeOf(x); - }; - - // ========================================================================= // Async IO: Promise-aware rendering for client-side IO primitives // ========================================================================= @@ -7320,6 +7239,14 @@ return forEach(function(d) { return invoke(d); }, subDisposers); }); context: sxContext, emit: sxEmit, emitted: sxEmitted, + cekRun: cekRun, + makeCekState: makeCekState, + makeCekValue: makeCekValue, + cekStep: cekStep, + cekTerminal: cekTerminal_p, + cekValue: cekValue, + makeReactiveResetFrame: makeReactiveResetFrame, + evalExpr: evalExpr, _version: "ref-2.0 (boot+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)" }; diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py index 013cf71..f606d20 100644 --- a/shared/sx/ref/platform_js.py +++ b/shared/sx/ref/platform_js.py @@ -3076,7 +3076,7 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_ return "\n".join(lines) -def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False, has_signals=False, has_page_helpers=False): +def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False, has_signals=False, has_page_helpers=False, has_cek=False): # Parser: use compiled sxParse from parser.sx, or inline a minimal fallback if has_parser: parser = ''' @@ -3272,6 +3272,15 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has api_lines.append(' context: sxContext,') api_lines.append(' emit: sxEmit,') api_lines.append(' emitted: sxEmitted,') + if has_cek: + api_lines.append(' cekRun: cekRun,') + api_lines.append(' makeCekState: makeCekState,') + api_lines.append(' makeCekValue: makeCekValue,') + api_lines.append(' cekStep: cekStep,') + api_lines.append(' cekTerminal: cekTerminal_p,') + api_lines.append(' cekValue: cekValue,') + api_lines.append(' makeReactiveResetFrame: makeReactiveResetFrame,') + api_lines.append(' evalExpr: evalExpr,') api_lines.append(f' _version: "{version}"') api_lines.append(' };') api_lines.append('') diff --git a/shared/sx/ref/run_js_sx.py b/shared/sx/ref/run_js_sx.py index 9edfa24..50cbaf9 100644 --- a/shared/sx/ref/run_js_sx.py +++ b/shared/sx/ref/run_js_sx.py @@ -226,7 +226,7 @@ def compile_ref_to_js( parts.append(CONTINUATIONS_JS) if has_dom: parts.append(ASYNC_IO_JS) - parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals, has_page_helpers)) + parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals, has_page_helpers, has_cek)) parts.append(EPILOGUE) build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") diff --git a/sx/sx/geography/cek.sx b/sx/sx/geography/cek.sx new file mode 100644 index 0000000..a723d6a --- /dev/null +++ b/sx/sx/geography/cek.sx @@ -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"))))) diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 6115c5c..ea4458b 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -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} diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index 45574b4..c0aa01e 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -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/" + :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 ;; --------------------------------------------------------------------------- diff --git a/sx/sxc/pages/sx_router.py b/sx/sxc/pages/sx_router.py index 883489d..bf13bc8 100644 --- a/sx/sxc/pages/sx_router.py +++ b/sx/sxc/pages/sx_router.py @@ -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))"),