Add live CEK stepper island — interactive stepping debugger
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m50s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m50s
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:
@@ -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
|
||||
// =========================================================================
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -148,6 +148,317 @@
|
||||
"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))
|
||||
(container-ref nil)
|
||||
(dom-stack (list))
|
||||
(built-nodes (list)))
|
||||
(letrec
|
||||
((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 ()
|
||||
(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")))
|
||||
(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
|
||||
(when container-ref
|
||||
(set! dom-stack (list container-ref))))))))
|
||||
(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) container-ref (last dom-stack))))
|
||||
(cond
|
||||
(= step-type "open")
|
||||
(let ((el (dom-create-element (get step "tag") nil))
|
||||
(attrs (get step "attrs")))
|
||||
;; Set attrs pairwise
|
||||
(let loop ((i 0))
|
||||
(when (< i (len attrs))
|
||||
(dom-set-attr el (keyword-name (nth attrs i)) (nth attrs (+ i 1)))
|
||||
(loop (+ i 2))))
|
||||
(when parent (dom-append parent el))
|
||||
(append! dom-stack el)
|
||||
(append! built-nodes 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")))
|
||||
(when parent
|
||||
(if (string? val)
|
||||
(dom-append parent (create-text-node val))
|
||||
dom-append parent (create-text-node (str val)))))
|
||||
(= step-type "expr")
|
||||
;; Evaluate expression and render to DOM
|
||||
(let ((rendered (render-to-dom (get step "expr") (make-env) nil)))
|
||||
(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.")))
|
||||
(when (deref parsed-ok)
|
||||
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
(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
|
||||
;; ---------------------------------------------------------------------------
|
||||
@@ -157,39 +468,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")))))
|
||||
|
||||
Reference in New Issue
Block a user