diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 137bf26..ed3eb02 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-14T10:44:25Z"; + var SX_VERSION = "2026-03-14T15:35:55Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -5096,6 +5096,167 @@ return (function() { })(); }; + // ========================================================================= + // Post-transpilation fixups + // ========================================================================= + // The reference spec's call-lambda only handles Lambda objects, but HO forms + // (map, reduce, etc.) may receive native primitives. Wrap to handle both. + var _rawCallLambda = callLambda; + callLambda = function(f, args, callerEnv) { + if (typeof f === "function") return f.apply(null, args); + return _rawCallLambda(f, args, callerEnv); + }; + + // Expose render functions as primitives so SX code can call them + if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml; + if (typeof renderToSx === "function") PRIMITIVES["render-to-sx"] = renderToSx; + if (typeof aser === "function") PRIMITIVES["aser"] = aser; + if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom; + + // Expose signal functions as primitives so runtime-evaluated SX code + // (e.g. island bodies from .sx files) can call them + PRIMITIVES["signal"] = signal; + PRIMITIVES["signal?"] = isSignal; + PRIMITIVES["deref"] = deref; + PRIMITIVES["reset!"] = reset_b; + PRIMITIVES["swap!"] = swap_b; + PRIMITIVES["computed"] = computed; + PRIMITIVES["effect"] = effect; + PRIMITIVES["batch"] = batch; + // Timer primitives for island code + PRIMITIVES["set-interval"] = setInterval_; + PRIMITIVES["clear-interval"] = clearInterval_; + // Reactive DOM helpers for island code + PRIMITIVES["reactive-text"] = reactiveText; + PRIMITIVES["create-text-node"] = createTextNode; + PRIMITIVES["dom-set-text-content"] = domSetTextContent; + PRIMITIVES["dom-listen"] = domListen; + PRIMITIVES["dom-dispatch"] = domDispatch; + PRIMITIVES["event-detail"] = eventDetail; + PRIMITIVES["resource"] = resource; + PRIMITIVES["promise-delayed"] = promiseDelayed; + PRIMITIVES["promise-then"] = promiseThen; + PRIMITIVES["def-store"] = defStore; + PRIMITIVES["use-store"] = useStore; + PRIMITIVES["emit-event"] = emitEvent; + PRIMITIVES["on-event"] = onEvent; + PRIMITIVES["bridge-event"] = bridgeEvent; + // DOM primitives for island code + PRIMITIVES["dom-focus"] = domFocus; + PRIMITIVES["dom-tag-name"] = domTagName; + PRIMITIVES["dom-get-prop"] = domGetProp; + PRIMITIVES["dom-set-prop"] = domSetProp; + PRIMITIVES["dom-call-method"] = domCallMethod; + PRIMITIVES["dom-post-message"] = domPostMessage; + PRIMITIVES["stop-propagation"] = stopPropagation_; + PRIMITIVES["error-message"] = errorMessage; + PRIMITIVES["schedule-idle"] = scheduleIdle; + PRIMITIVES["error"] = function(msg) { throw new Error(msg); }; + PRIMITIVES["filter"] = filter; + // DOM primitives for sx-on:* handlers and data-init scripts + if (typeof domBody === "function") PRIMITIVES["dom-body"] = domBody; + if (typeof domQuery === "function") PRIMITIVES["dom-query"] = domQuery; + if (typeof domQueryAll === "function") PRIMITIVES["dom-query-all"] = domQueryAll; + if (typeof domQueryById === "function") PRIMITIVES["dom-query-by-id"] = domQueryById; + if (typeof domSetAttr === "function") PRIMITIVES["dom-set-attr"] = domSetAttr; + if (typeof domGetAttr === "function") PRIMITIVES["dom-get-attr"] = domGetAttr; + if (typeof domRemoveAttr === "function") PRIMITIVES["dom-remove-attr"] = domRemoveAttr; + if (typeof domHasAttr === "function") PRIMITIVES["dom-has-attr?"] = domHasAttr; + if (typeof domAddClass === "function") PRIMITIVES["dom-add-class"] = domAddClass; + if (typeof domRemoveClass === "function") PRIMITIVES["dom-remove-class"] = domRemoveClass; + if (typeof domHasClass === "function") PRIMITIVES["dom-has-class?"] = domHasClass; + if (typeof domClosest === "function") PRIMITIVES["dom-closest"] = domClosest; + if (typeof domMatches === "function") PRIMITIVES["dom-matches?"] = domMatches; + if (typeof preventDefault_ === "function") PRIMITIVES["prevent-default"] = preventDefault_; + if (typeof elementValue === "function") PRIMITIVES["element-value"] = elementValue; + if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml; + if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml; + if (typeof domTextContent === "function") PRIMITIVES["dom-text-content"] = domTextContent; + if (typeof domCreateElement === "function") PRIMITIVES["dom-create-element"] = domCreateElement; + if (typeof domAppend === "function") PRIMITIVES["dom-append"] = domAppend; + if (typeof jsonParse === "function") PRIMITIVES["json-parse"] = jsonParse; + if (typeof nowMs === "function") PRIMITIVES["now-ms"] = nowMs; + PRIMITIVES["sx-parse"] = sxParse; + PRIMITIVES["console-log"] = function() { console.log.apply(console, ["[sx]"].concat(Array.prototype.slice.call(arguments))); return arguments.length > 0 ? arguments[0] : NIL; }; + + // Expose deps module functions as primitives so runtime-evaluated SX code + // (e.g. test-deps.sx in browser) can call them + // Platform functions (from PLATFORM_DEPS_JS) + PRIMITIVES["component-deps"] = componentDeps; + PRIMITIVES["component-set-deps!"] = componentSetDeps; + PRIMITIVES["component-css-classes"] = componentCssClasses; + PRIMITIVES["env-components"] = envComponents; + PRIMITIVES["regex-find-all"] = regexFindAll; + PRIMITIVES["scan-css-classes"] = scanCssClasses; + // Transpiled functions (from deps.sx) + PRIMITIVES["scan-refs"] = scanRefs; + PRIMITIVES["scan-refs-walk"] = scanRefsWalk; + PRIMITIVES["transitive-deps"] = transitiveDeps; + PRIMITIVES["transitive-deps-walk"] = transitiveDepsWalk; + PRIMITIVES["compute-all-deps"] = computeAllDeps; + PRIMITIVES["scan-components-from-source"] = scanComponentsFromSource; + PRIMITIVES["components-needed"] = componentsNeeded; + PRIMITIVES["page-component-bundle"] = pageComponentBundle; + PRIMITIVES["page-css-classes"] = pageCssClasses; + PRIMITIVES["scan-io-refs-walk"] = scanIoRefsWalk; + PRIMITIVES["scan-io-refs"] = scanIoRefs; + PRIMITIVES["transitive-io-refs-walk"] = transitiveIoRefsWalk; + PRIMITIVES["transitive-io-refs"] = transitiveIoRefs; + PRIMITIVES["compute-all-io-refs"] = computeAllIoRefs; + PRIMITIVES["component-io-refs-cached"] = componentIoRefsCached; + PRIMITIVES["component-pure?"] = componentPure_p; + PRIMITIVES["render-target"] = renderTarget; + PRIMITIVES["page-render-plan"] = pageRenderPlan; + + // Expose page-helper functions as primitives + PRIMITIVES["categorize-special-forms"] = categorizeSpecialForms; + PRIMITIVES["extract-define-kwargs"] = extractDefineKwargs; + PRIMITIVES["build-reference-data"] = buildReferenceData; + PRIMITIVES["build-ref-items-with-href"] = buildRefItemsWithHref; + PRIMITIVES["build-attr-detail"] = buildAttrDetail; + PRIMITIVES["build-header-detail"] = buildHeaderDetail; + PRIMITIVES["build-event-detail"] = buildEventDetail; + PRIMITIVES["build-component-source"] = buildComponentSource; + PRIMITIVES["build-bundle-analysis"] = buildBundleAnalysis; + PRIMITIVES["build-routing-analysis"] = buildRoutingAnalysis; + PRIMITIVES["build-affinity-analysis"] = buildAffinityAnalysis; + + // Override recursive cekRun with iterative loop (avoids stack overflow) + cekRun = function(state) { + while (!cekTerminal_p(state)) { state = cekStep(state); } + return cekValue(state); + }; + + // Expose spec functions so evaluated SX code can use them. + // Type inspection (platform interface from boundary.sx) + PRIMITIVES["type-of"] = typeOf; + PRIMITIVES["symbol-name"] = symbolName; + PRIMITIVES["keyword-name"] = keywordName; + PRIMITIVES["callable?"] = isCallable; + PRIMITIVES["lambda?"] = isLambda; + PRIMITIVES["lambda-name"] = lambdaName; + PRIMITIVES["component?"] = isComponent; + PRIMITIVES["island?"] = isIsland; + PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); }; + + // Parser (from parser.sx) + PRIMITIVES["sx-serialize"] = sxSerialize; + + // CEK machine (from cek.sx/frames.sx) + PRIMITIVES["make-cek-state"] = makeCekState; + PRIMITIVES["cek-step"] = cekStep; + PRIMITIVES["cek-run"] = cekRun; + PRIMITIVES["cek-terminal?"] = cekTerminal_p; + PRIMITIVES["cek-value"] = cekValue; + PRIMITIVES["eval-expr-cek"] = evalExprCek; + + // Render (from adapter-html.sx / render.sx) + PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; }; + + // Environment (for creating eval contexts) + PRIMITIVES["make-env"] = function() { return merge(componentEnv, PRIMITIVES); }; + + // ========================================================================= // Platform interface — DOM adapter (browser-only) // ========================================================================= @@ -6418,136 +6579,6 @@ return (function() { - // ========================================================================= - // Post-transpilation fixups - // ========================================================================= - // The reference spec's call-lambda only handles Lambda objects, but HO forms - // (map, reduce, etc.) may receive native primitives. Wrap to handle both. - var _rawCallLambda = callLambda; - callLambda = function(f, args, callerEnv) { - if (typeof f === "function") return f.apply(null, args); - return _rawCallLambda(f, args, callerEnv); - }; - - // Expose render functions as primitives so SX code can call them - if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml; - if (typeof renderToSx === "function") PRIMITIVES["render-to-sx"] = renderToSx; - if (typeof aser === "function") PRIMITIVES["aser"] = aser; - if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom; - - // Expose signal functions as primitives so runtime-evaluated SX code - // (e.g. island bodies from .sx files) can call them - PRIMITIVES["signal"] = signal; - PRIMITIVES["signal?"] = isSignal; - PRIMITIVES["deref"] = deref; - PRIMITIVES["reset!"] = reset_b; - PRIMITIVES["swap!"] = swap_b; - PRIMITIVES["computed"] = computed; - PRIMITIVES["effect"] = effect; - PRIMITIVES["batch"] = batch; - // Timer primitives for island code - PRIMITIVES["set-interval"] = setInterval_; - PRIMITIVES["clear-interval"] = clearInterval_; - // Reactive DOM helpers for island code - PRIMITIVES["reactive-text"] = reactiveText; - PRIMITIVES["create-text-node"] = createTextNode; - PRIMITIVES["dom-set-text-content"] = domSetTextContent; - PRIMITIVES["dom-listen"] = domListen; - PRIMITIVES["dom-dispatch"] = domDispatch; - PRIMITIVES["event-detail"] = eventDetail; - PRIMITIVES["resource"] = resource; - PRIMITIVES["promise-delayed"] = promiseDelayed; - PRIMITIVES["promise-then"] = promiseThen; - PRIMITIVES["def-store"] = defStore; - PRIMITIVES["use-store"] = useStore; - PRIMITIVES["emit-event"] = emitEvent; - PRIMITIVES["on-event"] = onEvent; - PRIMITIVES["bridge-event"] = bridgeEvent; - // DOM primitives for island code - PRIMITIVES["dom-focus"] = domFocus; - PRIMITIVES["dom-tag-name"] = domTagName; - PRIMITIVES["dom-get-prop"] = domGetProp; - PRIMITIVES["dom-set-prop"] = domSetProp; - PRIMITIVES["dom-call-method"] = domCallMethod; - PRIMITIVES["dom-post-message"] = domPostMessage; - PRIMITIVES["stop-propagation"] = stopPropagation_; - PRIMITIVES["error-message"] = errorMessage; - PRIMITIVES["schedule-idle"] = scheduleIdle; - PRIMITIVES["error"] = function(msg) { throw new Error(msg); }; - PRIMITIVES["filter"] = filter; - // DOM primitives for sx-on:* handlers and data-init scripts - if (typeof domBody === "function") PRIMITIVES["dom-body"] = domBody; - if (typeof domQuery === "function") PRIMITIVES["dom-query"] = domQuery; - if (typeof domQueryAll === "function") PRIMITIVES["dom-query-all"] = domQueryAll; - if (typeof domQueryById === "function") PRIMITIVES["dom-query-by-id"] = domQueryById; - if (typeof domSetAttr === "function") PRIMITIVES["dom-set-attr"] = domSetAttr; - if (typeof domGetAttr === "function") PRIMITIVES["dom-get-attr"] = domGetAttr; - if (typeof domRemoveAttr === "function") PRIMITIVES["dom-remove-attr"] = domRemoveAttr; - if (typeof domHasAttr === "function") PRIMITIVES["dom-has-attr?"] = domHasAttr; - if (typeof domAddClass === "function") PRIMITIVES["dom-add-class"] = domAddClass; - if (typeof domRemoveClass === "function") PRIMITIVES["dom-remove-class"] = domRemoveClass; - if (typeof domHasClass === "function") PRIMITIVES["dom-has-class?"] = domHasClass; - if (typeof domClosest === "function") PRIMITIVES["dom-closest"] = domClosest; - if (typeof domMatches === "function") PRIMITIVES["dom-matches?"] = domMatches; - if (typeof preventDefault_ === "function") PRIMITIVES["prevent-default"] = preventDefault_; - if (typeof elementValue === "function") PRIMITIVES["element-value"] = elementValue; - if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml; - if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml; - if (typeof domTextContent === "function") PRIMITIVES["dom-text-content"] = domTextContent; - if (typeof jsonParse === "function") PRIMITIVES["json-parse"] = jsonParse; - if (typeof nowMs === "function") PRIMITIVES["now-ms"] = nowMs; - PRIMITIVES["sx-parse"] = sxParse; - PRIMITIVES["console-log"] = function() { console.log.apply(console, ["[sx]"].concat(Array.prototype.slice.call(arguments))); return arguments.length > 0 ? arguments[0] : NIL; }; - - // Expose deps module functions as primitives so runtime-evaluated SX code - // (e.g. test-deps.sx in browser) can call them - // Platform functions (from PLATFORM_DEPS_JS) - PRIMITIVES["component-deps"] = componentDeps; - PRIMITIVES["component-set-deps!"] = componentSetDeps; - PRIMITIVES["component-css-classes"] = componentCssClasses; - PRIMITIVES["env-components"] = envComponents; - PRIMITIVES["regex-find-all"] = regexFindAll; - PRIMITIVES["scan-css-classes"] = scanCssClasses; - // Transpiled functions (from deps.sx) - PRIMITIVES["scan-refs"] = scanRefs; - PRIMITIVES["scan-refs-walk"] = scanRefsWalk; - PRIMITIVES["transitive-deps"] = transitiveDeps; - PRIMITIVES["transitive-deps-walk"] = transitiveDepsWalk; - PRIMITIVES["compute-all-deps"] = computeAllDeps; - PRIMITIVES["scan-components-from-source"] = scanComponentsFromSource; - PRIMITIVES["components-needed"] = componentsNeeded; - PRIMITIVES["page-component-bundle"] = pageComponentBundle; - PRIMITIVES["page-css-classes"] = pageCssClasses; - PRIMITIVES["scan-io-refs-walk"] = scanIoRefsWalk; - PRIMITIVES["scan-io-refs"] = scanIoRefs; - PRIMITIVES["transitive-io-refs-walk"] = transitiveIoRefsWalk; - PRIMITIVES["transitive-io-refs"] = transitiveIoRefs; - PRIMITIVES["compute-all-io-refs"] = computeAllIoRefs; - PRIMITIVES["component-io-refs-cached"] = componentIoRefsCached; - PRIMITIVES["component-pure?"] = componentPure_p; - PRIMITIVES["render-target"] = renderTarget; - PRIMITIVES["page-render-plan"] = pageRenderPlan; - - // Expose page-helper functions as primitives - PRIMITIVES["categorize-special-forms"] = categorizeSpecialForms; - PRIMITIVES["extract-define-kwargs"] = extractDefineKwargs; - PRIMITIVES["build-reference-data"] = buildReferenceData; - PRIMITIVES["build-ref-items-with-href"] = buildRefItemsWithHref; - PRIMITIVES["build-attr-detail"] = buildAttrDetail; - PRIMITIVES["build-header-detail"] = buildHeaderDetail; - PRIMITIVES["build-event-detail"] = buildEventDetail; - PRIMITIVES["build-component-source"] = buildComponentSource; - PRIMITIVES["build-bundle-analysis"] = buildBundleAnalysis; - PRIMITIVES["build-routing-analysis"] = buildRoutingAnalysis; - PRIMITIVES["build-affinity-analysis"] = buildAffinityAnalysis; - - // Override recursive cekRun with iterative loop (avoids stack overflow) - cekRun = function(state) { - while (!cekTerminal_p(state)) { state = cekStep(state); } - return cekValue(state); - }; - - // ========================================================================= // Async IO: Promise-aware rendering for client-side IO primitives // ========================================================================= diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py index 218418b..a63c8c2 100644 --- a/shared/sx/ref/platform_js.py +++ b/shared/sx/ref/platform_js.py @@ -1508,6 +1508,35 @@ CEK_FIXUPS_JS = ''' while (!cekTerminal_p(state)) { state = cekStep(state); } return cekValue(state); }; + + // Expose spec functions so evaluated SX code can use them. + // Type inspection (platform interface from boundary.sx) + PRIMITIVES["type-of"] = typeOf; + PRIMITIVES["symbol-name"] = symbolName; + PRIMITIVES["keyword-name"] = keywordName; + PRIMITIVES["callable?"] = isCallable; + PRIMITIVES["lambda?"] = isLambda; + PRIMITIVES["lambda-name"] = lambdaName; + PRIMITIVES["component?"] = isComponent; + PRIMITIVES["island?"] = isIsland; + PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); }; + + // Parser (from parser.sx) + PRIMITIVES["sx-serialize"] = sxSerialize; + + // CEK machine (from cek.sx/frames.sx) + PRIMITIVES["make-cek-state"] = makeCekState; + PRIMITIVES["cek-step"] = cekStep; + PRIMITIVES["cek-run"] = cekRun; + PRIMITIVES["cek-terminal?"] = cekTerminal_p; + PRIMITIVES["cek-value"] = cekValue; + PRIMITIVES["eval-expr-cek"] = evalExprCek; + + // Render (from adapter-html.sx / render.sx) + PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; }; + + // Environment (for creating eval contexts) + PRIMITIVES["make-env"] = function() { return merge(componentEnv, PRIMITIVES); }; ''' @@ -3033,6 +3062,8 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_ if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml; if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml; if (typeof domTextContent === "function") PRIMITIVES["dom-text-content"] = domTextContent; + if (typeof domCreateElement === "function") PRIMITIVES["dom-create-element"] = domCreateElement; + if (typeof domAppend === "function") PRIMITIVES["dom-append"] = domAppend; if (typeof jsonParse === "function") PRIMITIVES["json-parse"] = jsonParse; if (typeof nowMs === "function") PRIMITIVES["now-ms"] = nowMs; PRIMITIVES["sx-parse"] = sxParse; diff --git a/shared/sx/ref/run_js_sx.py b/shared/sx/ref/run_js_sx.py index 9a6a1e7..2dfe0b4 100644 --- a/shared/sx/ref/run_js_sx.py +++ b/shared/sx/ref/run_js_sx.py @@ -216,13 +216,15 @@ def compile_ref_to_js( # Platform JS for selected adapters if not has_dom: parts.append("\n var _hasDom = false;\n") - for name in ("dom", "engine", "orchestration", "boot"): - if name in adapter_set and name in adapter_platform: - parts.append(adapter_platform[name]) + # CEK fixups + general fixups BEFORE boot (boot hydrates islands that need these) parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers)) if has_cek: parts.append(CEK_FIXUPS_JS) + + for name in ("dom", "engine", "orchestration", "boot"): + if name in adapter_set and name in adapter_platform: + parts.append(adapter_platform[name]) if has_continuations: parts.append(CONTINUATIONS_JS) if has_dom: diff --git a/sx/sx/geography/cek.sx b/sx/sx/geography/cek.sx index a723d6a..9b6d080 100644 --- a/sx/sx/geography/cek.sx +++ b/sx/sx/geography/cek.sx @@ -148,6 +148,319 @@ "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))) + (letrec + ((container-ref nil) + (dom-stack (list)) + (built-nodes (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)) + (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)) + :else (do (set! ckw false) (append! cch a)))) + cargs) + (append! result {"type" "open" "tag" ctag "attrs" cat}) + (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 () + (console-log "do-parse called") + (reset! error-msg nil) + (reset! step-idx 0) + (reset! parsed-ok false) + (set! dom-stack (list)) + (set! built-nodes (list)) + ;; Clear preview container + (let ((c (dom-query "#render-preview"))) + (console-log "container found:" c) + (when c (set! container-ref c) + (dom-set-prop c "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 up DOM stack with the preview container as root + (set! dom-stack (list (dom-query "#render-preview")))))))) + (do-step (fn () + (console-log "do-step: idx=" (deref step-idx) "len=" (len (deref steps)) "stack=" (len dom-stack) "parent=" (if (empty? dom-stack) "nil" "ok")) + (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)))) + (console-log " step-type=" step-type "parent=" parent) + (cond + (= step-type "open") + (let ((el (dom-create-element (get step "tag") nil)) + (attrs (get step "attrs"))) + (let loop ((i 0)) + (when (< i (len attrs)) + (dom-set-attr el (keyword-name (nth attrs i)) (nth attrs (+ i 1))) + (loop (+ i 2)))) + (console-log " created:" el "appending to:" parent) + (when parent (dom-append parent el)) + (set! dom-stack (append dom-stack (list el))) + (set! built-nodes (append built-nodes (list el)))) + (= step-type "close") + (when (> (len dom-stack) 1) + (set! dom-stack (slice dom-stack 0 (- (len dom-stack) 1)))) + (= step-type "leaf") + (let ((val (get step "expr"))) + (console-log " leaf:" val "parent=" parent) + (when parent + (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))) + (console-log " expr rendered:" rendered) + (when (and parent rendered) + (dom-append parent rendered) + (flush-cssx-to-dom))))) + (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)) + ;; Reset and replay up to step-idx - 1 + (let ((target (- (deref step-idx) 1))) + (do-parse) + (let loop ((n 0)) + (when (< n target) + (do-step) + (loop (+ n 1))))))))) + (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 "") + :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 ;; --------------------------------------------------------------------------- @@ -157,39 +470,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")))))