diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js
index 1a36e68..69090b8 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-11T04:41:27Z";
+ var SX_VERSION = "2026-03-11T13:57:48Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -974,8 +974,8 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var head = first(template);
return (isSxTruthy((isSxTruthy((typeOf(head) == "symbol")) && (symbolName(head) == "unquote"))) ? trampoline(evalExpr(nth(template, 1), env)) : reduce(function(result, item) { return (isSxTruthy((isSxTruthy((typeOf(item) == "list")) && isSxTruthy((len(item) == 2)) && isSxTruthy((typeOf(first(item)) == "symbol")) && (symbolName(first(item)) == "splice-unquote"))) ? (function() {
var spliced = trampoline(evalExpr(nth(item, 1), env));
- return (isSxTruthy((typeOf(spliced) == "list")) ? concat(result, spliced) : (isSxTruthy(isNil(spliced)) ? result : append(result, spliced)));
-})() : append(result, qqExpand(item, env))); }, [], template));
+ return (isSxTruthy((typeOf(spliced) == "list")) ? concat(result, spliced) : (isSxTruthy(isNil(spliced)) ? result : concat(result, [spliced])));
+})() : concat(result, [qqExpand(item, env)])); }, [], template));
})())); };
// sf-thread-first
@@ -1658,7 +1658,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
var attrExpr = nth(args, (get(state, "i") + 1));
(isSxTruthy(startsWith(attrName, "on-")) ? (function() {
var attrVal = trampoline(evalExpr(attrExpr, env));
- return (isSxTruthy(isCallable(attrVal)) ? domListen(el, slice(attrName, 3), attrVal) : NIL);
+ return (isSxTruthy(isCallable(attrVal)) ? domListen(el, slice(attrName, 3), (isSxTruthy((isSxTruthy(isLambda(attrVal)) && (len(lambdaParams(attrVal)) == 0))) ? function(e) { return callLambda(attrVal, [], lambdaClosure(attrVal)); } : attrVal)) : NIL);
})() : (isSxTruthy((attrName == "bind")) ? (function() {
var attrVal = trampoline(evalExpr(attrExpr, env));
return (isSxTruthy(isSignal(attrVal)) ? bindInput(el, attrVal) : NIL);
@@ -3433,6 +3433,125 @@ callExpr.push(dictGet(kwargs, k)); } }
})(); }, keys(env)); };
+ // === Transpiled from page-helpers (pure data transformation helpers) ===
+
+ // special-form-category-map
+ var specialFormCategoryMap = {"if": "Control Flow", "when": "Control Flow", "cond": "Control Flow", "case": "Control Flow", "and": "Control Flow", "or": "Control Flow", "let": "Binding", "let*": "Binding", "letrec": "Binding", "define": "Binding", "set!": "Binding", "lambda": "Functions & Components", "fn": "Functions & Components", "defcomp": "Functions & Components", "defmacro": "Functions & Components", "begin": "Sequencing & Threading", "do": "Sequencing & Threading", "->": "Sequencing & Threading", "quote": "Quoting", "quasiquote": "Quoting", "reset": "Continuations", "shift": "Continuations", "dynamic-wind": "Guards", "map": "Higher-Order Forms", "map-indexed": "Higher-Order Forms", "filter": "Higher-Order Forms", "reduce": "Higher-Order Forms", "some": "Higher-Order Forms", "every?": "Higher-Order Forms", "for-each": "Higher-Order Forms", "defstyle": "Domain Definitions", "defhandler": "Domain Definitions", "defpage": "Domain Definitions", "defquery": "Domain Definitions", "defaction": "Domain Definitions"};
+
+ // extract-define-kwargs
+ var extractDefineKwargs = function(expr) { return (function() {
+ var result = {};
+ var items = slice(expr, 2);
+ var n = len(items);
+ { var _c = range(0, n); for (var _i = 0; _i < _c.length; _i++) { var idx = _c[_i]; if (isSxTruthy((isSxTruthy(((idx + 1) < n)) && (typeOf(nth(items, idx)) == "keyword")))) {
+ (function() {
+ var key = keywordName(nth(items, idx));
+ var val = nth(items, (idx + 1));
+ return dictSet(result, key, (isSxTruthy((typeOf(val) == "list")) ? (String("(") + String(join(" ", map(serialize, val))) + String(")")) : (String(val))));
+})();
+} } }
+ return result;
+})(); };
+
+ // categorize-special-forms
+ var categorizeSpecialForms = function(parsedExprs) { return (function() {
+ var categories = {};
+ { var _c = parsedExprs; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(expr) == "list")) && isSxTruthy((len(expr) >= 2)) && isSxTruthy((typeOf(first(expr)) == "symbol")) && (symbolName(first(expr)) == "define-special-form")))) {
+ (function() {
+ var name = nth(expr, 1);
+ var kwargs = extractDefineKwargs(expr);
+ var category = sxOr(get(specialFormCategoryMap, name), "Other");
+ if (isSxTruthy(!isSxTruthy(dictHas(categories, category)))) {
+ categories[category] = [];
+}
+ return append_b(get(categories, category), {"name": name, "syntax": sxOr(get(kwargs, "syntax"), ""), "doc": sxOr(get(kwargs, "doc"), ""), "tail-position": sxOr(get(kwargs, "tail-position"), ""), "example": sxOr(get(kwargs, "example"), "")});
+})();
+} } }
+ return categories;
+})(); };
+
+ // build-ref-items-with-href
+ var buildRefItemsWithHref = function(items, basePath, detailKeys, nFields) { return map(function(item) { return (isSxTruthy((nFields == 3)) ? (function() {
+ var name = nth(item, 0);
+ var field2 = nth(item, 1);
+ var field3 = nth(item, 2);
+ return {"name": name, "desc": field2, "exists": field3, "href": (isSxTruthy((isSxTruthy(field3) && some(function(k) { return (k == name); }, detailKeys))) ? (String(basePath) + String(name)) : NIL)};
+})() : (function() {
+ var name = nth(item, 0);
+ var desc = nth(item, 1);
+ return {"name": name, "desc": desc, "href": (isSxTruthy(some(function(k) { return (k == name); }, detailKeys)) ? (String(basePath) + String(name)) : NIL)};
+})()); }, items); };
+
+ // build-reference-data
+ var buildReferenceData = function(slug, rawData, detailKeys) { return (function() { var _m = slug; if (_m == "attributes") return {"req-attrs": buildRefItemsWithHref(get(rawData, "req-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "beh-attrs": buildRefItemsWithHref(get(rawData, "beh-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "uniq-attrs": buildRefItemsWithHref(get(rawData, "uniq-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3)}; if (_m == "headers") return {"req-headers": buildRefItemsWithHref(get(rawData, "req-headers"), "/hypermedia/reference/headers/", detailKeys, 3), "resp-headers": buildRefItemsWithHref(get(rawData, "resp-headers"), "/hypermedia/reference/headers/", detailKeys, 3)}; if (_m == "events") return {"events-list": buildRefItemsWithHref(get(rawData, "events-list"), "/hypermedia/reference/events/", detailKeys, 2)}; if (_m == "js-api") return {"js-api-list": map(function(item) { return {"name": nth(item, 0), "desc": nth(item, 1)}; }, get(rawData, "js-api-list"))}; return {"req-attrs": buildRefItemsWithHref(get(rawData, "req-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "beh-attrs": buildRefItemsWithHref(get(rawData, "beh-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "uniq-attrs": buildRefItemsWithHref(get(rawData, "uniq-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3)}; })(); };
+
+ // build-attr-detail
+ var buildAttrDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"attr-not-found": true} : {"attr-not-found": NIL, "attr-title": slug, "attr-description": get(detail, "description"), "attr-example": get(detail, "example"), "attr-handler": get(detail, "handler"), "attr-demo": get(detail, "demo"), "attr-wire-id": (isSxTruthy(dictHas(detail, "handler")) ? (String("ref-wire-") + String(replace_(replace_(slug, ":", "-"), "*", "star"))) : NIL)}); };
+
+ // build-header-detail
+ var buildHeaderDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"header-not-found": true} : {"header-not-found": NIL, "header-title": slug, "header-direction": get(detail, "direction"), "header-description": get(detail, "description"), "header-example": get(detail, "example"), "header-demo": get(detail, "demo")}); };
+
+ // build-event-detail
+ var buildEventDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"event-not-found": true} : {"event-not-found": NIL, "event-title": slug, "event-description": get(detail, "description"), "event-example": get(detail, "example"), "event-demo": get(detail, "demo")}); };
+
+ // build-component-source
+ var buildComponentSource = function(compData) { return (function() {
+ var compType = get(compData, "type");
+ var name = get(compData, "name");
+ var params = get(compData, "params");
+ var hasChildren = get(compData, "has-children");
+ var bodySx = get(compData, "body-sx");
+ var affinity = get(compData, "affinity");
+ return (isSxTruthy((compType == "not-found")) ? (String(";; component ") + String(name) + String(" not found")) : (function() {
+ var paramStrs = (isSxTruthy(isEmpty(params)) ? (isSxTruthy(hasChildren) ? ["&rest", "children"] : []) : (isSxTruthy(hasChildren) ? append(cons("&key", params), ["&rest", "children"]) : cons("&key", params)));
+ var paramsSx = (String("(") + String(join(" ", paramStrs)) + String(")"));
+ var formName = (isSxTruthy((compType == "island")) ? "defisland" : "defcomp");
+ var affinityStr = (isSxTruthy((isSxTruthy((compType == "component")) && isSxTruthy(!isSxTruthy(isNil(affinity))) && !isSxTruthy((affinity == "auto")))) ? (String(" :affinity ") + String(affinity)) : "");
+ return (String("(") + String(formName) + String(" ") + String(name) + String(" ") + String(paramsSx) + String(affinityStr) + String("\n ") + String(bodySx) + String(")"));
+})());
+})(); };
+
+ // build-bundle-analysis
+ var buildBundleAnalysis = function(pagesRaw, componentsRaw, totalComponents, totalMacros, pureCount, ioCount) { return (function() {
+ var pagesData = [];
+ { var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() {
+ var neededNames = get(page, "needed-names");
+ var n = len(neededNames);
+ var pct = (isSxTruthy((totalComponents > 0)) ? round(((n / totalComponents) * 100)) : 0);
+ var savings = (100 - pct);
+ var pureInPage = 0;
+ var ioInPage = 0;
+ var pageIoRefs = [];
+ var compDetails = [];
+ { var _c = neededNames; for (var _i = 0; _i < _c.length; _i++) { var compName = _c[_i]; (function() {
+ var info = get(componentsRaw, compName);
+ return (isSxTruthy(!isSxTruthy(isNil(info))) ? ((isSxTruthy(get(info, "is-pure")) ? (pureInPage = (pureInPage + 1)) : ((ioInPage = (ioInPage + 1)), forEach(function(ref) { return (isSxTruthy(!isSxTruthy(some(function(r) { return (r == ref); }, pageIoRefs))) ? append_b(pageIoRefs, ref) : NIL); }, sxOr(get(info, "io-refs"), [])))), append_b(compDetails, {"name": compName, "is-pure": get(info, "is-pure"), "affinity": get(info, "affinity"), "render-target": get(info, "render-target"), "io-refs": sxOr(get(info, "io-refs"), []), "deps": sxOr(get(info, "deps"), []), "source": get(info, "source")})) : NIL);
+})(); } }
+ return append_b(pagesData, {"name": get(page, "name"), "path": get(page, "path"), "direct": get(page, "direct"), "needed": n, "pct": pct, "savings": savings, "io-refs": len(pageIoRefs), "pure-in-page": pureInPage, "io-in-page": ioInPage, "components": compDetails});
+})(); } }
+ return {"pages": pagesData, "total-components": totalComponents, "total-macros": totalMacros, "pure-count": pureCount, "io-count": ioCount};
+})(); };
+
+ // build-routing-analysis
+ var buildRoutingAnalysis = function(pagesRaw) { return (function() {
+ var pagesData = [];
+ var clientCount = 0;
+ var serverCount = 0;
+ { var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() {
+ var hasData = get(page, "has-data");
+ var contentSrc = sxOr(get(page, "content-src"), "");
+ var mode = NIL;
+ var reason = "";
+ (isSxTruthy(hasData) ? ((mode = "server"), (reason = "Has :data expression — needs server IO"), (serverCount = (serverCount + 1))) : (isSxTruthy(isEmpty(contentSrc)) ? ((mode = "server"), (reason = "No content expression"), (serverCount = (serverCount + 1))) : ((mode = "client"), (clientCount = (clientCount + 1)))));
+ return append_b(pagesData, {"name": get(page, "name"), "path": get(page, "path"), "mode": mode, "has-data": hasData, "content-expr": (isSxTruthy((len(contentSrc) > 80)) ? (String(slice(contentSrc, 0, 80)) + String("...")) : contentSrc), "reason": reason});
+})(); } }
+ return {"pages": pagesData, "total-pages": (clientCount + serverCount), "client-count": clientCount, "server-count": serverCount};
+})(); };
+
+ // build-affinity-analysis
+ var buildAffinityAnalysis = function(demoComponents, pagePlans) { return {"components": demoComponents, "page-plans": pagePlans}; };
+
+
// === Transpiled from router (client-side route matching) ===
// split-path-segments
@@ -3947,7 +4066,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
}
}
- function nowMs() { return Date.now(); }
+ function nowMs() { return (typeof performance !== "undefined") ? performance.now() : Date.now(); }
function parseHeaderValue(s) {
if (!s) return null;
@@ -5060,6 +5179,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
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;
// Expose deps module functions as primitives so runtime-evaluated SX code
// (e.g. test-deps.sx in browser) can call them
@@ -5090,6 +5213,19 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
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;
+
// =========================================================================
// Async IO: Promise-aware rendering for client-side IO primitives
// =========================================================================
@@ -5823,6 +5959,15 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
transitiveIoRefs: transitiveIoRefs,
computeAllIoRefs: computeAllIoRefs,
componentPure_p: componentPure_p,
+ categorizeSpecialForms: categorizeSpecialForms,
+ buildReferenceData: buildReferenceData,
+ buildAttrDetail: buildAttrDetail,
+ buildHeaderDetail: buildHeaderDetail,
+ buildEventDetail: buildEventDetail,
+ buildComponentSource: buildComponentSource,
+ buildBundleAnalysis: buildBundleAnalysis,
+ buildRoutingAnalysis: buildRoutingAnalysis,
+ buildAffinityAnalysis: buildAffinityAnalysis,
splitPathSegments: splitPathSegments,
parseRoutePattern: parseRoutePattern,
matchRoute: matchRoute,
diff --git a/shared/sx/pages.py b/shared/sx/pages.py
index 51dc76d..2050a5b 100644
--- a/shared/sx/pages.py
+++ b/shared/sx/pages.py
@@ -170,7 +170,11 @@ async def _eval_slot(expr: Any, env: dict, ctx: Any) -> str:
Expands component calls (so IO in the body executes) but serializes
the result as SX wire format, not HTML.
"""
- from .async_eval import async_eval_slot_to_sx
+ import os
+ if os.environ.get("SX_USE_REF") == "1":
+ from .ref.async_eval_ref import async_eval_slot_to_sx
+ else:
+ from .async_eval import async_eval_slot_to_sx
return await async_eval_slot_to_sx(expr, env, ctx)
diff --git a/shared/sx/ref/adapter-async.sx b/shared/sx/ref/adapter-async.sx
new file mode 100644
index 0000000..f57dfe3
--- /dev/null
+++ b/shared/sx/ref/adapter-async.sx
@@ -0,0 +1,1198 @@
+;; ==========================================================================
+;; adapter-async.sx — Async rendering and serialization adapter
+;;
+;; Async versions of adapter-html.sx (render) and adapter-sx.sx (aser)
+;; for use with I/O-capable server environments (Python async, JS promises).
+;;
+;; Structurally identical to the sync adapters but uses async primitives:
+;; async-eval — evaluate with I/O interception (platform primitive)
+;; async-render — defined here, async HTML rendering
+;; async-aser — defined here, async SX wire format
+;;
+;; All functions in this file are emitted as async by the bootstrapper.
+;; Calls to other async functions receive await automatically.
+;;
+;; Depends on:
+;; eval.sx — cond-scheme?, eval-cond-scheme, eval-cond-clojure,
+;; expand-macro, env-merge, lambda?, component?, island?,
+;; macro?, lambda-closure, lambda-params, lambda-body
+;; render.sx — HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,
+;; render-attrs, definition-form?, process-bindings, eval-cond
+;;
+;; Platform primitives (provided by host):
+;; (async-eval expr env ctx) — evaluate with I/O interception
+;; (io-primitive? name) — check if name is I/O primitive
+;; (execute-io name args kw ctx) — execute an I/O primitive
+;; (expand-components?) — context var: expand components in aser?
+;; (svg-context?) — context var: in SVG rendering context?
+;; (svg-context-set! val) — set SVG context
+;; (svg-context-reset! token) — reset SVG context
+;; (css-class-collect! val) — collect CSS classes for bundling
+;; (is-raw-html? x) — check if value is raw HTML marker
+;; (raw-html-content x) — extract HTML string from marker
+;; (make-raw-html s) — wrap string as raw HTML
+;; (async-coroutine? x) — check if value is a coroutine
+;; (async-await! x) — await a coroutine value
+;; ==========================================================================
+
+
+;; --------------------------------------------------------------------------
+;; Async HTML renderer
+;; --------------------------------------------------------------------------
+
+(define-async async-render
+ (fn (expr env ctx)
+ (case (type-of expr)
+ "nil" ""
+ "boolean" ""
+ "string" (escape-html expr)
+ "number" (escape-html (str expr))
+ "raw-html" (raw-html-content expr)
+ "symbol" (let ((val (async-eval expr env ctx)))
+ (async-render val env ctx))
+ "keyword" (escape-html (keyword-name expr))
+ "list" (if (empty? expr) "" (async-render-list expr env ctx))
+ "dict" ""
+ :else (escape-html (str expr)))))
+
+
+(define-async async-render-list
+ (fn (expr env ctx)
+ (let ((head (first expr)))
+ (if (not (= (type-of head) "symbol"))
+ ;; Non-symbol head — data list, render each item
+ (if (or (lambda? head) (= (type-of head) "list"))
+ ;; Lambda/list call — eval then render
+ (async-render (async-eval expr env ctx) env ctx)
+ ;; Data list
+ (join "" (async-map-render expr env ctx)))
+
+ ;; Symbol head — dispatch
+ (let ((name (symbol-name head))
+ (args (rest expr)))
+ (cond
+ ;; I/O primitive
+ (io-primitive? name)
+ (async-render (async-eval expr env ctx) env ctx)
+
+ ;; raw!
+ (= name "raw!")
+ (async-render-raw args env ctx)
+
+ ;; Fragment
+ (= name "<>")
+ (join "" (async-map-render args env ctx))
+
+ ;; html: prefix
+ (starts-with? name "html:")
+ (async-render-element (slice name 5) args env ctx)
+
+ ;; Render-aware special form (but check HTML tag + keyword first)
+ (async-render-form? name)
+ (if (and (contains? HTML_TAGS name)
+ (or (and (> (len expr) 1) (= (type-of (nth expr 1)) "keyword"))
+ (svg-context?)))
+ (async-render-element name args env ctx)
+ (dispatch-async-render-form name expr env ctx))
+
+ ;; Macro
+ (and (env-has? env name) (macro? (env-get env name)))
+ (async-render
+ (trampoline (expand-macro (env-get env name) args env))
+ env ctx)
+
+ ;; HTML tag
+ (contains? HTML_TAGS name)
+ (async-render-element name args env ctx)
+
+ ;; Island (~name)
+ (and (starts-with? name "~")
+ (env-has? env name)
+ (island? (env-get env name)))
+ (async-render-island (env-get env name) args env ctx)
+
+ ;; Component (~name)
+ (starts-with? name "~")
+ (let ((val (if (env-has? env name) (env-get env name) nil)))
+ (cond
+ (component? val) (async-render-component val args env ctx)
+ (macro? val) (async-render (trampoline (expand-macro val args env)) env ctx)
+ :else (async-render (async-eval expr env ctx) env ctx)))
+
+ ;; Custom element (has - and keyword arg)
+ (and (> (index-of name "-") 0)
+ (> (len expr) 1)
+ (= (type-of (nth expr 1)) "keyword"))
+ (async-render-element name args env ctx)
+
+ ;; SVG context
+ (svg-context?)
+ (async-render-element name args env ctx)
+
+ ;; Fallback — eval then render
+ :else
+ (async-render (async-eval expr env ctx) env ctx)))))))
+
+
+;; --------------------------------------------------------------------------
+;; async-render-raw — handle (raw! ...) in async context
+;; --------------------------------------------------------------------------
+
+(define-async async-render-raw
+ (fn (args env ctx)
+ (let ((parts (list)))
+ (for-each
+ (fn (arg)
+ (let ((val (async-eval arg env ctx)))
+ (cond
+ (is-raw-html? val) (append! parts (raw-html-content val))
+ (= (type-of val) "string") (append! parts val)
+ (and (not (nil? val)) (not (= val false)))
+ (append! parts (str val)))))
+ args)
+ (join "" parts))))
+
+
+;; --------------------------------------------------------------------------
+;; async-render-element — render an HTML element with async arg evaluation
+;; --------------------------------------------------------------------------
+
+(define-async async-render-element
+ (fn (tag args env ctx)
+ (let ((attrs (dict))
+ (children (list)))
+ ;; Parse keyword attrs and children
+ (async-parse-element-args args attrs children env ctx)
+ ;; Collect CSS classes
+ (let ((class-val (dict-get attrs "class")))
+ (when (and (not (nil? class-val)) (not (= class-val false)))
+ (css-class-collect! (str class-val))))
+ ;; Build opening tag
+ (let ((opening (str "<" tag (render-attrs attrs) ">")))
+ (if (contains? VOID_ELEMENTS tag)
+ opening
+ (let ((token (if (or (= tag "svg") (= tag "math"))
+ (svg-context-set! true)
+ nil))
+ (child-html (join "" (async-map-render children env ctx))))
+ (when token (svg-context-reset! token))
+ (str opening child-html "" tag ">")))))))
+
+
+;; --------------------------------------------------------------------------
+;; async-parse-element-args — parse :key val pairs + children, async eval
+;; --------------------------------------------------------------------------
+;; Uses for-each + mutable state instead of reduce, because the bootstrapper
+;; compiles inline for-each lambdas as for loops (which can contain await).
+
+(define-async async-parse-element-args
+ (fn (args attrs children env ctx)
+ (let ((skip false)
+ (i 0))
+ (for-each
+ (fn (arg)
+ (if skip
+ (do (set! skip false)
+ (set! i (inc i)))
+ (if (and (= (type-of arg) "keyword")
+ (< (inc i) (len args)))
+ (let ((val (async-eval (nth args (inc i)) env ctx)))
+ (dict-set! attrs (keyword-name arg) val)
+ (set! skip true)
+ (set! i (inc i)))
+ (do
+ (append! children arg)
+ (set! i (inc i))))))
+ args))))
+
+
+;; --------------------------------------------------------------------------
+;; async-render-component — expand and render a component asynchronously
+;; --------------------------------------------------------------------------
+
+(define-async async-render-component
+ (fn (comp args env ctx)
+ (let ((kwargs (dict))
+ (children (list)))
+ ;; Parse keyword args and children
+ (async-parse-kw-args args kwargs children env ctx)
+ ;; Build env: closure + caller env + params
+ (let ((local (env-merge (component-closure comp) env)))
+ (for-each
+ (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
+ (component-params comp))
+ (when (component-has-children? comp)
+ (env-set! local "children"
+ (make-raw-html
+ (join "" (async-map-render children env ctx)))))
+ (async-render (component-body comp) local ctx)))))
+
+
+;; --------------------------------------------------------------------------
+;; async-render-island — SSR render of reactive island with hydration markers
+;; --------------------------------------------------------------------------
+
+(define-async async-render-island
+ (fn (island args env ctx)
+ (let ((kwargs (dict))
+ (children (list)))
+ (async-parse-kw-args args kwargs children env ctx)
+ (let ((local (env-merge (component-closure island) env))
+ (island-name (component-name island)))
+ (for-each
+ (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
+ (component-params island))
+ (when (component-has-children? island)
+ (env-set! local "children"
+ (make-raw-html
+ (join "" (async-map-render children env ctx)))))
+ (let ((body-html (async-render (component-body island) local ctx))
+ (state-json (serialize-island-state kwargs)))
+ (str ""
+ body-html
+ ""))))))
+
+
+;; --------------------------------------------------------------------------
+;; async-render-lambda — render lambda body in HTML context
+;; --------------------------------------------------------------------------
+
+(define-async async-render-lambda
+ (fn (f args env ctx)
+ (let ((local (env-merge (lambda-closure f) env)))
+ (for-each-indexed
+ (fn (i p) (env-set! local p (nth args i)))
+ (lambda-params f))
+ (async-render (lambda-body f) local ctx))))
+
+
+;; --------------------------------------------------------------------------
+;; async-parse-kw-args — parse keyword args and children with async eval
+;; --------------------------------------------------------------------------
+
+(define-async async-parse-kw-args
+ (fn (args kwargs children env ctx)
+ (let ((skip false)
+ (i 0))
+ (for-each
+ (fn (arg)
+ (if skip
+ (do (set! skip false)
+ (set! i (inc i)))
+ (if (and (= (type-of arg) "keyword")
+ (< (inc i) (len args)))
+ (let ((val (async-eval (nth args (inc i)) env ctx)))
+ (dict-set! kwargs (keyword-name arg) val)
+ (set! skip true)
+ (set! i (inc i)))
+ (do
+ (append! children arg)
+ (set! i (inc i))))))
+ args))))
+
+
+;; --------------------------------------------------------------------------
+;; async-map-render — map async-render over a list, return list of strings
+;; --------------------------------------------------------------------------
+;; Bootstrapper emits this as: [await async_render(x, env, ctx) for x in exprs]
+
+(define-async async-map-render
+ (fn (exprs env ctx)
+ (let ((results (list)))
+ (for-each
+ (fn (x) (append! results (async-render x env ctx)))
+ exprs)
+ results)))
+
+
+;; --------------------------------------------------------------------------
+;; Render-aware form classification
+;; --------------------------------------------------------------------------
+
+(define ASYNC_RENDER_FORMS
+ (list "if" "when" "cond" "case" "let" "let*" "begin" "do"
+ "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
+ "map" "map-indexed" "filter" "for-each"))
+
+(define async-render-form?
+ (fn (name)
+ (contains? ASYNC_RENDER_FORMS name)))
+
+
+;; --------------------------------------------------------------------------
+;; dispatch-async-render-form — async special form rendering for HTML output
+;; --------------------------------------------------------------------------
+;;
+;; Uses cond-scheme? from eval.sx (the FIXED version with every? check)
+;; and eval-cond from render.sx for correct scheme/clojure classification.
+
+(define-async dispatch-async-render-form
+ (fn (name expr env ctx)
+ (cond
+ ;; if
+ (= name "if")
+ (let ((cond-val (async-eval (nth expr 1) env ctx)))
+ (if cond-val
+ (async-render (nth expr 2) env ctx)
+ (if (> (len expr) 3)
+ (async-render (nth expr 3) env ctx)
+ "")))
+
+ ;; when
+ (= name "when")
+ (if (not (async-eval (nth expr 1) env ctx))
+ ""
+ (join "" (async-map-render (slice expr 2) env ctx)))
+
+ ;; cond — uses cond-scheme? (every? check) from eval.sx
+ (= name "cond")
+ (let ((clauses (rest expr)))
+ (if (cond-scheme? clauses)
+ (async-render-cond-scheme clauses env ctx)
+ (async-render-cond-clojure clauses env ctx)))
+
+ ;; case
+ (= name "case")
+ (async-render (async-eval expr env ctx) env ctx)
+
+ ;; let / let*
+ (or (= name "let") (= name "let*"))
+ (let ((local (async-process-bindings (nth expr 1) env ctx)))
+ (join "" (async-map-render (slice expr 2) local ctx)))
+
+ ;; begin / do
+ (or (= name "begin") (= name "do"))
+ (join "" (async-map-render (rest expr) env ctx))
+
+ ;; Definition forms
+ (definition-form? name)
+ (do (async-eval expr env ctx) "")
+
+ ;; map
+ (= name "map")
+ (let ((f (async-eval (nth expr 1) env ctx))
+ (coll (async-eval (nth expr 2) env ctx)))
+ (join ""
+ (async-map-fn-render f coll env ctx)))
+
+ ;; map-indexed
+ (= name "map-indexed")
+ (let ((f (async-eval (nth expr 1) env ctx))
+ (coll (async-eval (nth expr 2) env ctx)))
+ (join ""
+ (async-map-indexed-fn-render f coll env ctx)))
+
+ ;; filter — eval fully then render
+ (= name "filter")
+ (async-render (async-eval expr env ctx) env ctx)
+
+ ;; for-each (render variant)
+ (= name "for-each")
+ (let ((f (async-eval (nth expr 1) env ctx))
+ (coll (async-eval (nth expr 2) env ctx)))
+ (join ""
+ (async-map-fn-render f coll env ctx)))
+
+ ;; Fallback
+ :else
+ (async-render (async-eval expr env ctx) env ctx))))
+
+
+;; --------------------------------------------------------------------------
+;; async-render-cond-scheme — scheme-style cond for render mode
+;; --------------------------------------------------------------------------
+
+(define-async async-render-cond-scheme
+ (fn (clauses env ctx)
+ (if (empty? clauses)
+ ""
+ (let ((clause (first clauses))
+ (test (first clause))
+ (body (nth clause 1)))
+ (if (or (and (= (type-of test) "symbol")
+ (or (= (symbol-name test) "else")
+ (= (symbol-name test) ":else")))
+ (and (= (type-of test) "keyword")
+ (= (keyword-name test) "else")))
+ (async-render body env ctx)
+ (if (async-eval test env ctx)
+ (async-render body env ctx)
+ (async-render-cond-scheme (rest clauses) env ctx)))))))
+
+
+;; --------------------------------------------------------------------------
+;; async-render-cond-clojure — clojure-style cond for render mode
+;; --------------------------------------------------------------------------
+
+(define-async async-render-cond-clojure
+ (fn (clauses env ctx)
+ (if (< (len clauses) 2)
+ ""
+ (let ((test (first clauses))
+ (body (nth clauses 1)))
+ (if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else"))
+ (and (= (type-of test) "symbol")
+ (or (= (symbol-name test) "else")
+ (= (symbol-name test) ":else"))))
+ (async-render body env ctx)
+ (if (async-eval test env ctx)
+ (async-render body env ctx)
+ (async-render-cond-clojure (slice clauses 2) env ctx)))))))
+
+
+;; --------------------------------------------------------------------------
+;; async-process-bindings — evaluate let-bindings asynchronously
+;; --------------------------------------------------------------------------
+
+(define-async async-process-bindings
+ (fn (bindings env ctx)
+ (let ((local (merge env)))
+ (if (and (= (type-of bindings) "list") (not (empty? bindings)))
+ (if (= (type-of (first bindings)) "list")
+ ;; Scheme-style: ((name val) ...)
+ (for-each
+ (fn (pair)
+ (when (and (= (type-of pair) "list") (>= (len pair) 2))
+ (let ((name (if (= (type-of (first pair)) "symbol")
+ (symbol-name (first pair))
+ (str (first pair)))))
+ (env-set! local name (async-eval (nth pair 1) local ctx)))))
+ bindings)
+ ;; Clojure-style: (name val name val ...)
+ (async-process-bindings-flat bindings local ctx)))
+ local)))
+
+
+(define-async async-process-bindings-flat
+ (fn (bindings local ctx)
+ (let ((skip false)
+ (i 0))
+ (for-each
+ (fn (item)
+ (if skip
+ (do (set! skip false)
+ (set! i (inc i)))
+ (do
+ (let ((name (if (= (type-of item) "symbol")
+ (symbol-name item)
+ (str item))))
+ (when (< (inc i) (len bindings))
+ (env-set! local name
+ (async-eval (nth bindings (inc i)) local ctx))))
+ (set! skip true)
+ (set! i (inc i)))))
+ bindings))))
+
+
+;; --------------------------------------------------------------------------
+;; async-map-fn-render — map a lambda/callable over collection for render
+;; --------------------------------------------------------------------------
+
+(define-async async-map-fn-render
+ (fn (f coll env ctx)
+ (let ((results (list)))
+ (for-each
+ (fn (item)
+ (if (lambda? f)
+ (append! results (async-render-lambda f (list item) env ctx))
+ (let ((r (async-invoke f item)))
+ (append! results (async-render r env ctx)))))
+ coll)
+ results)))
+
+
+;; --------------------------------------------------------------------------
+;; async-map-indexed-fn-render — map-indexed variant for render
+;; --------------------------------------------------------------------------
+
+(define-async async-map-indexed-fn-render
+ (fn (f coll env ctx)
+ (let ((results (list))
+ (i 0))
+ (for-each
+ (fn (item)
+ (if (lambda? f)
+ (append! results (async-render-lambda f (list i item) env ctx))
+ (let ((r (async-invoke f i item)))
+ (append! results (async-render r env ctx))))
+ (set! i (inc i)))
+ coll)
+ results)))
+
+
+;; --------------------------------------------------------------------------
+;; async-invoke — call a native callable, await if coroutine
+;; --------------------------------------------------------------------------
+
+(define-async async-invoke
+ (fn (f &rest args)
+ (let ((r (apply f args)))
+ (if (async-coroutine? r)
+ (async-await! r)
+ r))))
+
+
+;; ==========================================================================
+;; Async SX wire format (aser)
+;; ==========================================================================
+
+(define-async async-aser
+ (fn (expr env ctx)
+ (case (type-of expr)
+ "number" expr
+ "string" expr
+ "boolean" expr
+ "nil" nil
+
+ "symbol"
+ (let ((name (symbol-name expr)))
+ (cond
+ (env-has? env name) (env-get env name)
+ (primitive? name) (get-primitive name)
+ (= name "true") true
+ (= name "false") false
+ (= name "nil") nil
+ :else (error (str "Undefined symbol: " name))))
+
+ "keyword" (keyword-name expr)
+
+ "dict" (async-aser-dict expr env ctx)
+
+ "list"
+ (if (empty? expr)
+ (list)
+ (async-aser-list expr env ctx))
+
+ :else expr)))
+
+
+(define-async async-aser-dict
+ (fn (expr env ctx)
+ (let ((result (dict)))
+ (for-each
+ (fn (key)
+ (dict-set! result key (async-aser (dict-get expr key) env ctx)))
+ (keys expr))
+ result)))
+
+
+;; --------------------------------------------------------------------------
+;; async-aser-list — dispatch on list head for aser mode
+;; --------------------------------------------------------------------------
+
+(define-async async-aser-list
+ (fn (expr env ctx)
+ (let ((head (first expr))
+ (args (rest expr)))
+ (if (not (= (type-of head) "symbol"))
+ ;; Non-symbol head
+ (if (or (lambda? head) (= (type-of head) "list"))
+ ;; Function/list call — eval fully
+ (async-aser-eval-call head args env ctx)
+ ;; Data list — aser each
+ (async-aser-map-list expr env ctx))
+
+ ;; Symbol head — dispatch
+ (let ((name (symbol-name head)))
+ (cond
+ ;; I/O primitive
+ (io-primitive? name)
+ (async-eval expr env ctx)
+
+ ;; Fragment
+ (= name "<>")
+ (async-aser-fragment args env ctx)
+
+ ;; raw!
+ (= name "raw!")
+ (async-aser-call "raw!" args env ctx)
+
+ ;; html: prefix
+ (starts-with? name "html:")
+ (async-aser-call (slice name 5) args env ctx)
+
+ ;; Component call (~name)
+ (starts-with? name "~")
+ (let ((val (if (env-has? env name) (env-get env name) nil)))
+ (cond
+ (macro? val)
+ (async-aser (trampoline (expand-macro val args env)) env ctx)
+ (and (component? val)
+ (or (expand-components?)
+ (= (component-affinity val) "server")))
+ (async-aser-component val args env ctx)
+ :else
+ (async-aser-call name args env ctx)))
+
+ ;; Special/HO forms
+ (or (async-aser-form? name))
+ (if (and (contains? HTML_TAGS name)
+ (or (and (> (len expr) 1) (= (type-of (nth expr 1)) "keyword"))
+ (svg-context?)))
+ (async-aser-call name args env ctx)
+ (dispatch-async-aser-form name expr env ctx))
+
+ ;; HTML tag
+ (contains? HTML_TAGS name)
+ (async-aser-call name args env ctx)
+
+ ;; Macro
+ (and (env-has? env name) (macro? (env-get env name)))
+ (async-aser (trampoline (expand-macro (env-get env name) args env)) env ctx)
+
+ ;; Custom element
+ (and (> (index-of name "-") 0)
+ (> (len expr) 1)
+ (= (type-of (nth expr 1)) "keyword"))
+ (async-aser-call name args env ctx)
+
+ ;; SVG context
+ (svg-context?)
+ (async-aser-call name args env ctx)
+
+ ;; Fallback — function/lambda call
+ :else
+ (async-aser-eval-call head args env ctx)))))))
+
+
+;; --------------------------------------------------------------------------
+;; async-aser-eval-call — evaluate a function call fully in aser mode
+;; --------------------------------------------------------------------------
+
+(define-async async-aser-eval-call
+ (fn (head args env ctx)
+ (let ((f (async-eval head env ctx))
+ (evaled-args (async-eval-args args env ctx)))
+ (cond
+ (and (callable? f) (not (lambda? f)) (not (component? f)))
+ (async-invoke f evaled-args)
+ (lambda? f)
+ (let ((local (env-merge (lambda-closure f) env)))
+ (for-each-indexed
+ (fn (i p) (env-set! local p (nth evaled-args i)))
+ (lambda-params f))
+ (async-aser (lambda-body f) local ctx))
+ (component? f)
+ (async-aser-call (str "~" (component-name f)) args env ctx)
+ (island? f)
+ (async-aser-call (str "~" (component-name f)) args env ctx)
+ :else
+ (error (str "Not callable: " (inspect f)))))))
+
+
+;; --------------------------------------------------------------------------
+;; async-eval-args — evaluate a list of args asynchronously
+;; --------------------------------------------------------------------------
+
+(define-async async-eval-args
+ (fn (args env ctx)
+ (let ((results (list)))
+ (for-each
+ (fn (a) (append! results (async-eval a env ctx)))
+ args)
+ results)))
+
+
+;; --------------------------------------------------------------------------
+;; async-aser-map-list — aser each element of a list
+;; --------------------------------------------------------------------------
+
+(define-async async-aser-map-list
+ (fn (exprs env ctx)
+ (let ((results (list)))
+ (for-each
+ (fn (x) (append! results (async-aser x env ctx)))
+ exprs)
+ results)))
+
+
+;; --------------------------------------------------------------------------
+;; async-aser-fragment — serialize (<> child1 child2 ...) in aser mode
+;; --------------------------------------------------------------------------
+
+(define-async async-aser-fragment
+ (fn (children env ctx)
+ (let ((parts (list)))
+ (for-each
+ (fn (c)
+ (let ((result (async-aser c env ctx)))
+ (if (= (type-of result) "list")
+ (for-each
+ (fn (item)
+ (when (not (nil? item))
+ (append! parts (serialize item))))
+ result)
+ (when (not (nil? result))
+ (append! parts (serialize result))))))
+ children)
+ (if (empty? parts)
+ (make-sx-expr "")
+ (make-sx-expr (str "(<> " (join " " parts) ")"))))))
+
+
+;; --------------------------------------------------------------------------
+;; async-aser-component — expand component server-side in aser mode
+;; --------------------------------------------------------------------------
+
+(define-async async-aser-component
+ (fn (comp args env ctx)
+ (let ((kwargs (dict))
+ (children (list)))
+ (async-parse-aser-kw-args args kwargs children env ctx)
+ (let ((local (env-merge (component-closure comp) env)))
+ (for-each
+ (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
+ (component-params comp))
+ (when (component-has-children? comp)
+ (let ((child-parts (list)))
+ (for-each
+ (fn (c)
+ (let ((result (async-aser c env ctx)))
+ (if (list? result)
+ (for-each
+ (fn (item)
+ (when (not (nil? item))
+ (append! child-parts (serialize item))))
+ result)
+ (when (not (nil? result))
+ (append! child-parts (serialize result))))))
+ children)
+ (env-set! local "children"
+ (make-sx-expr (str "(<> " (join " " child-parts) ")")))))
+ (async-aser (component-body comp) local ctx)))))
+
+
+;; --------------------------------------------------------------------------
+;; async-parse-aser-kw-args — parse keyword args for aser mode
+;; --------------------------------------------------------------------------
+
+(define-async async-parse-aser-kw-args
+ (fn (args kwargs children env ctx)
+ (let ((skip false)
+ (i 0))
+ (for-each
+ (fn (arg)
+ (if skip
+ (do (set! skip false)
+ (set! i (inc i)))
+ (if (and (= (type-of arg) "keyword")
+ (< (inc i) (len args)))
+ (let ((val (async-aser (nth args (inc i)) env ctx)))
+ (dict-set! kwargs (keyword-name arg) val)
+ (set! skip true)
+ (set! i (inc i)))
+ (do
+ (append! children arg)
+ (set! i (inc i))))))
+ args))))
+
+
+;; --------------------------------------------------------------------------
+;; async-aser-call — serialize an SX call (tag or component) in aser mode
+;; --------------------------------------------------------------------------
+
+(define-async async-aser-call
+ (fn (name args env ctx)
+ (let ((token (if (or (= name "svg") (= name "math"))
+ (svg-context-set! true)
+ nil))
+ (parts (list name))
+ (skip false)
+ (i 0))
+ (for-each
+ (fn (arg)
+ (if skip
+ (do (set! skip false)
+ (set! i (inc i)))
+ (if (and (= (type-of arg) "keyword")
+ (< (inc i) (len args)))
+ (let ((val (async-aser (nth args (inc i)) env ctx)))
+ (when (not (nil? val))
+ (append! parts (str ":" (keyword-name arg)))
+ (if (= (type-of val) "list")
+ (let ((live (filter (fn (v) (not (nil? v))) val)))
+ (if (empty? live)
+ (append! parts "nil")
+ (let ((items (map serialize live)))
+ (if (some (fn (v) (sx-expr? v)) live)
+ (append! parts (str "(<> " (join " " items) ")"))
+ (append! parts (str "(list " (join " " items) ")"))))))
+ (append! parts (serialize val))))
+ (set! skip true)
+ (set! i (inc i)))
+ (let ((result (async-aser arg env ctx)))
+ (when (not (nil? result))
+ (if (= (type-of result) "list")
+ (for-each
+ (fn (item)
+ (when (not (nil? item))
+ (append! parts (serialize item))))
+ result)
+ (append! parts (serialize result))))
+ (set! i (inc i))))))
+ args)
+ (when token (svg-context-reset! token))
+ (make-sx-expr (str "(" (join " " parts) ")")))))
+
+
+;; --------------------------------------------------------------------------
+;; Aser form classification
+;; --------------------------------------------------------------------------
+
+(define ASYNC_ASER_FORM_NAMES
+ (list "if" "when" "cond" "case" "and" "or"
+ "let" "let*" "lambda" "fn"
+ "define" "defcomp" "defmacro" "defstyle"
+ "defhandler" "defpage" "defquery" "defaction"
+ "begin" "do" "quote" "->" "set!" "defisland"))
+
+(define ASYNC_ASER_HO_NAMES
+ (list "map" "map-indexed" "filter" "for-each"))
+
+(define async-aser-form?
+ (fn (name)
+ (or (contains? ASYNC_ASER_FORM_NAMES name)
+ (contains? ASYNC_ASER_HO_NAMES name))))
+
+
+;; --------------------------------------------------------------------------
+;; dispatch-async-aser-form — evaluate special/HO forms in aser mode
+;; --------------------------------------------------------------------------
+;;
+;; Uses cond-scheme? from eval.sx (the FIXED version with every? check).
+
+(define-async dispatch-async-aser-form
+ (fn (name expr env ctx)
+ (let ((args (rest expr)))
+ (cond
+ ;; if
+ (= name "if")
+ (let ((cond-val (async-eval (first args) env ctx)))
+ (if cond-val
+ (async-aser (nth args 1) env ctx)
+ (if (> (len args) 2)
+ (async-aser (nth args 2) env ctx)
+ nil)))
+
+ ;; when
+ (= name "when")
+ (if (not (async-eval (first args) env ctx))
+ nil
+ (let ((result nil))
+ (for-each
+ (fn (body) (set! result (async-aser body env ctx)))
+ (rest args))
+ result))
+
+ ;; cond — uses cond-scheme? (every? check)
+ (= name "cond")
+ (if (cond-scheme? args)
+ (async-aser-cond-scheme args env ctx)
+ (async-aser-cond-clojure args env ctx))
+
+ ;; case
+ (= name "case")
+ (let ((match-val (async-eval (first args) env ctx)))
+ (async-aser-case-loop match-val (rest args) env ctx))
+
+ ;; let / let*
+ (or (= name "let") (= name "let*"))
+ (let ((local (async-process-bindings (first args) env ctx))
+ (result nil))
+ (for-each
+ (fn (body) (set! result (async-aser body local ctx)))
+ (rest args))
+ result)
+
+ ;; begin / do
+ (or (= name "begin") (= name "do"))
+ (let ((result nil))
+ (for-each
+ (fn (body) (set! result (async-aser body env ctx)))
+ args)
+ result)
+
+ ;; and — short-circuit via flag to avoid 'some' with async lambda
+ (= name "and")
+ (let ((result true)
+ (stop false))
+ (for-each (fn (arg)
+ (when (not stop)
+ (set! result (async-eval arg env ctx))
+ (when (not result)
+ (set! stop true))))
+ args)
+ result)
+
+ ;; or — short-circuit via flag to avoid 'some' with async lambda
+ (= name "or")
+ (let ((result false)
+ (stop false))
+ (for-each (fn (arg)
+ (when (not stop)
+ (set! result (async-eval arg env ctx))
+ (when result
+ (set! stop true))))
+ args)
+ result)
+
+ ;; lambda / fn
+ (or (= name "lambda") (= name "fn"))
+ (sf-lambda args env)
+
+ ;; quote
+ (= name "quote")
+ (if (empty? args) nil (first args))
+
+ ;; -> thread-first
+ (= name "->")
+ (async-aser-thread-first args env ctx)
+
+ ;; set!
+ (= name "set!")
+ (let ((value (async-eval (nth args 1) env ctx)))
+ (env-set! env (symbol-name (first args)) value)
+ value)
+
+ ;; map
+ (= name "map")
+ (async-aser-ho-map args env ctx)
+
+ ;; map-indexed
+ (= name "map-indexed")
+ (async-aser-ho-map-indexed args env ctx)
+
+ ;; filter
+ (= name "filter")
+ (async-eval expr env ctx)
+
+ ;; for-each
+ (= name "for-each")
+ (async-aser-ho-for-each args env ctx)
+
+ ;; defisland — evaluate AND serialize
+ (= name "defisland")
+ (do (async-eval expr env ctx)
+ (serialize expr))
+
+ ;; Definition forms — evaluate for side effects
+ (or (= name "define") (= name "defcomp") (= name "defmacro")
+ (= name "defstyle") (= name "defhandler") (= name "defpage")
+ (= name "defquery") (= name "defaction"))
+ (do (async-eval expr env ctx) nil)
+
+ ;; Fallback
+ :else
+ (async-eval expr env ctx)))))
+
+
+;; --------------------------------------------------------------------------
+;; async-aser-cond-scheme — scheme-style cond for aser mode
+;; --------------------------------------------------------------------------
+
+(define-async async-aser-cond-scheme
+ (fn (clauses env ctx)
+ (if (empty? clauses)
+ nil
+ (let ((clause (first clauses))
+ (test (first clause))
+ (body (nth clause 1)))
+ (if (or (and (= (type-of test) "symbol")
+ (or (= (symbol-name test) "else")
+ (= (symbol-name test) ":else")))
+ (and (= (type-of test) "keyword")
+ (= (keyword-name test) "else")))
+ (async-aser body env ctx)
+ (if (async-eval test env ctx)
+ (async-aser body env ctx)
+ (async-aser-cond-scheme (rest clauses) env ctx)))))))
+
+
+;; --------------------------------------------------------------------------
+;; async-aser-cond-clojure — clojure-style cond for aser mode
+;; --------------------------------------------------------------------------
+
+(define-async async-aser-cond-clojure
+ (fn (clauses env ctx)
+ (if (< (len clauses) 2)
+ nil
+ (let ((test (first clauses))
+ (body (nth clauses 1)))
+ (if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else"))
+ (and (= (type-of test) "symbol")
+ (or (= (symbol-name test) "else")
+ (= (symbol-name test) ":else"))))
+ (async-aser body env ctx)
+ (if (async-eval test env ctx)
+ (async-aser body env ctx)
+ (async-aser-cond-clojure (slice clauses 2) env ctx)))))))
+
+
+;; --------------------------------------------------------------------------
+;; async-aser-case-loop — case dispatch for aser mode
+;; --------------------------------------------------------------------------
+
+(define-async async-aser-case-loop
+ (fn (match-val clauses env ctx)
+ (if (< (len clauses) 2)
+ nil
+ (let ((test (first clauses))
+ (body (nth clauses 1)))
+ (if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else"))
+ (and (= (type-of test) "symbol")
+ (or (= (symbol-name test) ":else")
+ (= (symbol-name test) "else"))))
+ (async-aser body env ctx)
+ (if (= match-val (async-eval test env ctx))
+ (async-aser body env ctx)
+ (async-aser-case-loop match-val (slice clauses 2) env ctx)))))))
+
+
+;; --------------------------------------------------------------------------
+;; async-aser-thread-first — -> form in aser mode
+;; --------------------------------------------------------------------------
+
+(define-async async-aser-thread-first
+ (fn (args env ctx)
+ (let ((result (async-eval (first args) env ctx)))
+ (for-each
+ (fn (form)
+ (if (= (type-of form) "list")
+ (let ((f (async-eval (first form) env ctx))
+ (fn-args (cons result
+ (async-eval-args (rest form) env ctx))))
+ (set! result (async-invoke-or-lambda f fn-args env ctx)))
+ (let ((f (async-eval form env ctx)))
+ (set! result (async-invoke-or-lambda f (list result) env ctx)))))
+ (rest args))
+ result)))
+
+
+;; --------------------------------------------------------------------------
+;; async-invoke-or-lambda — invoke a callable or lambda with args
+;; --------------------------------------------------------------------------
+
+(define-async async-invoke-or-lambda
+ (fn (f args env ctx)
+ (cond
+ (and (callable? f) (not (lambda? f)) (not (component? f)))
+ (let ((r (apply f args)))
+ (if (async-coroutine? r)
+ (async-await! r)
+ r))
+ (lambda? f)
+ (let ((local (env-merge (lambda-closure f) env)))
+ (for-each-indexed
+ (fn (i p) (env-set! local p (nth args i)))
+ (lambda-params f))
+ (async-eval (lambda-body f) local ctx))
+ :else
+ (error (str "-> form not callable: " (inspect f))))))
+
+
+;; --------------------------------------------------------------------------
+;; Async aser HO forms (map, map-indexed, for-each)
+;; --------------------------------------------------------------------------
+
+(define-async async-aser-ho-map
+ (fn (args env ctx)
+ (let ((f (async-eval (first args) env ctx))
+ (coll (async-eval (nth args 1) env ctx))
+ (results (list)))
+ (for-each
+ (fn (item)
+ (if (lambda? f)
+ (let ((local (env-merge (lambda-closure f) env)))
+ (env-set! local (first (lambda-params f)) item)
+ (append! results (async-aser (lambda-body f) local ctx)))
+ (append! results (async-invoke f item))))
+ coll)
+ results)))
+
+
+(define-async async-aser-ho-map-indexed
+ (fn (args env ctx)
+ (let ((f (async-eval (first args) env ctx))
+ (coll (async-eval (nth args 1) env ctx))
+ (results (list))
+ (i 0))
+ (for-each
+ (fn (item)
+ (if (lambda? f)
+ (let ((local (env-merge (lambda-closure f) env)))
+ (env-set! local (first (lambda-params f)) i)
+ (env-set! local (nth (lambda-params f) 1) item)
+ (append! results (async-aser (lambda-body f) local ctx)))
+ (append! results (async-invoke f i item)))
+ (set! i (inc i)))
+ coll)
+ results)))
+
+
+(define-async async-aser-ho-for-each
+ (fn (args env ctx)
+ (let ((f (async-eval (first args) env ctx))
+ (coll (async-eval (nth args 1) env ctx))
+ (results (list)))
+ (for-each
+ (fn (item)
+ (if (lambda? f)
+ (let ((local (env-merge (lambda-closure f) env)))
+ (env-set! local (first (lambda-params f)) item)
+ (append! results (async-aser (lambda-body f) local ctx)))
+ (append! results (async-invoke f item))))
+ coll)
+ results)))
+
+
+;; --------------------------------------------------------------------------
+;; Platform interface — async adapter
+;; --------------------------------------------------------------------------
+;;
+;; Async evaluation (provided by platform):
+;; (async-eval expr env ctx) — evaluate with I/O interception
+;; (execute-io name args kw ctx) — execute I/O primitive
+;; (io-primitive? name) — check if name is I/O primitive
+;;
+;; From eval.sx:
+;; cond-scheme?, eval-cond-scheme, eval-cond-clojure
+;; eval-expr, trampoline, expand-macro, sf-lambda
+;; env-has?, env-get, env-set!, env-merge
+;; lambda?, component?, island?, macro?, callable?
+;; lambda-closure, lambda-params, lambda-body
+;; component-params, component-body, component-closure,
+;; component-has-children?, component-name
+;; inspect
+;;
+;; From render.sx:
+;; HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS
+;; render-attrs, definition-form?, cond-scheme?
+;; escape-html, escape-attr, raw-html-content
+;;
+;; From adapter-html.sx:
+;; serialize-island-state
+;;
+;; Context management (platform):
+;; (expand-components?) — check if component expansion is enabled
+;; (svg-context?) — check if in SVG context
+;; (svg-context-set! val) — set SVG context (returns reset token)
+;; (svg-context-reset! token) — reset SVG context
+;; (css-class-collect! val) — collect CSS classes
+;;
+;; Raw HTML:
+;; (is-raw-html? x) — check if raw HTML marker
+;; (make-raw-html s) — wrap string as raw HTML
+;; (raw-html-content x) — unwrap raw HTML
+;;
+;; SxExpr:
+;; (make-sx-expr s) — wrap as SxExpr (wire format string)
+;; (sx-expr? x) — check if SxExpr
+;;
+;; Async primitives:
+;; (async-coroutine? x) — check if value is a coroutine
+;; (async-await! x) — await a coroutine
+;; --------------------------------------------------------------------------
diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx
index e659b99..3e84f4f 100644
--- a/shared/sx/ref/adapter-dom.sx
+++ b/shared/sx/ref/adapter-dom.sx
@@ -186,10 +186,15 @@
(attr-expr (nth args (inc (get state "i")))))
(cond
;; Event handler: evaluate eagerly, bind listener
+ ;; If handler is a 0-arity lambda, wrap to ignore the event arg
(starts-with? attr-name "on-")
(let ((attr-val (trampoline (eval-expr attr-expr env))))
(when (callable? attr-val)
- (dom-listen el (slice attr-name 3) attr-val)))
+ (dom-listen el (slice attr-name 3)
+ (if (and (lambda? attr-val)
+ (= (len (lambda-params attr-val)) 0))
+ (fn (e) (call-lambda attr-val (list) (lambda-closure attr-val)))
+ attr-val))))
;; Two-way input binding: :bind signal
(= attr-name "bind")
(let ((attr-val (trampoline (eval-expr attr-expr env))))
diff --git a/shared/sx/ref/async_eval_ref.py b/shared/sx/ref/async_eval_ref.py
index af9c903..eebe2b8 100644
--- a/shared/sx/ref/async_eval_ref.py
+++ b/shared/sx/ref/async_eval_ref.py
@@ -26,7 +26,7 @@ import contextvars
import inspect
from typing import Any
-from ..types import Component, Keyword, Lambda, Macro, NIL, Symbol
+from ..types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
from ..parser import SxExpr, serialize
from ..primitives_io import IO_PRIMITIVES, RequestContext, execute_io
from ..html import (
@@ -210,9 +210,11 @@ async def _arender_list(expr, env, ctx):
if name in HTML_TAGS:
return await _arender_element(name, expr[1:], env, ctx)
- # Component
+ # Component / Island
if name.startswith("~"):
val = env.get(name)
+ if isinstance(val, Island):
+ return sx_ref.render_html_island(val, expr[1:], env)
if isinstance(val, Component):
return await _arender_component(val, expr[1:], env, ctx)
@@ -455,6 +457,7 @@ _ASYNC_RENDER_FORMS = {
"defcomp": _arsf_define,
"defmacro": _arsf_define,
"defhandler": _arsf_define,
+ "defisland": _arsf_define,
"map": _arsf_map,
"map-indexed": _arsf_map_indexed,
"filter": _arsf_filter,
@@ -505,6 +508,8 @@ async def _eval_slot_inner(expr, env, ctx):
if isinstance(result, str):
return SxExpr(result)
return SxExpr(serialize(result))
+ elif isinstance(comp, Island):
+ pass # Islands serialize as SX for client hydration
result = await _aser(expr, env, ctx)
result = await _maybe_expand_component_result(result, env, ctx)
if isinstance(result, SxExpr):
@@ -530,6 +535,9 @@ async def _maybe_expand_component_result(result, env, ctx):
return result
+_aser_stack: list[str] = [] # diagnostic: track expression context
+
+
async def _aser(expr, env, ctx):
"""Evaluate for SX wire format — serialize rendering forms, evaluate control flow."""
if isinstance(expr, (int, float, bool)):
@@ -553,7 +561,8 @@ async def _aser(expr, env, ctx):
return False
if name == "nil":
return NIL
- raise EvalError(f"Undefined symbol: {name}")
+ ctx_info = " → ".join(_aser_stack[-5:]) if _aser_stack else "(top)"
+ raise EvalError(f"Undefined symbol: {name} [aser context: {ctx_info}]")
if isinstance(expr, Keyword):
return expr.name
@@ -590,7 +599,7 @@ async def _aser(expr, env, ctx):
if name.startswith("html:"):
return await _aser_call(name[5:], expr[1:], env, ctx)
- # Component call
+ # Component / Island call
if name.startswith("~"):
val = env.get(name)
if isinstance(val, Macro):
@@ -598,7 +607,10 @@ async def _aser(expr, env, ctx):
sx_ref.expand_macro(val, expr[1:], env)
)
return await _aser(expanded, env, ctx)
- if isinstance(val, Component) and _expand_components.get():
+ if isinstance(val, Component) and (
+ _expand_components.get()
+ or getattr(val, "render_target", None) == "server"
+ ):
return await _aser_component(val, expr[1:], env, ctx)
return await _aser_call(name, expr[1:], env, ctx)
@@ -633,11 +645,11 @@ async def _aser(expr, env, ctx):
if _svg_context.get(False):
return await _aser_call(name, expr[1:], env, ctx)
- # Function/lambda call
+ # Function/lambda call — fallback: evaluate head as callable
fn = await async_eval(head, env, ctx)
args = [await async_eval(a, env, ctx) for a in expr[1:]]
- if callable(fn) and not isinstance(fn, (Lambda, Component)):
+ if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
result = fn(*args)
if inspect.iscoroutine(result):
return await result
@@ -650,7 +662,9 @@ async def _aser(expr, env, ctx):
return await _aser(fn.body, local, ctx)
if isinstance(fn, Component):
return await _aser_call(f"~{fn.name}", expr[1:], env, ctx)
- raise EvalError(f"Not callable: {fn!r}")
+ if isinstance(fn, Island):
+ return await _aser_call(f"~{fn.name}", expr[1:], env, ctx)
+ raise EvalError(f"Not callable in aser: {fn!r} (expr head: {head!r})")
async def _aser_fragment(children, env, ctx):
@@ -669,28 +683,41 @@ async def _aser_fragment(children, env, ctx):
async def _aser_component(comp, args, env, ctx):
- kwargs = {}
- children = []
- i = 0
- while i < len(args):
- arg = args[i]
- if isinstance(arg, Keyword) and i + 1 < len(args):
- kwargs[arg.name] = await _aser(args[i + 1], env, ctx)
- i += 2
- else:
- children.append(arg)
- i += 1
- local = dict(comp.closure)
- local.update(env)
- for p in comp.params:
- local[p] = kwargs.get(p, NIL)
- if comp.has_children:
- child_parts = [serialize(await _aser(c, env, ctx)) for c in children]
- local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")")
- return await _aser(comp.body, local, ctx)
+ _aser_stack.append(f"~{comp.name}")
+ try:
+ kwargs = {}
+ children = []
+ i = 0
+ while i < len(args):
+ arg = args[i]
+ if isinstance(arg, Keyword) and i + 1 < len(args):
+ kwargs[arg.name] = await _aser(args[i + 1], env, ctx)
+ i += 2
+ else:
+ children.append(arg)
+ i += 1
+ local = dict(comp.closure)
+ local.update(env)
+ for p in comp.params:
+ local[p] = kwargs.get(p, NIL)
+ if comp.has_children:
+ child_parts = []
+ for c in children:
+ result = await _aser(c, env, ctx)
+ if isinstance(result, list):
+ for item in result:
+ if item is not NIL and item is not None:
+ child_parts.append(serialize(item))
+ elif result is not NIL and result is not None:
+ child_parts.append(serialize(result))
+ local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")")
+ return await _aser(comp.body, local, ctx)
+ finally:
+ _aser_stack.pop()
async def _aser_call(name, args, env, ctx):
+ _aser_stack.append(name)
token = None
if name in ("svg", "math"):
token = _svg_context.set(True)
@@ -730,6 +757,7 @@ async def _aser_call(name, args, env, ctx):
_merge_class_into_parts(parts, extra_class)
return SxExpr("(" + " ".join(parts) + ")")
finally:
+ _aser_stack.pop()
if token is not None:
_svg_context.reset(token)
@@ -885,7 +913,7 @@ async def _assf_thread_first(expr, env, ctx):
else:
fn = await async_eval(form, env, ctx)
fn_args = [result]
- if callable(fn) and not isinstance(fn, (Lambda, Component)):
+ if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
result = fn(*fn_args)
if inspect.iscoroutine(result):
result = await result
@@ -981,6 +1009,7 @@ _ASER_FORMS = {
"defcomp": _assf_define,
"defmacro": _assf_define,
"defhandler": _assf_define,
+ "defisland": _assf_define,
"begin": _assf_begin,
"do": _assf_begin,
"quote": _assf_quote,
diff --git a/shared/sx/ref/bootstrap_py.py b/shared/sx/ref/bootstrap_py.py
index 20c2383..875b381 100644
--- a/shared/sx/ref/bootstrap_py.py
+++ b/shared/sx/ref/bootstrap_py.py
@@ -1196,6 +1196,8 @@ def compile_ref_to_py(
spec_mod_set.add("deps")
if "signals" in SPEC_MODULES:
spec_mod_set.add("signals")
+ if "page-helpers" in SPEC_MODULES:
+ spec_mod_set.add("page-helpers")
has_deps = "deps" in spec_mod_set
# Core files always included, then selected adapters, then spec modules
diff --git a/shared/sx/ref/page-helpers.sx b/shared/sx/ref/page-helpers.sx
new file mode 100644
index 0000000..69e2ba1
--- /dev/null
+++ b/shared/sx/ref/page-helpers.sx
@@ -0,0 +1,368 @@
+;; ==========================================================================
+;; page-helpers.sx — Pure data-transformation page helpers
+;;
+;; These functions take raw data (from Python I/O edge) and return
+;; structured dicts for page rendering. No I/O — pure transformations
+;; only. Bootstrapped to every host.
+;; ==========================================================================
+
+
+;; --------------------------------------------------------------------------
+;; categorize-special-forms
+;;
+;; Parses define-special-form declarations from special-forms.sx AST,
+;; categorizes each form by name lookup, returns dict of category → forms.
+;; --------------------------------------------------------------------------
+
+(define special-form-category-map
+ {"if" "Control Flow" "when" "Control Flow" "cond" "Control Flow"
+ "case" "Control Flow" "and" "Control Flow" "or" "Control Flow"
+ "let" "Binding" "let*" "Binding" "letrec" "Binding"
+ "define" "Binding" "set!" "Binding"
+ "lambda" "Functions & Components" "fn" "Functions & Components"
+ "defcomp" "Functions & Components" "defmacro" "Functions & Components"
+ "begin" "Sequencing & Threading" "do" "Sequencing & Threading"
+ "->" "Sequencing & Threading"
+ "quote" "Quoting" "quasiquote" "Quoting"
+ "reset" "Continuations" "shift" "Continuations"
+ "dynamic-wind" "Guards"
+ "map" "Higher-Order Forms" "map-indexed" "Higher-Order Forms"
+ "filter" "Higher-Order Forms" "reduce" "Higher-Order Forms"
+ "some" "Higher-Order Forms" "every?" "Higher-Order Forms"
+ "for-each" "Higher-Order Forms"
+ "defstyle" "Domain Definitions"
+ "defhandler" "Domain Definitions" "defpage" "Domain Definitions"
+ "defquery" "Domain Definitions" "defaction" "Domain Definitions"})
+
+
+(define extract-define-kwargs
+ (fn (expr)
+ ;; Extract keyword args from a define-special-form expression.
+ ;; Returns dict of keyword-name → string value.
+ ;; Walks items pairwise: when item[i] is a keyword, item[i+1] is its value.
+ (let ((result {})
+ (items (slice expr 2))
+ (n (len items)))
+ (for-each
+ (fn (idx)
+ (when (and (< (+ idx 1) n)
+ (= (type-of (nth items idx)) "keyword"))
+ (let ((key (keyword-name (nth items idx)))
+ (val (nth items (+ idx 1))))
+ (dict-set! result key
+ (if (= (type-of val) "list")
+ (str "(" (join " " (map serialize val)) ")")
+ (str val))))))
+ (range 0 n))
+ result)))
+
+
+(define categorize-special-forms
+ (fn (parsed-exprs)
+ ;; parsed-exprs: result of parse-all on special-forms.sx
+ ;; Returns dict of category-name → list of form dicts.
+ (let ((categories {}))
+ (for-each
+ (fn (expr)
+ (when (and (= (type-of expr) "list")
+ (>= (len expr) 2)
+ (= (type-of (first expr)) "symbol")
+ (= (symbol-name (first expr)) "define-special-form"))
+ (let ((name (nth expr 1))
+ (kwargs (extract-define-kwargs expr))
+ (category (or (get special-form-category-map name) "Other")))
+ (when (not (has-key? categories category))
+ (dict-set! categories category (list)))
+ (append! (get categories category)
+ {"name" name
+ "syntax" (or (get kwargs "syntax") "")
+ "doc" (or (get kwargs "doc") "")
+ "tail-position" (or (get kwargs "tail-position") "")
+ "example" (or (get kwargs "example") "")}))))
+ parsed-exprs)
+ categories)))
+
+
+;; --------------------------------------------------------------------------
+;; build-reference-data
+;;
+;; Takes a slug and raw reference data, returns structured dict for rendering.
+;; --------------------------------------------------------------------------
+
+(define build-ref-items-with-href
+ (fn (items base-path detail-keys n-fields)
+ ;; items: list of lists (tuples), each with n-fields elements
+ ;; base-path: e.g. "/hypermedia/reference/attributes/"
+ ;; detail-keys: list of strings (keys that have detail pages)
+ ;; n-fields: 2 or 3 (number of fields per tuple)
+ (map
+ (fn (item)
+ (if (= n-fields 3)
+ ;; [name, desc/value, exists/desc]
+ (let ((name (nth item 0))
+ (field2 (nth item 1))
+ (field3 (nth item 2)))
+ {"name" name
+ "desc" field2
+ "exists" field3
+ "href" (if (and field3 (some (fn (k) (= k name)) detail-keys))
+ (str base-path name)
+ nil)})
+ ;; [name, desc]
+ (let ((name (nth item 0))
+ (desc (nth item 1)))
+ {"name" name
+ "desc" desc
+ "href" (if (some (fn (k) (= k name)) detail-keys)
+ (str base-path name)
+ nil)})))
+ items)))
+
+
+(define build-reference-data
+ (fn (slug raw-data detail-keys)
+ ;; slug: "attributes", "headers", "events", "js-api"
+ ;; raw-data: dict with the raw data lists for this slug
+ ;; detail-keys: list of names that have detail pages
+ (case slug
+ "attributes"
+ {"req-attrs" (build-ref-items-with-href
+ (get raw-data "req-attrs")
+ "/hypermedia/reference/attributes/" detail-keys 3)
+ "beh-attrs" (build-ref-items-with-href
+ (get raw-data "beh-attrs")
+ "/hypermedia/reference/attributes/" detail-keys 3)
+ "uniq-attrs" (build-ref-items-with-href
+ (get raw-data "uniq-attrs")
+ "/hypermedia/reference/attributes/" detail-keys 3)}
+
+ "headers"
+ {"req-headers" (build-ref-items-with-href
+ (get raw-data "req-headers")
+ "/hypermedia/reference/headers/" detail-keys 3)
+ "resp-headers" (build-ref-items-with-href
+ (get raw-data "resp-headers")
+ "/hypermedia/reference/headers/" detail-keys 3)}
+
+ "events"
+ {"events-list" (build-ref-items-with-href
+ (get raw-data "events-list")
+ "/hypermedia/reference/events/" detail-keys 2)}
+
+ "js-api"
+ {"js-api-list" (map (fn (item) {"name" (nth item 0) "desc" (nth item 1)})
+ (get raw-data "js-api-list"))}
+
+ ;; default: attributes
+ :else
+ {"req-attrs" (build-ref-items-with-href
+ (get raw-data "req-attrs")
+ "/hypermedia/reference/attributes/" detail-keys 3)
+ "beh-attrs" (build-ref-items-with-href
+ (get raw-data "beh-attrs")
+ "/hypermedia/reference/attributes/" detail-keys 3)
+ "uniq-attrs" (build-ref-items-with-href
+ (get raw-data "uniq-attrs")
+ "/hypermedia/reference/attributes/" detail-keys 3)})))
+
+
+;; --------------------------------------------------------------------------
+;; build-attr-detail / build-header-detail / build-event-detail
+;;
+;; Lookup a slug in a detail dict, reshape for page rendering.
+;; --------------------------------------------------------------------------
+
+(define build-attr-detail
+ (fn (slug detail)
+ ;; detail: dict with "description", "example", "handler", "demo" keys or nil
+ (if (nil? detail)
+ {"attr-not-found" true}
+ {"attr-not-found" nil
+ "attr-title" slug
+ "attr-description" (get detail "description")
+ "attr-example" (get detail "example")
+ "attr-handler" (get detail "handler")
+ "attr-demo" (get detail "demo")
+ "attr-wire-id" (if (has-key? detail "handler")
+ (str "ref-wire-"
+ (replace (replace slug ":" "-") "*" "star"))
+ nil)})))
+
+
+(define build-header-detail
+ (fn (slug detail)
+ (if (nil? detail)
+ {"header-not-found" true}
+ {"header-not-found" nil
+ "header-title" slug
+ "header-direction" (get detail "direction")
+ "header-description" (get detail "description")
+ "header-example" (get detail "example")
+ "header-demo" (get detail "demo")})))
+
+
+(define build-event-detail
+ (fn (slug detail)
+ (if (nil? detail)
+ {"event-not-found" true}
+ {"event-not-found" nil
+ "event-title" slug
+ "event-description" (get detail "description")
+ "event-example" (get detail "example")
+ "event-demo" (get detail "demo")})))
+
+
+;; --------------------------------------------------------------------------
+;; build-component-source
+;;
+;; Reconstruct defcomp/defisland source from component metadata.
+;; --------------------------------------------------------------------------
+
+(define build-component-source
+ (fn (comp-data)
+ ;; comp-data: dict with "type", "name", "params", "has-children", "body-sx", "affinity"
+ (let ((comp-type (get comp-data "type"))
+ (name (get comp-data "name"))
+ (params (get comp-data "params"))
+ (has-children (get comp-data "has-children"))
+ (body-sx (get comp-data "body-sx"))
+ (affinity (get comp-data "affinity")))
+ (if (= comp-type "not-found")
+ (str ";; component " name " not found")
+ (let ((param-strs (if (empty? params)
+ (if has-children
+ (list "&rest" "children")
+ (list))
+ (if has-children
+ (append (cons "&key" params) (list "&rest" "children"))
+ (cons "&key" params))))
+ (params-sx (str "(" (join " " param-strs) ")"))
+ (form-name (if (= comp-type "island") "defisland" "defcomp"))
+ (affinity-str (if (and (= comp-type "component")
+ (not (nil? affinity))
+ (not (= affinity "auto")))
+ (str " :affinity " affinity)
+ "")))
+ (str "(" form-name " " name " " params-sx affinity-str "\n " body-sx ")"))))))
+
+
+;; --------------------------------------------------------------------------
+;; build-bundle-analysis
+;;
+;; Compute per-page bundle stats from pre-extracted component data.
+;; --------------------------------------------------------------------------
+
+(define build-bundle-analysis
+ (fn (pages-raw components-raw total-components total-macros pure-count io-count)
+ ;; pages-raw: list of {:name :path :direct :needed-names}
+ ;; components-raw: dict of name → {:is-pure :affinity :render-target :io-refs :deps :source}
+ (let ((pages-data (list)))
+ (for-each
+ (fn (page)
+ (let ((needed-names (get page "needed-names"))
+ (n (len needed-names))
+ (pct (if (> total-components 0)
+ (round (* (/ n total-components) 100))
+ 0))
+ (savings (- 100 pct))
+ (pure-in-page 0)
+ (io-in-page 0)
+ (page-io-refs (list))
+ (comp-details (list)))
+ ;; Walk needed components
+ (for-each
+ (fn (comp-name)
+ (let ((info (get components-raw comp-name)))
+ (when (not (nil? info))
+ (if (get info "is-pure")
+ (set! pure-in-page (+ pure-in-page 1))
+ (do
+ (set! io-in-page (+ io-in-page 1))
+ (for-each
+ (fn (ref) (when (not (some (fn (r) (= r ref)) page-io-refs))
+ (append! page-io-refs ref)))
+ (or (get info "io-refs") (list)))))
+ (append! comp-details
+ {"name" comp-name
+ "is-pure" (get info "is-pure")
+ "affinity" (get info "affinity")
+ "render-target" (get info "render-target")
+ "io-refs" (or (get info "io-refs") (list))
+ "deps" (or (get info "deps") (list))
+ "source" (get info "source")}))))
+ needed-names)
+ (append! pages-data
+ {"name" (get page "name")
+ "path" (get page "path")
+ "direct" (get page "direct")
+ "needed" n
+ "pct" pct
+ "savings" savings
+ "io-refs" (len page-io-refs)
+ "pure-in-page" pure-in-page
+ "io-in-page" io-in-page
+ "components" comp-details})))
+ pages-raw)
+ {"pages" pages-data
+ "total-components" total-components
+ "total-macros" total-macros
+ "pure-count" pure-count
+ "io-count" io-count})))
+
+
+;; --------------------------------------------------------------------------
+;; build-routing-analysis
+;;
+;; Classify pages by routing mode (client vs server).
+;; --------------------------------------------------------------------------
+
+(define build-routing-analysis
+ (fn (pages-raw)
+ ;; pages-raw: list of {:name :path :has-data :content-src}
+ (let ((pages-data (list))
+ (client-count 0)
+ (server-count 0))
+ (for-each
+ (fn (page)
+ (let ((has-data (get page "has-data"))
+ (content-src (or (get page "content-src") ""))
+ (mode nil)
+ (reason ""))
+ (cond
+ has-data
+ (do (set! mode "server")
+ (set! reason "Has :data expression — needs server IO")
+ (set! server-count (+ server-count 1)))
+ (empty? content-src)
+ (do (set! mode "server")
+ (set! reason "No content expression")
+ (set! server-count (+ server-count 1)))
+ :else
+ (do (set! mode "client")
+ (set! client-count (+ client-count 1))))
+ (append! pages-data
+ {"name" (get page "name")
+ "path" (get page "path")
+ "mode" mode
+ "has-data" has-data
+ "content-expr" (if (> (len content-src) 80)
+ (str (slice content-src 0 80) "...")
+ content-src)
+ "reason" reason})))
+ pages-raw)
+ {"pages" pages-data
+ "total-pages" (+ client-count server-count)
+ "client-count" client-count
+ "server-count" server-count})))
+
+
+;; --------------------------------------------------------------------------
+;; build-affinity-analysis
+;;
+;; Package component affinity info + page render plans for display.
+;; --------------------------------------------------------------------------
+
+(define build-affinity-analysis
+ (fn (demo-components page-plans)
+ {"components" demo-components
+ "page-plans" page-plans}))
diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py
new file mode 100644
index 0000000..9dd2881
--- /dev/null
+++ b/shared/sx/ref/platform_js.py
@@ -0,0 +1,3163 @@
+"""
+JS platform constants and functions for the SX bootstrap compiler.
+
+This module contains all platform-specific JS code (string constants, helper
+functions, and configuration dicts) shared by bootstrap_js.py and run_js_sx.py.
+The JSEmitter class, compile_ref_to_js function, and main entry point remain
+in bootstrap_js.py.
+"""
+from __future__ import annotations
+
+from shared.sx.parser import parse_all
+from shared.sx.types import Symbol
+
+
+def extract_defines(source: str) -> list[tuple[str, list]]:
+ """Parse .sx source, return list of (name, define-expr) for top-level defines."""
+ exprs = parse_all(source)
+ defines = []
+ for expr in exprs:
+ if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
+ if expr[0].name == "define":
+ name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
+ defines.append((name, expr))
+ return defines
+
+ADAPTER_FILES = {
+ "parser": ("parser.sx", "parser"),
+ "html": ("adapter-html.sx", "adapter-html"),
+ "sx": ("adapter-sx.sx", "adapter-sx"),
+ "dom": ("adapter-dom.sx", "adapter-dom"),
+ "engine": ("engine.sx", "engine"),
+ "orchestration": ("orchestration.sx","orchestration"),
+ "boot": ("boot.sx", "boot"),
+}
+
+# Dependencies
+ADAPTER_DEPS = {
+ "engine": ["dom"],
+ "orchestration": ["engine", "dom"],
+ "boot": ["dom", "engine", "orchestration", "parser"],
+ "parser": [],
+}
+
+SPEC_MODULES = {
+ "deps": ("deps.sx", "deps (component dependency analysis)"),
+ "router": ("router.sx", "router (client-side route matching)"),
+ "signals": ("signals.sx", "signals (reactive signal runtime)"),
+ "page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
+}
+
+
+EXTENSION_NAMES = {"continuations"}
+CONTINUATIONS_JS = '''
+ // =========================================================================
+ // 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_JS = '''
+ // =========================================================================
+ // Async IO: Promise-aware rendering for client-side IO primitives
+ // =========================================================================
+ //
+ // IO primitives (query, current-user, etc.) return Promises on the client.
+ // asyncRenderToDom walks the component tree; when it encounters an IO
+ // primitive, it awaits the Promise and continues rendering.
+ //
+ // The sync evaluator/renderer is untouched. This is a separate async path
+ // used only when a page's component tree contains IO references.
+
+ var IO_PRIMITIVES = {};
+
+ function registerIoPrimitive(name, fn) {
+ IO_PRIMITIVES[name] = fn;
+ }
+
+ function isPromise(x) {
+ return x != null && typeof x === "object" && typeof x.then === "function";
+ }
+
+ // Async trampoline: resolves thunks, awaits Promises
+ function asyncTrampoline(val) {
+ if (isPromise(val)) return val.then(asyncTrampoline);
+ if (isThunk(val)) return asyncTrampoline(evalExpr(thunkExpr(val), thunkEnv(val)));
+ return val;
+ }
+
+ // Async eval: like trampoline(evalExpr(...)) but handles IO primitives
+ function asyncEval(expr, env) {
+ // Intercept IO primitive calls at the AST level
+ if (Array.isArray(expr) && expr.length > 0) {
+ var head = expr[0];
+ if (head && head._sym) {
+ var name = head.name;
+ if (IO_PRIMITIVES[name]) {
+ // Evaluate args, then call the IO primitive
+ return asyncEvalIoCall(name, expr.slice(1), env);
+ }
+ }
+ }
+ // Non-IO: use sync eval, but result might be a thunk
+ var result = evalExpr(expr, env);
+ return asyncTrampoline(result);
+ }
+
+ function asyncEvalIoCall(name, rawArgs, env) {
+ // Parse keyword args and positional args, evaluating each (may be async)
+ var kwargs = {};
+ var args = [];
+ var promises = [];
+ var i = 0;
+ while (i < rawArgs.length) {
+ var arg = rawArgs[i];
+ if (arg && arg._kw && (i + 1) < rawArgs.length) {
+ var kName = arg.name;
+ var kVal = asyncEval(rawArgs[i + 1], env);
+ if (isPromise(kVal)) {
+ (function(k) { promises.push(kVal.then(function(v) { kwargs[k] = v; })); })(kName);
+ } else {
+ kwargs[kName] = kVal;
+ }
+ i += 2;
+ } else {
+ var aVal = asyncEval(arg, env);
+ if (isPromise(aVal)) {
+ (function(idx) { promises.push(aVal.then(function(v) { args[idx] = v; })); })(args.length);
+ args.push(null); // placeholder
+ } else {
+ args.push(aVal);
+ }
+ i++;
+ }
+ }
+ var ioFn = IO_PRIMITIVES[name];
+ if (promises.length > 0) {
+ return Promise.all(promises).then(function() { return ioFn(args, kwargs); });
+ }
+ return ioFn(args, kwargs);
+ }
+
+ // Async render-to-dom: returns Promise or Node
+ function asyncRenderToDom(expr, env, ns) {
+ // Literals
+ if (expr === NIL || expr === null || expr === undefined) return null;
+ if (expr === true || expr === false) return null;
+ if (typeof expr === "string") return document.createTextNode(expr);
+ if (typeof expr === "number") return document.createTextNode(String(expr));
+
+ // Symbol -> async eval then render
+ if (expr && expr._sym) {
+ var val = asyncEval(expr, env);
+ if (isPromise(val)) return val.then(function(v) { return asyncRenderToDom(v, env, ns); });
+ return asyncRenderToDom(val, env, ns);
+ }
+
+ // Keyword
+ if (expr && expr._kw) return document.createTextNode(expr.name);
+
+ // DocumentFragment / DOM nodes pass through
+ if (expr instanceof DocumentFragment || (expr && expr.nodeType)) return expr;
+
+ // Dict -> skip
+ if (expr && typeof expr === "object" && !Array.isArray(expr)) return null;
+
+ // List
+ if (!Array.isArray(expr) || expr.length === 0) return null;
+
+ var head = expr[0];
+ if (!head) return null;
+
+ // Symbol head
+ if (head._sym) {
+ var hname = head.name;
+
+ // IO primitive
+ if (IO_PRIMITIVES[hname]) {
+ var ioResult = asyncEval(expr, env);
+ if (isPromise(ioResult)) return ioResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
+ return asyncRenderToDom(ioResult, env, ns);
+ }
+
+ // Fragment
+ if (hname === "<>") return asyncRenderChildren(expr.slice(1), env, ns);
+
+ // raw!
+ if (hname === "raw!") {
+ return asyncEvalRaw(expr.slice(1), env);
+ }
+
+ // Special forms that need async handling
+ if (hname === "if") return asyncRenderIf(expr, env, ns);
+ if (hname === "when") return asyncRenderWhen(expr, env, ns);
+ if (hname === "cond") return asyncRenderCond(expr, env, ns);
+ if (hname === "case") return asyncRenderCase(expr, env, ns);
+ if (hname === "let" || hname === "let*") return asyncRenderLet(expr, env, ns);
+ if (hname === "begin" || hname === "do") return asyncRenderChildren(expr.slice(1), env, ns);
+ if (hname === "map") return asyncRenderMap(expr, env, ns);
+ if (hname === "map-indexed") return asyncRenderMapIndexed(expr, env, ns);
+ if (hname === "for-each") return asyncRenderMap(expr, env, ns);
+
+ // define/defcomp/defmacro — eval for side effects
+ if (hname === "define" || hname === "defcomp" || hname === "defmacro" ||
+ hname === "defstyle" || hname === "defhandler") {
+ trampoline(evalExpr(expr, env));
+ return null;
+ }
+
+ // quote
+ if (hname === "quote") return null;
+
+ // lambda/fn
+ if (hname === "lambda" || hname === "fn") {
+ trampoline(evalExpr(expr, env));
+ return null;
+ }
+
+ // and/or — eval and render result
+ if (hname === "and" || hname === "or" || hname === "->") {
+ var aoResult = asyncEval(expr, env);
+ if (isPromise(aoResult)) return aoResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
+ return asyncRenderToDom(aoResult, env, ns);
+ }
+
+ // set!
+ if (hname === "set!") {
+ asyncEval(expr, env);
+ return null;
+ }
+
+ // Component or Island
+ if (hname.charAt(0) === "~") {
+ var comp = env[hname];
+ if (comp && comp._island) return renderDomIsland(comp, expr.slice(1), env, ns);
+ if (comp && comp._component) return asyncRenderComponent(comp, expr.slice(1), env, ns);
+ if (comp && comp._macro) {
+ var expanded = trampoline(expandMacro(comp, expr.slice(1), env));
+ return asyncRenderToDom(expanded, env, ns);
+ }
+ }
+
+ // Macro
+ if (env[hname] && env[hname]._macro) {
+ var mac = env[hname];
+ var expanded = trampoline(expandMacro(mac, expr.slice(1), env));
+ return asyncRenderToDom(expanded, env, ns);
+ }
+
+ // HTML tag
+ if (typeof renderDomElement === "function" && contains(HTML_TAGS, hname)) {
+ return asyncRenderElement(hname, expr.slice(1), env, ns);
+ }
+
+ // html: prefix
+ if (hname.indexOf("html:") === 0) {
+ return asyncRenderElement(hname.slice(5), expr.slice(1), env, ns);
+ }
+
+ // Custom element
+ if (hname.indexOf("-") >= 0 && expr.length > 1 && expr[1] && expr[1]._kw) {
+ return asyncRenderElement(hname, expr.slice(1), env, ns);
+ }
+
+ // SVG context
+ if (ns) return asyncRenderElement(hname, expr.slice(1), env, ns);
+
+ // Fallback: eval and render
+ var fResult = asyncEval(expr, env);
+ if (isPromise(fResult)) return fResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
+ return asyncRenderToDom(fResult, env, ns);
+ }
+
+ // Non-symbol head: eval call
+ var cResult = asyncEval(expr, env);
+ if (isPromise(cResult)) return cResult.then(function(v) { return asyncRenderToDom(v, env, ns); });
+ return asyncRenderToDom(cResult, env, ns);
+ }
+
+ function asyncRenderChildren(exprs, env, ns) {
+ var frag = document.createDocumentFragment();
+ var pending = [];
+ for (var i = 0; i < exprs.length; i++) {
+ var result = asyncRenderToDom(exprs[i], env, ns);
+ if (isPromise(result)) {
+ // Insert placeholder, replace when resolved
+ var placeholder = document.createComment("async");
+ frag.appendChild(placeholder);
+ (function(ph) {
+ pending.push(result.then(function(node) {
+ if (node) ph.parentNode.replaceChild(node, ph);
+ else ph.parentNode.removeChild(ph);
+ }));
+ })(placeholder);
+ } else if (result) {
+ frag.appendChild(result);
+ }
+ }
+ if (pending.length > 0) {
+ return Promise.all(pending).then(function() { return frag; });
+ }
+ return frag;
+ }
+
+ function asyncRenderElement(tag, args, env, ns) {
+ var newNs = tag === "svg" ? SVG_NS : tag === "math" ? MATH_NS : ns;
+ var el = domCreateElement(tag, newNs);
+ var pending = [];
+ var isVoid = contains(VOID_ELEMENTS, tag);
+ for (var i = 0; i < args.length; i++) {
+ var arg = args[i];
+ if (arg && arg._kw && (i + 1) < args.length) {
+ var attrName = arg.name;
+ var attrVal = asyncEval(args[i + 1], env);
+ i++;
+ if (isPromise(attrVal)) {
+ (function(an, av) {
+ pending.push(av.then(function(v) {
+ if (!isNil(v) && v !== false) {
+ if (contains(BOOLEAN_ATTRS, an)) { if (isSxTruthy(v)) el.setAttribute(an, ""); }
+ else if (v === true) el.setAttribute(an, "");
+ else el.setAttribute(an, String(v));
+ }
+ }));
+ })(attrName, attrVal);
+ } else {
+ if (!isNil(attrVal) && attrVal !== false) {
+ if (contains(BOOLEAN_ATTRS, attrName)) {
+ if (isSxTruthy(attrVal)) el.setAttribute(attrName, "");
+ } else if (attrVal === true) {
+ el.setAttribute(attrName, "");
+ } else {
+ el.setAttribute(attrName, String(attrVal));
+ }
+ }
+ }
+ } else if (!isVoid) {
+ var child = asyncRenderToDom(arg, env, newNs);
+ if (isPromise(child)) {
+ var placeholder = document.createComment("async");
+ el.appendChild(placeholder);
+ (function(ph) {
+ pending.push(child.then(function(node) {
+ if (node) ph.parentNode.replaceChild(node, ph);
+ else ph.parentNode.removeChild(ph);
+ }));
+ })(placeholder);
+ } else if (child) {
+ el.appendChild(child);
+ }
+ }
+ }
+ if (pending.length > 0) return Promise.all(pending).then(function() { return el; });
+ return el;
+ }
+
+ function asyncRenderComponent(comp, args, env, ns) {
+ var kwargs = {};
+ var children = [];
+ var pending = [];
+ for (var i = 0; i < args.length; i++) {
+ var arg = args[i];
+ if (arg && arg._kw && (i + 1) < args.length) {
+ var kName = arg.name;
+ var kVal = asyncEval(args[i + 1], env);
+ if (isPromise(kVal)) {
+ (function(k) { pending.push(kVal.then(function(v) { kwargs[k] = v; })); })(kName);
+ } else {
+ kwargs[kName] = kVal;
+ }
+ i++;
+ } else {
+ children.push(arg);
+ }
+ }
+
+ function doRender() {
+ var local = Object.create(componentClosure(comp));
+ for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k];
+ var params = componentParams(comp);
+ for (var j = 0; j < params.length; j++) {
+ local[params[j]] = params[j] in kwargs ? kwargs[params[j]] : NIL;
+ }
+ if (componentHasChildren(comp)) {
+ var childResult = asyncRenderChildren(children, env, ns);
+ if (isPromise(childResult)) {
+ return childResult.then(function(childFrag) {
+ local["children"] = childFrag;
+ return asyncRenderToDom(componentBody(comp), local, ns);
+ });
+ }
+ local["children"] = childResult;
+ }
+ return asyncRenderToDom(componentBody(comp), local, ns);
+ }
+
+ if (pending.length > 0) return Promise.all(pending).then(doRender);
+ return doRender();
+ }
+
+ function asyncRenderIf(expr, env, ns) {
+ var cond = asyncEval(expr[1], env);
+ if (isPromise(cond)) {
+ return cond.then(function(v) {
+ return isSxTruthy(v)
+ ? asyncRenderToDom(expr[2], env, ns)
+ : (expr.length > 3 ? asyncRenderToDom(expr[3], env, ns) : null);
+ });
+ }
+ return isSxTruthy(cond)
+ ? asyncRenderToDom(expr[2], env, ns)
+ : (expr.length > 3 ? asyncRenderToDom(expr[3], env, ns) : null);
+ }
+
+ function asyncRenderWhen(expr, env, ns) {
+ var cond = asyncEval(expr[1], env);
+ if (isPromise(cond)) {
+ return cond.then(function(v) {
+ return isSxTruthy(v) ? asyncRenderChildren(expr.slice(2), env, ns) : null;
+ });
+ }
+ return isSxTruthy(cond) ? asyncRenderChildren(expr.slice(2), env, ns) : null;
+ }
+
+ function asyncRenderCond(expr, env, ns) {
+ var clauses = expr.slice(1);
+ function step(idx) {
+ if (idx >= clauses.length) return null;
+ var clause = clauses[idx];
+ if (!Array.isArray(clause) || clause.length < 2) return step(idx + 1);
+ var test = clause[0];
+ if ((test && test._sym && (test.name === "else" || test.name === ":else")) ||
+ (test && test._kw && test.name === "else")) {
+ return asyncRenderToDom(clause[1], env, ns);
+ }
+ var v = asyncEval(test, env);
+ if (isPromise(v)) return v.then(function(r) { return isSxTruthy(r) ? asyncRenderToDom(clause[1], env, ns) : step(idx + 1); });
+ return isSxTruthy(v) ? asyncRenderToDom(clause[1], env, ns) : step(idx + 1);
+ }
+ return step(0);
+ }
+
+ function asyncRenderCase(expr, env, ns) {
+ var matchVal = asyncEval(expr[1], env);
+ function doCase(mv) {
+ var clauses = expr.slice(2);
+ for (var i = 0; i < clauses.length - 1; i += 2) {
+ var test = clauses[i];
+ if ((test && test._kw && test.name === "else") ||
+ (test && test._sym && (test.name === "else" || test.name === ":else"))) {
+ return asyncRenderToDom(clauses[i + 1], env, ns);
+ }
+ var tv = trampoline(evalExpr(test, env));
+ if (mv === tv || (typeof mv === "string" && typeof tv === "string" && mv === tv)) {
+ return asyncRenderToDom(clauses[i + 1], env, ns);
+ }
+ }
+ return null;
+ }
+ if (isPromise(matchVal)) return matchVal.then(doCase);
+ return doCase(matchVal);
+ }
+
+ function asyncRenderLet(expr, env, ns) {
+ var bindings = expr[1];
+ var local = Object.create(env);
+ for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k];
+ function bindStep(idx) {
+ if (!Array.isArray(bindings)) return asyncRenderChildren(expr.slice(2), local, ns);
+ // Nested pairs: ((a 1) (b 2))
+ if (bindings.length > 0 && Array.isArray(bindings[0])) {
+ if (idx >= bindings.length) return asyncRenderChildren(expr.slice(2), local, ns);
+ var b = bindings[idx];
+ var vname = b[0]._sym ? b[0].name : String(b[0]);
+ var val = asyncEval(b[1], local);
+ if (isPromise(val)) return val.then(function(v) { local[vname] = v; return bindStep(idx + 1); });
+ local[vname] = val;
+ return bindStep(idx + 1);
+ }
+ // Flat pairs: (a 1 b 2)
+ if (idx >= bindings.length) return asyncRenderChildren(expr.slice(2), local, ns);
+ var vn = bindings[idx]._sym ? bindings[idx].name : String(bindings[idx]);
+ var vv = asyncEval(bindings[idx + 1], local);
+ if (isPromise(vv)) return vv.then(function(v) { local[vn] = v; return bindStep(idx + 2); });
+ local[vn] = vv;
+ return bindStep(idx + 2);
+ }
+ return bindStep(0);
+ }
+
+ function asyncRenderMap(expr, env, ns) {
+ var fn = asyncEval(expr[1], env);
+ var coll = asyncEval(expr[2], env);
+ function doMap(f, c) {
+ if (!Array.isArray(c)) return null;
+ var frag = document.createDocumentFragment();
+ var pending = [];
+ for (var i = 0; i < c.length; i++) {
+ var item = c[i];
+ var result;
+ if (f && f._lambda) {
+ var lenv = Object.create(f.closure || env);
+ for (var k in env) if (env.hasOwnProperty(k)) lenv[k] = env[k];
+ lenv[f.params[0]] = item;
+ result = asyncRenderToDom(f.body, lenv, null);
+ } else if (typeof f === "function") {
+ var r = f(item);
+ result = isPromise(r) ? r.then(function(v) { return asyncRenderToDom(v, env, null); }) : asyncRenderToDom(r, env, null);
+ } else {
+ result = asyncRenderToDom(item, env, null);
+ }
+ if (isPromise(result)) {
+ var ph = document.createComment("async");
+ frag.appendChild(ph);
+ (function(p) { pending.push(result.then(function(n) { if (n) p.parentNode.replaceChild(n, p); else p.parentNode.removeChild(p); })); })(ph);
+ } else if (result) {
+ frag.appendChild(result);
+ }
+ }
+ if (pending.length) return Promise.all(pending).then(function() { return frag; });
+ return frag;
+ }
+ if (isPromise(fn) || isPromise(coll)) {
+ return Promise.all([isPromise(fn) ? fn : Promise.resolve(fn), isPromise(coll) ? coll : Promise.resolve(coll)])
+ .then(function(r) { return doMap(r[0], r[1]); });
+ }
+ return doMap(fn, coll);
+ }
+
+ function asyncRenderMapIndexed(expr, env, ns) {
+ var fn = asyncEval(expr[1], env);
+ var coll = asyncEval(expr[2], env);
+ function doMap(f, c) {
+ if (!Array.isArray(c)) return null;
+ var frag = document.createDocumentFragment();
+ var pending = [];
+ for (var i = 0; i < c.length; i++) {
+ var item = c[i];
+ var result;
+ if (f && f._lambda) {
+ var lenv = Object.create(f.closure || env);
+ for (var k in env) if (env.hasOwnProperty(k)) lenv[k] = env[k];
+ lenv[f.params[0]] = i;
+ lenv[f.params[1]] = item;
+ result = asyncRenderToDom(f.body, lenv, null);
+ } else if (typeof f === "function") {
+ var r = f(i, item);
+ result = isPromise(r) ? r.then(function(v) { return asyncRenderToDom(v, env, null); }) : asyncRenderToDom(r, env, null);
+ } else {
+ result = asyncRenderToDom(item, env, null);
+ }
+ if (isPromise(result)) {
+ var ph = document.createComment("async");
+ frag.appendChild(ph);
+ (function(p) { pending.push(result.then(function(n) { if (n) p.parentNode.replaceChild(n, p); else p.parentNode.removeChild(p); })); })(ph);
+ } else if (result) {
+ frag.appendChild(result);
+ }
+ }
+ if (pending.length) return Promise.all(pending).then(function() { return frag; });
+ return frag;
+ }
+ if (isPromise(fn) || isPromise(coll)) {
+ return Promise.all([isPromise(fn) ? fn : Promise.resolve(fn), isPromise(coll) ? coll : Promise.resolve(coll)])
+ .then(function(r) { return doMap(r[0], r[1]); });
+ }
+ return doMap(fn, coll);
+ }
+
+ function asyncEvalRaw(args, env) {
+ var parts = [];
+ var pending = [];
+ for (var i = 0; i < args.length; i++) {
+ var val = asyncEval(args[i], env);
+ if (isPromise(val)) {
+ (function(idx) {
+ pending.push(val.then(function(v) { parts[idx] = v; }));
+ })(parts.length);
+ parts.push(null);
+ } else {
+ parts.push(val);
+ }
+ }
+ function assemble() {
+ var html = "";
+ for (var j = 0; j < parts.length; j++) {
+ var p = parts[j];
+ if (p && p._rawHtml) html += p.html;
+ else if (typeof p === "string") html += p;
+ else if (p != null && !isNil(p)) html += String(p);
+ }
+ var el = document.createElement("span");
+ el.innerHTML = html;
+ var frag = document.createDocumentFragment();
+ while (el.firstChild) frag.appendChild(el.firstChild);
+ return frag;
+ }
+ if (pending.length) return Promise.all(pending).then(assemble);
+ return assemble();
+ }
+
+ // Async version of sxRenderWithEnv — returns Promise
+ function asyncSxRenderWithEnv(source, extraEnv) {
+ var env = extraEnv ? merge(componentEnv, extraEnv) : componentEnv;
+ var exprs = parse(source);
+ if (!_hasDom) return Promise.resolve(null);
+ return asyncRenderChildren(exprs, env, null);
+ }
+
+ // IO proxy cache: key → { value, expires }
+ var _ioCache = {};
+ var IO_CACHE_TTL = 300000; // 5 minutes
+
+ // Register a server-proxied IO primitive: fetches from /sx/io/
+ // Uses GET for short args, POST for long payloads (URL length safety).
+ // Results are cached client-side by (name + args) with a TTL.
+ function registerProxiedIo(name) {
+ registerIoPrimitive(name, function(args, kwargs) {
+ // Cache key: name + serialized args
+ var cacheKey = name;
+ for (var ci = 0; ci < args.length; ci++) cacheKey += "\0" + String(args[ci]);
+ for (var ck in kwargs) {
+ if (kwargs.hasOwnProperty(ck)) cacheKey += "\0" + ck + "=" + String(kwargs[ck]);
+ }
+ var cached = _ioCache[cacheKey];
+ if (cached && cached.expires > Date.now()) return cached.value;
+
+ var url = "/sx/io/" + encodeURIComponent(name);
+ var qs = [];
+ for (var i = 0; i < args.length; i++) {
+ qs.push("_arg" + i + "=" + encodeURIComponent(String(args[i])));
+ }
+ for (var k in kwargs) {
+ if (kwargs.hasOwnProperty(k)) {
+ qs.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(kwargs[k])));
+ }
+ }
+ var queryStr = qs.join("&");
+ var fetchOpts;
+ if (queryStr.length > 1500) {
+ // POST with JSON body for long payloads
+ var sArgs = [];
+ for (var j = 0; j < args.length; j++) sArgs.push(String(args[j]));
+ var sKwargs = {};
+ for (var kk in kwargs) {
+ if (kwargs.hasOwnProperty(kk)) sKwargs[kk] = String(kwargs[kk]);
+ }
+ var postHeaders = { "SX-Request": "true", "Content-Type": "application/json" };
+ var csrf = csrfToken();
+ if (csrf && csrf !== NIL) postHeaders["X-CSRFToken"] = csrf;
+ fetchOpts = {
+ method: "POST",
+ headers: postHeaders,
+ body: JSON.stringify({ args: sArgs, kwargs: sKwargs })
+ };
+ } else {
+ if (queryStr) url += "?" + queryStr;
+ fetchOpts = { headers: { "SX-Request": "true" } };
+ }
+ var result = fetch(url, fetchOpts)
+ .then(function(resp) {
+ if (!resp.ok) {
+ logWarn("sx:io " + name + " failed " + resp.status);
+ return NIL;
+ }
+ return resp.text();
+ })
+ .then(function(text) {
+ if (!text || text === "nil") return NIL;
+ try {
+ var exprs = parse(text);
+ var val = exprs.length === 1 ? exprs[0] : exprs;
+ _ioCache[cacheKey] = { value: val, expires: Date.now() + IO_CACHE_TTL };
+ return val;
+ } catch (e) {
+ logWarn("sx:io " + name + " parse error: " + (e && e.message ? e.message : e));
+ return NIL;
+ }
+ })
+ .catch(function(e) {
+ logWarn("sx:io " + name + " network error: " + (e && e.message ? e.message : e));
+ return NIL;
+ });
+ // Cache the in-flight promise too (dedup concurrent calls for same args)
+ _ioCache[cacheKey] = { value: result, expires: Date.now() + IO_CACHE_TTL };
+ return result;
+ });
+ }
+
+ // Register IO deps as proxied primitives (idempotent, called per-page)
+ function registerIoDeps(names) {
+ if (!names || !names.length) return;
+ var registered = 0;
+ for (var i = 0; i < names.length; i++) {
+ var name = names[i];
+ if (!IO_PRIMITIVES[name]) {
+ registerProxiedIo(name);
+ registered++;
+ }
+ }
+ if (registered > 0) {
+ logInfo("sx:io registered " + registered + " proxied primitives: " + names.join(", "));
+ }
+ }
+'''
+
+PREAMBLE = '''\
+/**
+ * sx-ref.js — Generated from reference SX evaluator specification.
+ *
+ * Bootstrap-compiled from shared/sx/ref/{eval,render,primitives}.sx
+ * Compare against hand-written sx.js for correctness verification.
+ *
+ * DO NOT EDIT — regenerate with: python bootstrap_js.py
+ */
+;(function(global) {
+ "use strict";
+
+ // =========================================================================
+ // Types
+ // =========================================================================
+
+ var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
+ var SX_VERSION = "BUILD_TIMESTAMP";
+
+ function isNil(x) { return x === NIL || x === null || x === undefined; }
+ function isSxTruthy(x) { return x !== false && !isNil(x); }
+
+ function Symbol(name) { this.name = name; }
+ Symbol.prototype.toString = function() { return this.name; };
+ Symbol.prototype._sym = true;
+
+ function Keyword(name) { this.name = name; }
+ Keyword.prototype.toString = function() { return ":" + this.name; };
+ Keyword.prototype._kw = true;
+
+ function Lambda(params, body, closure, name) {
+ this.params = params;
+ this.body = body;
+ this.closure = closure || {};
+ this.name = name || null;
+ }
+ Lambda.prototype._lambda = true;
+
+ function Component(name, params, hasChildren, body, closure, affinity) {
+ this.name = name;
+ this.params = params;
+ this.hasChildren = hasChildren;
+ this.body = body;
+ this.closure = closure || {};
+ this.affinity = affinity || "auto";
+ }
+ Component.prototype._component = true;
+
+ function Island(name, params, hasChildren, body, closure) {
+ this.name = name;
+ this.params = params;
+ this.hasChildren = hasChildren;
+ this.body = body;
+ this.closure = closure || {};
+ }
+ Island.prototype._island = true;
+
+ function SxSignal(value) {
+ this.value = value;
+ this.subscribers = [];
+ this.deps = [];
+ }
+ SxSignal.prototype._signal = true;
+
+ function TrackingCtx(notifyFn) {
+ this.notifyFn = notifyFn;
+ this.deps = [];
+ }
+
+ var _trackingContext = null;
+
+ function Macro(params, restParam, body, closure, name) {
+ this.params = params;
+ this.restParam = restParam;
+ this.body = body;
+ this.closure = closure || {};
+ this.name = name || null;
+ }
+ Macro.prototype._macro = true;
+
+ function Thunk(expr, env) { this.expr = expr; this.env = env; }
+ Thunk.prototype._thunk = true;
+
+ function RawHTML(html) { this.html = html; }
+ RawHTML.prototype._raw = true;
+
+ function isSym(x) { return x != null && x._sym === true; }
+ function isKw(x) { return x != null && x._kw === true; }
+
+ function merge() {
+ var out = {};
+ for (var i = 0; i < arguments.length; i++) {
+ var d = arguments[i];
+ if (d) for (var k in d) out[k] = d[k];
+ }
+ return out;
+ }
+
+ function sxOr() {
+ for (var i = 0; i < arguments.length; i++) {
+ if (isSxTruthy(arguments[i])) return arguments[i];
+ }
+ return arguments.length ? arguments[arguments.length - 1] : false;
+ }'''
+PRIMITIVES_JS_MODULES: dict[str, str] = {
+ "core.arithmetic": '''
+ // core.arithmetic
+ PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; };
+ PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; };
+ PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; };
+ PRIMITIVES["/"] = function(a, b) { return a / b; };
+ PRIMITIVES["mod"] = function(a, b) { return a % b; };
+ PRIMITIVES["inc"] = function(n) { return n + 1; };
+ PRIMITIVES["dec"] = function(n) { return n - 1; };
+ PRIMITIVES["abs"] = Math.abs;
+ PRIMITIVES["floor"] = Math.floor;
+ PRIMITIVES["ceil"] = Math.ceil;
+ PRIMITIVES["round"] = function(x, n) {
+ if (n === undefined || n === 0) return Math.round(x);
+ var f = Math.pow(10, n); return Math.round(x * f) / f;
+ };
+ PRIMITIVES["min"] = Math.min;
+ PRIMITIVES["max"] = Math.max;
+ PRIMITIVES["sqrt"] = Math.sqrt;
+ PRIMITIVES["pow"] = Math.pow;
+ PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); };
+''',
+
+ "core.comparison": '''
+ // core.comparison
+ PRIMITIVES["="] = function(a, b) { return a === b; };
+ PRIMITIVES["!="] = function(a, b) { return a !== b; };
+ PRIMITIVES["<"] = function(a, b) { return a < b; };
+ PRIMITIVES[">"] = function(a, b) { return a > b; };
+ PRIMITIVES["<="] = function(a, b) { return a <= b; };
+ PRIMITIVES[">="] = function(a, b) { return a >= b; };
+''',
+
+ "core.logic": '''
+ // core.logic
+ PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); };
+''',
+
+ "core.predicates": '''
+ // core.predicates
+ PRIMITIVES["nil?"] = isNil;
+ PRIMITIVES["number?"] = function(x) { return typeof x === "number"; };
+ PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
+ PRIMITIVES["list?"] = Array.isArray;
+ PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
+ PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
+ PRIMITIVES["contains?"] = function(c, k) {
+ if (typeof c === "string") return c.indexOf(String(k)) !== -1;
+ if (Array.isArray(c)) return c.indexOf(k) !== -1;
+ return k in c;
+ };
+ PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; };
+ PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
+ PRIMITIVES["zero?"] = function(n) { return n === 0; };
+ PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; };
+ PRIMITIVES["component-affinity"] = componentAffinity;
+''',
+
+ "core.strings": '''
+ // core.strings
+ PRIMITIVES["str"] = function() {
+ var p = [];
+ for (var i = 0; i < arguments.length; i++) {
+ var v = arguments[i]; if (isNil(v)) continue; p.push(String(v));
+ }
+ return p.join("");
+ };
+ PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); };
+ PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); };
+ PRIMITIVES["trim"] = function(s) { return String(s).trim(); };
+ PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
+ PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
+ PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
+ PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); };
+ PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
+ PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
+ PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); };
+ PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); };
+ PRIMITIVES["string-length"] = function(s) { return String(s).length; };
+ PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; };
+ PRIMITIVES["concat"] = function() {
+ var out = [];
+ for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]);
+ return out;
+ };
+''',
+
+ "core.collections": '''
+ // core.collections
+ PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); };
+ PRIMITIVES["dict"] = function() {
+ var d = {};
+ for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1];
+ return d;
+ };
+ PRIMITIVES["range"] = function(a, b, step) {
+ var r = []; step = step || 1;
+ for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i);
+ return r;
+ };
+ PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); };
+ PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; };
+ PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; };
+ PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; };
+ PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; };
+ PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
+ PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
+ PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); };
+ PRIMITIVES["append!"] = function(arr, x) { arr.push(x); return arr; };
+ PRIMITIVES["chunk-every"] = function(c, n) {
+ var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r;
+ };
+ PRIMITIVES["zip-pairs"] = function(c) {
+ var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r;
+ };
+ PRIMITIVES["reverse"] = function(c) { return Array.isArray(c) ? c.slice().reverse() : String(c).split("").reverse().join(""); };
+ PRIMITIVES["flatten"] = function(c) {
+ var out = [];
+ function walk(a) { for (var i = 0; i < a.length; i++) Array.isArray(a[i]) ? walk(a[i]) : out.push(a[i]); }
+ walk(c || []); return out;
+ };
+''',
+
+ "core.dict": '''
+ // core.dict
+ PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); };
+ PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; };
+ PRIMITIVES["merge"] = function() {
+ var out = {};
+ for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; }
+ return out;
+ };
+ PRIMITIVES["assoc"] = function(d) {
+ var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k];
+ for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1];
+ return out;
+ };
+ PRIMITIVES["dissoc"] = function(d) {
+ var out = {}; for (var k in d) out[k] = d[k];
+ for (var i = 1; i < arguments.length; i++) delete out[arguments[i]];
+ return out;
+ };
+ PRIMITIVES["dict-set!"] = function(d, k, v) { d[k] = v; return v; };
+ PRIMITIVES["has-key?"] = function(d, k) { return d !== null && d !== undefined && k in d; };
+ PRIMITIVES["into"] = function(target, coll) {
+ if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
+ var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
+ return r;
+ };
+''',
+
+ "stdlib.format": '''
+ // stdlib.format
+ PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };
+ PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; };
+ PRIMITIVES["format-date"] = function(s, fmt) {
+ if (!s) return "";
+ try {
+ var d = new Date(s);
+ if (isNaN(d.getTime())) return String(s);
+ var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
+ var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
+ return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2))
+ .replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()])
+ .replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2))
+ .replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2));
+ } catch (e) { return String(s); }
+ };
+ PRIMITIVES["parse-datetime"] = function(s) { return s ? String(s) : NIL; };
+''',
+
+ "stdlib.text": '''
+ // stdlib.text
+ PRIMITIVES["pluralize"] = function(n, s, p) {
+ if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s");
+ return n == 1 ? "" : "s";
+ };
+ PRIMITIVES["escape"] = function(s) {
+ return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'");
+ };
+ PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
+''',
+
+ "stdlib.debug": '''
+ // stdlib.debug
+ PRIMITIVES["assert"] = function(cond, msg) {
+ if (!isSxTruthy(cond)) throw new Error("Assertion error: " + (msg || "Assertion failed"));
+ return true;
+ };
+''',
+}
+# Modules to include by default (all)
+_ALL_JS_MODULES = list(PRIMITIVES_JS_MODULES.keys())
+
+def _assemble_primitives_js(modules: list[str] | None = None) -> str:
+ """Assemble JS primitive code from selected modules.
+
+ If modules is None, all modules are included.
+ Core modules are always included regardless of the list.
+ """
+ if modules is None:
+ modules = _ALL_JS_MODULES
+ parts = []
+ for mod in modules:
+ if mod in PRIMITIVES_JS_MODULES:
+ parts.append(PRIMITIVES_JS_MODULES[mod])
+ return "\n".join(parts)
+
+PLATFORM_JS_PRE = '''
+ // =========================================================================
+ // Platform interface — JS implementation
+ // =========================================================================
+
+ function typeOf(x) {
+ if (isNil(x)) return "nil";
+ if (typeof x === "number") return "number";
+ if (typeof x === "string") return "string";
+ if (typeof x === "boolean") return "boolean";
+ if (x._sym) return "symbol";
+ if (x._kw) return "keyword";
+ if (x._thunk) return "thunk";
+ if (x._lambda) return "lambda";
+ if (x._component) return "component";
+ if (x._island) return "island";
+ if (x._signal) return "signal";
+ if (x._macro) return "macro";
+ if (x._raw) return "raw-html";
+ if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
+ if (Array.isArray(x)) return "list";
+ if (typeof x === "object") return "dict";
+ return "unknown";
+ }
+
+ function symbolName(s) { return s.name; }
+ function keywordName(k) { return k.name; }
+ function makeSymbol(n) { return new Symbol(n); }
+ function makeKeyword(n) { return new Keyword(n); }
+
+ function makeLambda(params, body, env) { return new Lambda(params, body, merge(env)); }
+ function makeComponent(name, params, hasChildren, body, env, affinity) {
+ return new Component(name, params, hasChildren, body, merge(env), affinity);
+ }
+ function makeMacro(params, restParam, body, env, name) {
+ return new Macro(params, restParam, body, merge(env), name);
+ }
+ function makeThunk(expr, env) { return new Thunk(expr, env); }
+
+ function lambdaParams(f) { return f.params; }
+ function lambdaBody(f) { return f.body; }
+ function lambdaClosure(f) { return f.closure; }
+ function lambdaName(f) { return f.name; }
+ function setLambdaName(f, n) { f.name = n; }
+
+ function componentParams(c) { return c.params; }
+ function componentBody(c) { return c.body; }
+ function componentClosure(c) { return c.closure; }
+ function componentHasChildren(c) { return c.hasChildren; }
+ function componentName(c) { return c.name; }
+ function componentAffinity(c) { return c.affinity || "auto"; }
+
+ function macroParams(m) { return m.params; }
+ function macroRestParam(m) { return m.restParam; }
+ function macroBody(m) { return m.body; }
+ function macroClosure(m) { return m.closure; }
+
+ function isThunk(x) { return x != null && x._thunk === true; }
+ function thunkExpr(t) { return t.expr; }
+ function thunkEnv(t) { return t.env; }
+
+ function isCallable(x) { return typeof x === "function" || (x != null && x._lambda === true); }
+ function isLambda(x) { return x != null && x._lambda === true; }
+ function isComponent(x) { return x != null && x._component === true; }
+ function isIsland(x) { return x != null && x._island === true; }
+ function isMacro(x) { return x != null && x._macro === true; }
+ function isIdentical(a, b) { return a === b; }
+
+ // Island platform
+ function makeIsland(name, params, hasChildren, body, env) {
+ return new Island(name, params, hasChildren, body, merge(env));
+ }
+
+ // Signal platform
+ function makeSignal(value) { return new SxSignal(value); }
+ function isSignal(x) { return x != null && x._signal === true; }
+ function signalValue(s) { return s.value; }
+ function signalSetValue(s, v) { s.value = v; }
+ function signalSubscribers(s) { return s.subscribers.slice(); }
+ function signalAddSub(s, fn) { if (s.subscribers.indexOf(fn) < 0) s.subscribers.push(fn); }
+ function signalRemoveSub(s, fn) { var i = s.subscribers.indexOf(fn); if (i >= 0) s.subscribers.splice(i, 1); }
+ function signalDeps(s) { return s.deps.slice(); }
+ function signalSetDeps(s, deps) { s.deps = Array.isArray(deps) ? deps.slice() : []; }
+ function setTrackingContext(ctx) { _trackingContext = ctx; }
+ function getTrackingContext() { return _trackingContext || NIL; }
+ function makeTrackingContext(notifyFn) { return new TrackingCtx(notifyFn); }
+ function trackingContextDeps(ctx) { return ctx ? ctx.deps : []; }
+ function trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); }
+ function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; }
+
+ // invoke — call any callable (native fn or SX lambda) with args.
+ // Transpiled code emits direct calls f(args) which fail on SX lambdas
+ // from runtime-evaluated island bodies. invoke dispatches correctly.
+ function invoke() {
+ var f = arguments[0];
+ var args = Array.prototype.slice.call(arguments, 1);
+ if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f)));
+ if (typeof f === 'function') return f.apply(null, args);
+ return NIL;
+ }
+
+ // JSON / dict helpers for island state serialization
+ function jsonSerialize(obj) {
+ try { return JSON.stringify(obj); } catch(e) { return "{}"; }
+ }
+ function isEmptyDict(d) {
+ if (!d || typeof d !== "object") return true;
+ for (var k in d) if (d.hasOwnProperty(k)) return false;
+ return true;
+ }
+
+ function envHas(env, name) { return name in env; }
+ function envGet(env, name) { return env[name]; }
+ function envSet(env, name, val) { env[name] = val; }
+ function envExtend(env) { return Object.create(env); }
+ function envMerge(base, overlay) {
+ var child = Object.create(base);
+ if (overlay) for (var k in overlay) if (overlay.hasOwnProperty(k)) child[k] = overlay[k];
+ return child;
+ }
+
+ function dictSet(d, k, v) { d[k] = v; return v; }
+ function dictGet(d, k) { var v = d[k]; return v !== undefined ? v : NIL; }
+
+ // Render-expression detection — lets the evaluator delegate to the active adapter.
+ // Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements.
+ // Placeholder — overridden by transpiled version from render.sx
+ function isRenderExpr(expr) { return false; }
+
+ // Render dispatch — call the active adapter's render function.
+ // Set by each adapter when loaded; defaults to identity (no rendering).
+ var _renderExprFn = null;
+
+ // Render mode flag — set by render-to-html/aser, checked by eval-list.
+ // When false, render expressions fall through to evalCall.
+ var _renderMode = false;
+ function renderActiveP() { return _renderMode; }
+ function setRenderActiveB(val) { _renderMode = !!val; }
+
+ function renderExpr(expr, env) {
+ if (_renderExprFn) return _renderExprFn(expr, env);
+ // No adapter loaded — fall through to evalCall
+ return evalCall(first(expr), rest(expr), env);
+ }
+
+ function stripPrefix(s, prefix) {
+ return s.indexOf(prefix) === 0 ? s.slice(prefix.length) : s;
+ }
+
+ function error(msg) { throw new Error(msg); }
+ function inspect(x) { return JSON.stringify(x); }
+
+'''
+
+
+PLATFORM_JS_POST = '''
+ function isPrimitive(name) { return name in PRIMITIVES; }
+ function getPrimitive(name) { return PRIMITIVES[name]; }
+
+ // Higher-order helpers used by the transpiled code
+ function map(fn, coll) { return coll.map(fn); }
+ function mapIndexed(fn, coll) { return coll.map(function(item, i) { return fn(i, item); }); }
+ function filter(fn, coll) { return coll.filter(function(x) { return isSxTruthy(fn(x)); }); }
+ function reduce(fn, init, coll) {
+ var acc = init;
+ for (var i = 0; i < coll.length; i++) acc = fn(acc, coll[i]);
+ return acc;
+ }
+ function some(fn, coll) {
+ for (var i = 0; i < coll.length; i++) { var r = fn(coll[i]); if (isSxTruthy(r)) return r; }
+ return NIL;
+ }
+ function forEach(fn, coll) { for (var i = 0; i < coll.length; i++) fn(coll[i]); return NIL; }
+ function isEvery(fn, coll) {
+ for (var i = 0; i < coll.length; i++) { if (!isSxTruthy(fn(coll[i]))) return false; }
+ return true;
+ }
+ function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; }
+
+ // List primitives used directly by transpiled code
+ var len = PRIMITIVES["len"];
+ var first = PRIMITIVES["first"];
+ var last = PRIMITIVES["last"];
+ var rest = PRIMITIVES["rest"];
+ var nth = PRIMITIVES["nth"];
+ var cons = PRIMITIVES["cons"];
+ var append = PRIMITIVES["append"];
+ var isEmpty = PRIMITIVES["empty?"];
+ var contains = PRIMITIVES["contains?"];
+ var startsWith = PRIMITIVES["starts-with?"];
+ var slice = PRIMITIVES["slice"];
+ var concat = PRIMITIVES["concat"];
+ var str = PRIMITIVES["str"];
+ var join = PRIMITIVES["join"];
+ var keys = PRIMITIVES["keys"];
+ var get = PRIMITIVES["get"];
+ var assoc = PRIMITIVES["assoc"];
+ var range = PRIMITIVES["range"];
+ function zip(a, b) { var r = []; for (var i = 0; i < Math.min(a.length, b.length); i++) r.push([a[i], b[i]]); return r; }
+ function append_b(arr, x) { arr.push(x); return arr; }
+ var apply = function(f, args) {
+ if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f)));
+ return f.apply(null, args);
+ };
+
+ // Additional primitive aliases used by adapter/engine transpiled code
+ var split = PRIMITIVES["split"];
+ var trim = PRIMITIVES["trim"];
+ var upper = PRIMITIVES["upper"];
+ var lower = PRIMITIVES["lower"];
+ var replace_ = function(s, old, nw) { return s.split(old).join(nw); };
+ var endsWith = PRIMITIVES["ends-with?"];
+ var parseInt_ = PRIMITIVES["parse-int"];
+ var dict_fn = PRIMITIVES["dict"];
+
+ // HTML rendering helpers
+ function escapeHtml(s) {
+ return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,""");
+ }
+ function escapeAttr(s) { return escapeHtml(s); }
+ function rawHtmlContent(r) { return r.html; }
+ function makeRawHtml(s) { return { _raw: true, html: s }; }
+ function sxExprSource(x) { return x && x.source ? x.source : String(x); }
+
+ // Placeholders — overridden by transpiled spec from parser.sx / adapter-sx.sx
+ function serialize(val) { return String(val); }
+ function isSpecialForm(n) { return false; }
+ function isHoForm(n) { return false; }
+
+ // processBindings and evalCond — now specced in render.sx, bootstrapped above
+
+ function isDefinitionForm(name) {
+ return name === "define" || name === "defcomp" || name === "defmacro" ||
+ name === "defstyle" || name === "defhandler";
+ }
+
+ function indexOf_(s, ch) {
+ return typeof s === "string" ? s.indexOf(ch) : -1;
+ }
+
+ function dictHas(d, k) { return d != null && k in d; }
+ function dictDelete(d, k) { delete d[k]; }
+
+ function forEachIndexed(fn, coll) {
+ for (var i = 0; i < coll.length; i++) fn(i, coll[i]);
+ return NIL;
+ }
+
+ // =========================================================================
+ // Performance overrides — evaluator hot path
+ // =========================================================================
+
+ // Override parseKeywordArgs: imperative loop instead of reduce+assoc
+ parseKeywordArgs = function(rawArgs, env) {
+ var kwargs = {};
+ var children = [];
+ for (var i = 0; i < rawArgs.length; i++) {
+ var arg = rawArgs[i];
+ if (arg && arg._kw && (i + 1) < rawArgs.length) {
+ kwargs[arg.name] = trampoline(evalExpr(rawArgs[i + 1], env));
+ i++;
+ } else {
+ children.push(trampoline(evalExpr(arg, env)));
+ }
+ }
+ return [kwargs, children];
+ };
+
+ // Override callComponent: use prototype chain env, imperative kwarg binding
+ callComponent = function(comp, rawArgs, env) {
+ var kwargs = {};
+ var children = [];
+ for (var i = 0; i < rawArgs.length; i++) {
+ var arg = rawArgs[i];
+ if (arg && arg._kw && (i + 1) < rawArgs.length) {
+ kwargs[arg.name] = trampoline(evalExpr(rawArgs[i + 1], env));
+ i++;
+ } else {
+ children.push(trampoline(evalExpr(arg, env)));
+ }
+ }
+ var local = Object.create(componentClosure(comp));
+ for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k];
+ var params = componentParams(comp);
+ for (var j = 0; j < params.length; j++) {
+ var p = params[j];
+ local[p] = p in kwargs ? kwargs[p] : NIL;
+ }
+ if (componentHasChildren(comp)) {
+ local["children"] = children;
+ }
+ return makeThunk(componentBody(comp), local);
+ };'''
+
+
+PLATFORM_DEPS_JS = '''
+ // =========================================================================
+ // Platform: deps module — component dependency analysis
+ // =========================================================================
+
+ function componentDeps(c) {
+ return c.deps ? c.deps.slice() : [];
+ }
+
+ function componentSetDeps(c, deps) {
+ c.deps = deps;
+ }
+
+ function componentCssClasses(c) {
+ return c.cssClasses ? c.cssClasses.slice() : [];
+ }
+
+ function envComponents(env) {
+ var names = [];
+ for (var k in env) {
+ var v = env[k];
+ if (v && (v._component || v._macro)) names.push(k);
+ }
+ return names;
+ }
+
+ function regexFindAll(pattern, source) {
+ var re = new RegExp(pattern, "g");
+ var results = [];
+ var m;
+ while ((m = re.exec(source)) !== null) {
+ if (m[1] !== undefined) results.push(m[1]);
+ else results.push(m[0]);
+ }
+ return results;
+ }
+
+ function scanCssClasses(source) {
+ var classes = {};
+ var result = [];
+ var m;
+ var re1 = /:class\\s+"([^"]*)"/g;
+ while ((m = re1.exec(source)) !== null) {
+ var parts = m[1].split(/\\s+/);
+ for (var i = 0; i < parts.length; i++) {
+ if (parts[i] && !classes[parts[i]]) {
+ classes[parts[i]] = true;
+ result.push(parts[i]);
+ }
+ }
+ }
+ var re2 = /:class\\s+\\(str\\s+((?:"[^"]*"\\s*)+)\\)/g;
+ while ((m = re2.exec(source)) !== null) {
+ var re3 = /"([^"]*)"/g;
+ var m2;
+ while ((m2 = re3.exec(m[1])) !== null) {
+ var parts2 = m2[1].split(/\\s+/);
+ for (var j = 0; j < parts2.length; j++) {
+ if (parts2[j] && !classes[parts2[j]]) {
+ classes[parts2[j]] = true;
+ result.push(parts2[j]);
+ }
+ }
+ }
+ }
+ var re4 = /;;\\s*@css\\s+(.+)/g;
+ while ((m = re4.exec(source)) !== null) {
+ var parts3 = m[1].split(/\\s+/);
+ for (var k = 0; k < parts3.length; k++) {
+ if (parts3[k] && !classes[parts3[k]]) {
+ classes[parts3[k]] = true;
+ result.push(parts3[k]);
+ }
+ }
+ }
+ return result;
+ }
+
+ function componentIoRefs(c) {
+ return c.ioRefs ? c.ioRefs.slice() : [];
+ }
+
+ function componentSetIoRefs(c, refs) {
+ c.ioRefs = refs;
+ }
+'''
+
+
+PLATFORM_PARSER_JS = r"""
+ // =========================================================================
+ // Platform interface — Parser
+ // =========================================================================
+ // Character classification derived from the grammar:
+ // ident-start → [a-zA-Z_~*+\-><=/!?&]
+ // ident-char → ident-start + [0-9.:\/\[\]#,]
+
+ var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/;
+ var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]/;
+
+ function isIdentStart(ch) { return _identStartRe.test(ch); }
+ function isIdentChar(ch) { return _identCharRe.test(ch); }
+ function parseNumber(s) { return Number(s); }
+ function escapeString(s) {
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
+ }
+ function sxExprSource(e) { return typeof e === "string" ? e : String(e); }
+"""
+
+
+PLATFORM_DOM_JS = """
+ // =========================================================================
+ // Platform interface — DOM adapter (browser-only)
+ // =========================================================================
+
+ var _hasDom = typeof document !== "undefined";
+
+ // Register DOM adapter as the render dispatch target for the evaluator.
+ _renderExprFn = function(expr, env) { return renderToDom(expr, env, null); };
+ _renderMode = true; // Browser always evaluates in render context.
+
+ var SVG_NS = "http://www.w3.org/2000/svg";
+ var MATH_NS = "http://www.w3.org/1998/Math/MathML";
+
+ function domCreateElement(tag, ns) {
+ if (!_hasDom) return null;
+ if (ns && ns !== NIL) return document.createElementNS(ns, tag);
+ return document.createElement(tag);
+ }
+
+ function createTextNode(s) {
+ return _hasDom ? document.createTextNode(s) : null;
+ }
+
+ function createComment(s) {
+ return _hasDom ? document.createComment(s || "") : null;
+ }
+
+ function createFragment() {
+ return _hasDom ? document.createDocumentFragment() : null;
+ }
+
+ function domAppend(parent, child) {
+ if (parent && child) parent.appendChild(child);
+ }
+
+ function domPrepend(parent, child) {
+ if (parent && child) parent.insertBefore(child, parent.firstChild);
+ }
+
+ function domSetAttr(el, name, val) {
+ if (el && el.setAttribute) el.setAttribute(name, val);
+ }
+
+ function domGetAttr(el, name) {
+ if (!el || !el.getAttribute) return NIL;
+ var v = el.getAttribute(name);
+ return v === null ? NIL : v;
+ }
+
+ function domRemoveAttr(el, name) {
+ if (el && el.removeAttribute) el.removeAttribute(name);
+ }
+
+ function domHasAttr(el, name) {
+ return !!(el && el.hasAttribute && el.hasAttribute(name));
+ }
+
+ function domParseHtml(html) {
+ if (!_hasDom) return null;
+ var tpl = document.createElement("template");
+ tpl.innerHTML = html;
+ return tpl.content;
+ }
+
+ function domClone(node) {
+ return node && node.cloneNode ? node.cloneNode(true) : node;
+ }
+
+ function domParent(el) { return el ? el.parentNode : null; }
+ function domId(el) { return el && el.id ? el.id : NIL; }
+ function domNodeType(el) { return el ? el.nodeType : 0; }
+ function domNodeName(el) { return el ? el.nodeName : ""; }
+ function domTextContent(el) { return el ? el.textContent || el.nodeValue || "" : ""; }
+ function domSetTextContent(el, s) { if (el) { if (el.nodeType === 3 || el.nodeType === 8) el.nodeValue = s; else el.textContent = s; } }
+ function domIsFragment(el) { return el ? el.nodeType === 11 : false; }
+ function domIsChildOf(child, parent) { return !!(parent && child && child.parentNode === parent); }
+ function domIsActiveElement(el) { return _hasDom && el === document.activeElement; }
+ function domIsInputElement(el) {
+ if (!el || !el.tagName) return false;
+ var t = el.tagName;
+ return t === "INPUT" || t === "TEXTAREA" || t === "SELECT";
+ }
+ function domFirstChild(el) { return el ? el.firstChild : null; }
+ function domNextSibling(el) { return el ? el.nextSibling : null; }
+
+ function domChildList(el) {
+ if (!el || !el.childNodes) return [];
+ return Array.prototype.slice.call(el.childNodes);
+ }
+
+ function domAttrList(el) {
+ if (!el || !el.attributes) return [];
+ var r = [];
+ for (var i = 0; i < el.attributes.length; i++) {
+ r.push([el.attributes[i].name, el.attributes[i].value]);
+ }
+ return r;
+ }
+
+ function domInsertBefore(parent, node, ref) {
+ if (parent && node) parent.insertBefore(node, ref || null);
+ }
+
+ function domInsertAfter(ref, node) {
+ if (ref && ref.parentNode && node) {
+ ref.parentNode.insertBefore(node, ref.nextSibling);
+ }
+ }
+
+ function domRemoveChild(parent, child) {
+ if (parent && child && child.parentNode === parent) parent.removeChild(child);
+ }
+
+ function domReplaceChild(parent, newChild, oldChild) {
+ if (parent && newChild && oldChild) parent.replaceChild(newChild, oldChild);
+ }
+
+ function domSetInnerHtml(el, html) {
+ if (el) el.innerHTML = html;
+ }
+
+ function domInsertAdjacentHtml(el, pos, html) {
+ if (el && el.insertAdjacentHTML) el.insertAdjacentHTML(pos, html);
+ }
+
+ function domGetStyle(el, prop) {
+ return el && el.style ? el.style[prop] || "" : "";
+ }
+
+ function domSetStyle(el, prop, val) {
+ if (el && el.style) el.style[prop] = val;
+ }
+
+ function domGetProp(el, name) { return el ? el[name] : NIL; }
+ function domSetProp(el, name, val) { if (el) el[name] = val; }
+
+ function domAddClass(el, cls) {
+ if (el && el.classList) el.classList.add(cls);
+ }
+
+ function domRemoveClass(el, cls) {
+ if (el && el.classList) el.classList.remove(cls);
+ }
+
+ function domDispatch(el, name, detail) {
+ if (!_hasDom || !el) return false;
+ var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} });
+ return el.dispatchEvent(evt);
+ }
+
+ function domListen(el, name, handler) {
+ if (!_hasDom || !el) return function() {};
+ // Wrap SX lambdas from runtime-evaluated island code into native fns
+ var wrapped = isLambda(handler)
+ ? function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
+ : handler;
+ if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
+ el.addEventListener(name, wrapped);
+ return function() { el.removeEventListener(name, wrapped); };
+ }
+
+ function eventDetail(e) {
+ return (e && e.detail != null) ? e.detail : nil;
+ }
+
+ function domQuery(sel) {
+ return _hasDom ? document.querySelector(sel) : null;
+ }
+
+ function domEnsureElement(sel) {
+ if (!_hasDom) return null;
+ var el = document.querySelector(sel);
+ if (el) return el;
+ // Parse #id selector → create div with that id, append to body
+ if (sel.charAt(0) === '#') {
+ el = document.createElement('div');
+ el.id = sel.slice(1);
+ document.body.appendChild(el);
+ return el;
+ }
+ return null;
+ }
+
+ function domQueryAll(root, sel) {
+ if (!root || !root.querySelectorAll) return [];
+ return Array.prototype.slice.call(root.querySelectorAll(sel));
+ }
+
+ function domTagName(el) { return el && el.tagName ? el.tagName : ""; }
+
+ // Island DOM helpers
+ function domRemove(node) {
+ if (node && node.parentNode) node.parentNode.removeChild(node);
+ }
+ function domChildNodes(el) {
+ if (!el || !el.childNodes) return [];
+ return Array.prototype.slice.call(el.childNodes);
+ }
+ function domRemoveChildrenAfter(marker) {
+ if (!marker || !marker.parentNode) return;
+ var parent = marker.parentNode;
+ while (marker.nextSibling) parent.removeChild(marker.nextSibling);
+ }
+ function domSetData(el, key, val) {
+ if (el) { if (!el._sxData) el._sxData = {}; el._sxData[key] = val; }
+ }
+ function domGetData(el, key) {
+ return (el && el._sxData) ? (el._sxData[key] != null ? el._sxData[key] : nil) : nil;
+ }
+ function domInnerHtml(el) {
+ return (el && el.innerHTML != null) ? el.innerHTML : "";
+ }
+ function jsonParse(s) {
+ try { return JSON.parse(s); } catch(e) { return {}; }
+ }
+
+ // renderDomComponent and renderDomElement are transpiled from
+ // adapter-dom.sx — no imperative overrides needed.
+"""
+
+
+PLATFORM_ENGINE_PURE_JS = """
+ // =========================================================================
+ // Platform interface — Engine pure logic (browser + node compatible)
+ // =========================================================================
+
+ function browserLocationHref() {
+ return typeof location !== "undefined" ? location.href : "";
+ }
+
+ function browserSameOrigin(url) {
+ try { return new URL(url, location.href).origin === location.origin; }
+ catch (e) { return true; }
+ }
+
+ function browserPushState(url) {
+ if (typeof history !== "undefined") {
+ try { history.pushState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); }
+ catch (e) {}
+ }
+ }
+
+ function browserReplaceState(url) {
+ if (typeof history !== "undefined") {
+ try { history.replaceState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); }
+ catch (e) {}
+ }
+ }
+
+ function nowMs() { return (typeof performance !== "undefined") ? performance.now() : Date.now(); }
+
+ function parseHeaderValue(s) {
+ if (!s) return null;
+ try {
+ if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s);
+ return JSON.parse(s);
+ } catch (e) { return null; }
+ }
+"""
+
+
+PLATFORM_ORCHESTRATION_JS = """
+ // =========================================================================
+ // Platform interface — Orchestration (browser-only)
+ // =========================================================================
+
+ // --- Browser/Network ---
+
+ function browserNavigate(url) {
+ if (typeof location !== "undefined") location.assign(url);
+ }
+
+ function browserReload() {
+ if (typeof location !== "undefined") location.reload();
+ }
+
+ function browserScrollTo(x, y) {
+ if (typeof window !== "undefined") window.scrollTo(x, y);
+ }
+
+ function browserMediaMatches(query) {
+ if (typeof window === "undefined") return false;
+ return window.matchMedia(query).matches;
+ }
+
+ function browserConfirm(msg) {
+ if (typeof window === "undefined") return false;
+ return window.confirm(msg);
+ }
+
+ function browserPrompt(msg) {
+ if (typeof window === "undefined") return NIL;
+ var r = window.prompt(msg);
+ return r === null ? NIL : r;
+ }
+
+ function csrfToken() {
+ if (!_hasDom) return NIL;
+ var m = document.querySelector('meta[name="csrf-token"]');
+ return m ? m.getAttribute("content") : NIL;
+ }
+
+ function isCrossOrigin(url) {
+ try {
+ var h = new URL(url, location.href).hostname;
+ return h !== location.hostname &&
+ (h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0);
+ } catch (e) { return false; }
+ }
+
+ // --- Promises ---
+
+ function promiseResolve(val) { return Promise.resolve(val); }
+
+ function promiseThen(p, onResolve, onReject) {
+ if (!p || !p.then) return p;
+ return onReject ? p.then(onResolve, onReject) : p.then(onResolve);
+ }
+
+ function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
+
+ function promiseDelayed(ms, value) {
+ return new Promise(function(resolve) {
+ setTimeout(function() { resolve(value); }, ms);
+ });
+ }
+
+ // --- Abort controllers ---
+
+ var _controllers = typeof WeakMap !== "undefined" ? new WeakMap() : null;
+
+ function abortPrevious(el) {
+ if (_controllers) {
+ var prev = _controllers.get(el);
+ if (prev) prev.abort();
+ }
+ }
+
+ function trackController(el, ctrl) {
+ if (_controllers) _controllers.set(el, ctrl);
+ }
+
+ var _targetControllers = typeof WeakMap !== "undefined" ? new WeakMap() : null;
+
+ function abortPreviousTarget(el) {
+ if (_targetControllers) {
+ var prev = _targetControllers.get(el);
+ if (prev) prev.abort();
+ }
+ }
+
+ function trackControllerTarget(el, ctrl) {
+ if (_targetControllers) _targetControllers.set(el, ctrl);
+ }
+
+ function newAbortController() {
+ return typeof AbortController !== "undefined" ? new AbortController() : { signal: null, abort: function() {} };
+ }
+
+ function controllerSignal(ctrl) { return ctrl ? ctrl.signal : null; }
+
+ function isAbortError(err) { return err && err.name === "AbortError"; }
+
+ // --- Timers ---
+
+ function _wrapSxFn(fn) {
+ if (fn && fn._lambda) {
+ return function() { return trampoline(callLambda(fn, [], lambdaClosure(fn))); };
+ }
+ return fn;
+ }
+ function setTimeout_(fn, ms) { return setTimeout(_wrapSxFn(fn), ms || 0); }
+ function setInterval_(fn, ms) { return setInterval(_wrapSxFn(fn), ms || 1000); }
+ function clearTimeout_(id) { clearTimeout(id); }
+ function clearInterval_(id) { clearInterval(id); }
+ function requestAnimationFrame_(fn) {
+ var cb = _wrapSxFn(fn);
+ if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(cb);
+ else setTimeout(cb, 16);
+ }
+
+ // --- Fetch ---
+
+ function fetchRequest(config, successFn, errorFn) {
+ var opts = { method: config.method, headers: config.headers };
+ if (config.signal) opts.signal = config.signal;
+ if (config.body && config.method !== "GET") opts.body = config.body;
+ if (config["cross-origin"]) opts.credentials = "include";
+
+ var p = (config.preloaded && config.preloaded !== NIL)
+ ? Promise.resolve({
+ ok: true, status: 200,
+ headers: new Headers({ "Content-Type": config.preloaded["content-type"] || "" }),
+ text: function() { return Promise.resolve(config.preloaded.text); }
+ })
+ : fetch(config.url, opts);
+
+ return p.then(function(resp) {
+ return resp.text().then(function(text) {
+ var getHeader = function(name) {
+ var v = resp.headers.get(name);
+ return v === null ? NIL : v;
+ };
+ return successFn(resp.ok, resp.status, getHeader, text);
+ });
+ }).catch(function(err) {
+ return errorFn(err);
+ });
+ }
+
+ function fetchLocation(headerVal) {
+ if (!_hasDom) return;
+ var locUrl = headerVal;
+ try { var obj = JSON.parse(headerVal); locUrl = obj.path || obj; } catch (e) {}
+ fetch(locUrl, { headers: { "SX-Request": "true" } }).then(function(r) {
+ return r.text().then(function(t) {
+ var main = document.getElementById("main-panel");
+ if (main) {
+ main.innerHTML = t;
+ postSwap(main);
+ try { history.pushState({ sxUrl: locUrl }, "", locUrl); } catch (e) {}
+ }
+ });
+ });
+ }
+
+ function fetchAndRestore(main, url, headers, scrollY) {
+ var opts = { headers: headers };
+ try {
+ var h = new URL(url, location.href).hostname;
+ if (h !== location.hostname &&
+ (h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) {
+ opts.credentials = "include";
+ }
+ } catch (e) {}
+
+ fetch(url, opts).then(function(resp) {
+ return resp.text().then(function(text) {
+ text = stripComponentScripts(text);
+ text = extractResponseCss(text);
+ text = text.trim();
+ if (text.charAt(0) === "(") {
+ try {
+ var dom = sxRender(text);
+ var container = document.createElement("div");
+ container.appendChild(dom);
+ processOobSwaps(container, function(t, oob, s) {
+ swapDomNodes(t, oob, s);
+ sxHydrate(t);
+ processElements(t);
+ });
+ var newMain = container.querySelector("#main-panel");
+ morphChildren(main, newMain || container);
+ postSwap(main);
+ if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0);
+ } catch (err) {
+ console.error("sx-ref popstate error:", err);
+ location.reload();
+ }
+ } else {
+ var parser = new DOMParser();
+ var doc = parser.parseFromString(text, "text/html");
+ var newMain = doc.getElementById("main-panel");
+ if (newMain) {
+ morphChildren(main, newMain);
+ postSwap(main);
+ if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0);
+ } else {
+ location.reload();
+ }
+ }
+ });
+ }).catch(function() { location.reload(); });
+ }
+
+ function fetchStreaming(target, url, headers) {
+ // Streaming fetch for multi-stream pages.
+ // First chunk = OOB SX swap (shell with skeletons).
+ // Subsequent chunks = __sxResolve script tags filling suspense slots.
+ var opts = { headers: headers };
+ try {
+ var h = new URL(url, location.href).hostname;
+ if (h !== location.hostname &&
+ (h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) {
+ opts.credentials = "include";
+ }
+ } catch (e) {}
+
+ fetch(url, opts).then(function(resp) {
+ if (!resp.ok || !resp.body) {
+ // Fallback: non-streaming
+ return resp.text().then(function(text) {
+ text = stripComponentScripts(text);
+ text = extractResponseCss(text);
+ text = text.trim();
+ if (text.charAt(0) === "(") {
+ var dom = sxRender(text);
+ var container = document.createElement("div");
+ container.appendChild(dom);
+ processOobSwaps(container, function(t, oob, s) {
+ swapDomNodes(t, oob, s);
+ sxHydrate(t);
+ processElements(t);
+ });
+ var newMain = container.querySelector("#main-panel");
+ morphChildren(target, newMain || container);
+ postSwap(target);
+ }
+ });
+ }
+
+ var reader = resp.body.getReader();
+ var decoder = new TextDecoder();
+ var buffer = "";
+ var initialSwapDone = false;
+ // Regex to match __sxResolve script tags
+ var RESOLVE_START = "";
+
+ function processResolveScripts() {
+ // Strip and load any extra component defs before resolve scripts
+ buffer = stripSxScripts(buffer);
+ var idx;
+ while ((idx = buffer.indexOf(RESOLVE_START)) >= 0) {
+ var endIdx = buffer.indexOf(RESOLVE_END, idx);
+ if (endIdx < 0) break; // incomplete, wait for more data
+ var argsStr = buffer.substring(idx + RESOLVE_START.length, endIdx);
+ buffer = buffer.substring(endIdx + RESOLVE_END.length);
+ // argsStr is: "stream-id","sx source"
+ var commaIdx = argsStr.indexOf(",");
+ if (commaIdx >= 0) {
+ try {
+ var id = JSON.parse(argsStr.substring(0, commaIdx));
+ var sx = JSON.parse(argsStr.substring(commaIdx + 1));
+ if (typeof Sx !== "undefined" && Sx.resolveSuspense) {
+ Sx.resolveSuspense(id, sx);
+ }
+ } catch (e) {
+ console.error("[sx-ref] resolve parse error:", e);
+ }
+ }
+ }
+ }
+
+ function pump() {
+ return reader.read().then(function(result) {
+ buffer += decoder.decode(result.value || new Uint8Array(), { stream: !result.done });
+
+ if (!initialSwapDone) {
+ // Look for the first resolve script — everything before it is OOB content
+ var scriptIdx = buffer.indexOf(" (without data-components or data-init).
+ // These contain extra component defs from streaming resolve chunks.
+ // data-init scripts are preserved for process-sx-scripts to evaluate as side effects.
+ var SxObj = typeof Sx !== "undefined" ? Sx : null;
+ return text.replace(/