diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js
index 1a36e680..a5cf2edc 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-11T16:56:11Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -204,7 +204,7 @@
// JSON / dict helpers for island state serialization
function jsonSerialize(obj) {
- try { return JSON.stringify(obj); } catch(e) { return "{}"; }
+ return JSON.stringify(obj);
}
function isEmptyDict(d) {
if (!d || typeof d !== "object") return true;
@@ -214,11 +214,34 @@
function envHas(env, name) { return name in env; }
function envGet(env, name) { return env[name]; }
- function envSet(env, name, val) { env[name] = val; }
+ function envSet(env, name, val) {
+ // Walk prototype chain to find where the variable is defined (for set!)
+ var obj = env;
+ while (obj !== null && obj !== Object.prototype) {
+ if (obj.hasOwnProperty(name)) { obj[name] = val; return; }
+ obj = Object.getPrototypeOf(obj);
+ }
+ // Not found in any parent scope — set on the immediate env
+ env[name] = val;
+ }
function envExtend(env) { return Object.create(env); }
function envMerge(base, overlay) {
+ // Same env or overlay is descendant of base — just extend, no copy.
+ // This prevents set! inside lambdas from modifying shadow copies.
+ if (base === overlay) return Object.create(base);
+ var p = overlay;
+ for (var d = 0; p && p !== Object.prototype && d < 100; d++) {
+ if (p === base) return Object.create(base);
+ p = Object.getPrototypeOf(p);
+ }
+ // General case: extend base, copy ONLY overlay properties that don't
+ // exist in the base chain (avoids shadowing closure bindings).
var child = Object.create(base);
- if (overlay) for (var k in overlay) if (overlay.hasOwnProperty(k)) child[k] = overlay[k];
+ if (overlay) {
+ for (var k in overlay) {
+ if (overlay.hasOwnProperty(k) && !(k in base)) child[k] = overlay[k];
+ }
+ }
return child;
}
@@ -732,9 +755,9 @@
var kwargs = first(parsed);
var children = nth(parsed, 1);
var local = envMerge(componentClosure(comp), env);
- { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = sxOr(dictGet(kwargs, p), NIL); } }
+ { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, sxOr(dictGet(kwargs, p), NIL)); } }
if (isSxTruthy(componentHasChildren(comp))) {
- local["children"] = children;
+ envSet(local, "children", children);
}
return makeThunk(componentBody(comp), local);
})(); };
@@ -764,8 +787,11 @@
return (isSxTruthy((isSxTruthy(condition) && !isSxTruthy(isNil(condition)))) ? (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 1, (len(args) - 1))), makeThunk(last(args), env)) : NIL);
})(); };
+ // cond-scheme?
+ var condScheme_p = function(clauses) { return isEvery(function(c) { return (isSxTruthy((typeOf(c) == "list")) && (len(c) == 2)); }, clauses); };
+
// sf-cond
- var sfCond = function(args, env) { return (isSxTruthy((isSxTruthy((typeOf(first(args)) == "list")) && (len(first(args)) == 2))) ? sfCondScheme(args, env) : sfCondClojure(args, env)); };
+ var sfCond = function(args, env) { return (isSxTruthy(condScheme_p(args)) ? sfCondScheme(args, env) : sfCondClojure(args, env)); };
// sf-cond-scheme
var sfCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() {
@@ -841,7 +867,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var loopBody = (isSxTruthy((len(body) == 1)) ? first(body) : cons(makeSymbol("begin"), body));
var loopFn = makeLambda(params, loopBody, env);
loopFn.name = loopName;
- lambdaClosure(loopFn)[loopName] = loopFn;
+ envSet(lambdaClosure(loopFn), loopName, loopFn);
return (function() {
var initVals = map(function(e) { return trampoline(evalExpr(e, env)); }, inits);
return callLambda(loopFn, initVals, env);
@@ -865,7 +891,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
if (isSxTruthy((isSxTruthy(isLambda(value)) && isNil(lambdaName(value))))) {
value.name = symbolName(nameSym);
}
- env[symbolName(nameSym)] = value;
+ envSet(env, symbolName(nameSym), value);
return value;
})(); };
@@ -881,7 +907,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var affinity = defcompKwarg(args, "affinity", "auto");
return (function() {
var comp = makeComponent(compName, params, hasChildren, body, env, affinity);
- env[symbolName(nameSym)] = comp;
+ envSet(env, symbolName(nameSym), comp);
return comp;
})();
})(); };
@@ -924,7 +950,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var hasChildren = nth(parsed, 1);
return (function() {
var island = makeIsland(compName, params, hasChildren, body, env);
- env[symbolName(nameSym)] = island;
+ envSet(env, symbolName(nameSym), island);
return island;
})();
})(); };
@@ -939,7 +965,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var restParam = nth(parsed, 1);
return (function() {
var mac = makeMacro(params, restParam, body, env, symbolName(nameSym));
- env[symbolName(nameSym)] = mac;
+ envSet(env, symbolName(nameSym), mac);
return mac;
})();
})(); };
@@ -956,7 +982,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var sfDefstyle = function(args, env) { return (function() {
var nameSym = first(args);
var value = trampoline(evalExpr(nth(args, 1), env));
- env[symbolName(nameSym)] = value;
+ envSet(env, symbolName(nameSym), value);
return value;
})(); };
@@ -974,8 +1000,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
@@ -996,7 +1022,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var sfSetBang = function(args, env) { return (function() {
var name = symbolName(first(args));
var value = trampoline(evalExpr(nth(args, 1), env));
- env[name] = value;
+ envSet(env, name, value);
return value;
})(); };
@@ -1021,7 +1047,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
})(); }, NIL, range(0, (len(bindings) / 2))));
(function() {
var values = map(function(e) { return trampoline(evalExpr(e, local)); }, valExprs);
- { var _c = zip(names, values); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = nth(pair, 1); } }
+ { var _c = zip(names, values); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; envSet(local, first(pair), nth(pair, 1)); } }
return forEach(function(val) { return (isSxTruthy(isLambda(val)) ? forEach(function(n) { return envSet(lambdaClosure(val), n, envGet(local, n)); }, names) : NIL); }, values);
})();
{ var _c = slice(body, 0, (len(body) - 1)); for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; trampoline(evalExpr(e, local)); } }
@@ -1046,9 +1072,9 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
// expand-macro
var expandMacro = function(mac, rawArgs, env) { return (function() {
var local = envMerge(macroClosure(mac), env);
- { var _c = mapIndexed(function(i, p) { return [p, i]; }, macroParams(mac)); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = (isSxTruthy((nth(pair, 1) < len(rawArgs))) ? nth(rawArgs, nth(pair, 1)) : NIL); } }
+ { var _c = mapIndexed(function(i, p) { return [p, i]; }, macroParams(mac)); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; envSet(local, first(pair), (isSxTruthy((nth(pair, 1) < len(rawArgs))) ? nth(rawArgs, nth(pair, 1)) : NIL)); } }
if (isSxTruthy(macroRestParam(mac))) {
- local[macroRestParam(mac)] = slice(rawArgs, len(macroParams(mac)));
+ envSet(local, macroRestParam(mac), slice(rawArgs, len(macroParams(mac))));
}
return trampoline(evalExpr(macroBody(mac), local));
})(); };
@@ -1143,7 +1169,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
})(); }, keys(attrs))); };
// eval-cond
- var evalCond = function(clauses, env) { return (isSxTruthy((isSxTruthy(!isSxTruthy(isEmpty(clauses))) && isSxTruthy((typeOf(first(clauses)) == "list")) && (len(first(clauses)) == 2))) ? evalCondScheme(clauses, env) : evalCondClojure(clauses, env)); };
+ var evalCond = function(clauses, env) { return (isSxTruthy(condScheme_p(clauses)) ? evalCondScheme(clauses, env) : evalCondClojure(clauses, env)); };
// eval-cond-scheme
var evalCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() {
@@ -1162,7 +1188,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
// process-bindings
var processBindings = function(bindings, env) { return (function() {
- var local = merge(env);
+ var local = envExtend(env);
{ var _c = bindings; for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(pair) == "list")) && (len(pair) >= 2)))) {
(function() {
var name = (isSxTruthy((typeOf(first(pair)) == "symbol")) ? symbolName(first(pair)) : (String(first(pair))));
@@ -1392,9 +1418,9 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
})(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() {
var local = envMerge(componentClosure(comp), env);
- { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
+ { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(comp))) {
- local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)));
+ envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))));
}
return renderToHtml(componentBody(comp), local);
})();
@@ -1458,20 +1484,20 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
return (function() {
var local = envMerge(componentClosure(island), env);
var islandName = componentName(island);
- { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
+ { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(island))) {
- local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)));
+ envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))));
}
return (function() {
var bodyHtml = renderToHtml(componentBody(island), local);
- var stateJson = serializeIslandState(kwargs);
- return (String("") + String(bodyHtml) + String(""));
+ var stateSx = serializeIslandState(kwargs);
+ return (String("") + String(bodyHtml) + String(""));
})();
})();
})(); };
// serialize-island-state
- var serializeIslandState = function(kwargs) { return (isSxTruthy(isEmptyDict(kwargs)) ? NIL : jsonSerialize(kwargs)); };
+ var serializeIslandState = function(kwargs) { return (isSxTruthy(isEmptyDict(kwargs)) ? NIL : sxSerialize(kwargs)); };
// === Transpiled from adapter-sx ===
@@ -1505,30 +1531,34 @@ return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if
// aser-fragment
var aserFragment = function(children, env) { return (function() {
- var parts = filter(function(x) { return !isSxTruthy(isNil(x)); }, map(function(c) { return aser(c, env); }, children));
- return (isSxTruthy(isEmpty(parts)) ? "" : (String("(<> ") + String(join(" ", map(serialize, parts))) + String(")")));
+ var parts = [];
+ { var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() {
+ var result = aser(c, env);
+ return (isSxTruthy((typeOf(result) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, result) : (isSxTruthy(!isSxTruthy(isNil(result))) ? append_b(parts, serialize(result)) : NIL));
+})(); } }
+ return (isSxTruthy(isEmpty(parts)) ? "" : (String("(<> ") + String(join(" ", parts)) + String(")")));
})(); };
// aser-call
var aserCall = function(name, args, env) { return (function() {
var parts = [name];
- reduce(function(state, arg) { return (function() {
- var skip = get(state, "skip");
- return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() {
- var val = aser(nth(args, (get(state, "i") + 1)), env);
+ var skip = false;
+ var i = 0;
+ { var _c = args; for (var _i = 0; _i < _c.length; _i++) { var arg = _c[_i]; (isSxTruthy(skip) ? ((skip = false), (i = (i + 1))) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((i + 1) < len(args)))) ? (function() {
+ var val = aser(nth(args, (i + 1)), env);
if (isSxTruthy(!isSxTruthy(isNil(val)))) {
parts.push((String(":") + String(keywordName(arg))));
parts.push(serialize(val));
}
- return assoc(state, "skip", true, "i", (get(state, "i") + 1));
+ skip = true;
+ return (i = (i + 1));
})() : (function() {
var val = aser(arg, env);
if (isSxTruthy(!isSxTruthy(isNil(val)))) {
- parts.push(serialize(val));
+ (isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, val) : append_b(parts, serialize(val)));
}
- return assoc(state, "i", (get(state, "i") + 1));
-})()));
-})(); }, {["i"]: 0, ["skip"]: false}, args);
+ return (i = (i + 1));
+})())); } }
return (String("(") + String(join(" ", parts)) + String(")"));
})(); };
@@ -1582,7 +1612,7 @@ return result; }, args);
var coll = trampoline(evalExpr(nth(args, 1), env));
return map(function(item) { return (isSxTruthy(isLambda(f)) ? (function() {
var local = envMerge(lambdaClosure(f), env);
- local[first(lambdaParams(f))] = item;
+ envSet(local, first(lambdaParams(f)), item);
return aser(lambdaBody(f), local);
})() : invoke(f, item)); }, coll);
})() : (isSxTruthy((name == "map-indexed")) ? (function() {
@@ -1590,8 +1620,8 @@ return result; }, args);
var coll = trampoline(evalExpr(nth(args, 1), env));
return mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? (function() {
var local = envMerge(lambdaClosure(f), env);
- local[first(lambdaParams(f))] = i;
- local[nth(lambdaParams(f), 1)] = item;
+ envSet(local, first(lambdaParams(f)), i);
+ envSet(local, nth(lambdaParams(f), 1), item);
return aser(lambdaBody(f), local);
})() : invoke(f, i, item)); }, coll);
})() : (isSxTruthy((name == "for-each")) ? (function() {
@@ -1600,7 +1630,7 @@ return result; }, args);
var results = [];
{ var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var item = _c[_i]; (isSxTruthy(isLambda(f)) ? (function() {
var local = envMerge(lambdaClosure(f), env);
- local[first(lambdaParams(f))] = item;
+ envSet(local, first(lambdaParams(f)), item);
return append_b(results, aser(lambdaBody(f), local));
})() : invoke(f, item)); } }
return (isSxTruthy(isEmpty(results)) ? NIL : results);
@@ -1692,7 +1722,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
})(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() {
var local = envMerge(componentClosure(comp), env);
- { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
+ { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(comp))) {
(function() {
var childFrag = createFragment();
@@ -1883,7 +1913,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
return (function() {
var local = envMerge(componentClosure(island), env);
var islandName = componentName(island);
- { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
+ { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(island))) {
(function() {
var childFrag = createFragment();
@@ -2998,9 +3028,9 @@ return postSwap(target); }))) : NIL);
var exprs = sxParse(body);
return domListen(el, eventName, function(e) { return (function() {
var handlerEnv = envExtend({});
- handlerEnv["event"] = e;
- handlerEnv["this"] = el;
- handlerEnv["detail"] = eventDetail(e);
+ envSet(handlerEnv, "event", e);
+ envSet(handlerEnv, "this", el);
+ envSet(handlerEnv, "detail", eventDetail(e));
return forEach(function(expr) { return evalExpr(expr, handlerEnv); }, exprs);
})(); });
})()) : NIL);
@@ -3229,17 +3259,17 @@ callExpr.push(dictGet(kwargs, k)); } }
// hydrate-island
var hydrateIsland = function(el) { return (function() {
var name = domGetAttr(el, "data-sx-island");
- var stateJson = sxOr(domGetAttr(el, "data-sx-state"), "{}");
+ var stateSx = sxOr(domGetAttr(el, "data-sx-state"), "{}");
return (function() {
var compName = (String("~") + String(name));
var env = getRenderEnv(NIL);
return (function() {
var comp = envGet(env, compName);
return (isSxTruthy(!isSxTruthy(sxOr(isComponent(comp), isIsland(comp)))) ? logWarn((String("hydrate-island: unknown island ") + String(compName))) : (function() {
- var kwargs = jsonParse(stateJson);
+ var kwargs = sxOr(first(sxParse(stateSx)), {});
var disposers = [];
var local = envMerge(componentClosure(comp), env);
- { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
+ { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
return (function() {
var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); });
domSetTextContent(el, "");
@@ -3433,6 +3463,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
@@ -3853,8 +4002,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function domListen(el, name, handler) {
if (!_hasDom || !el) return function() {};
// Wrap SX lambdas from runtime-evaluated island code into native fns
+ // If lambda takes 0 params, call without event arg (convenience for on-click handlers)
var wrapped = isLambda(handler)
- ? function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
+ ? (lambdaParams(handler).length === 0
+ ? function(e) { try { invoke(handler); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
+ : 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);
@@ -3947,7 +4099,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 +5212,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 +5246,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 +5992,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/__init__.py b/shared/sx/__init__.py
index 20d357c8..9322cfb2 100644
--- a/shared/sx/__init__.py
+++ b/shared/sx/__init__.py
@@ -31,11 +31,8 @@ from .parser import (
parse_all,
serialize,
)
-from .evaluator import (
- EvalError,
- evaluate,
- make_env,
-)
+from .types import EvalError
+from .ref.sx_ref import evaluate, make_env
from .primitives import (
all_primitives,
diff --git a/shared/sx/async_eval.py b/shared/sx/async_eval.py
index beccab34..78c8e526 100644
--- a/shared/sx/async_eval.py
+++ b/shared/sx/async_eval.py
@@ -53,7 +53,8 @@ from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
_expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar(
"_expand_components", default=False
)
-from .evaluator import _expand_macro, EvalError
+from .ref.sx_ref import expand_macro as _expand_macro
+from .types import EvalError
from .primitives import _PRIMITIVES
from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io
from .parser import SxExpr, serialize
@@ -420,23 +421,23 @@ async def _asf_define(expr, env, ctx):
async def _asf_defcomp(expr, env, ctx):
- from .evaluator import _sf_defcomp
- return _sf_defcomp(expr, env)
+ from .ref.sx_ref import sf_defcomp
+ return sf_defcomp(expr[1:], env)
async def _asf_defstyle(expr, env, ctx):
- from .evaluator import _sf_defstyle
- return _sf_defstyle(expr, env)
+ from .ref.sx_ref import sf_defstyle
+ return sf_defstyle(expr[1:], env)
async def _asf_defmacro(expr, env, ctx):
- from .evaluator import _sf_defmacro
- return _sf_defmacro(expr, env)
+ from .ref.sx_ref import sf_defmacro
+ return sf_defmacro(expr[1:], env)
async def _asf_defhandler(expr, env, ctx):
- from .evaluator import _sf_defhandler
- return _sf_defhandler(expr, env)
+ from .ref.sx_ref import sf_defhandler
+ return sf_defhandler(expr[1:], env)
async def _asf_begin(expr, env, ctx):
@@ -599,7 +600,7 @@ async def _asf_reset(expr, env, ctx):
_ASYNC_RESET_RESUME.append(value if value is not None else NIL)
try:
# Sync re-evaluation; the async caller will trampoline
- from .evaluator import _eval as sync_eval, _trampoline
+ from .ref.sx_ref import eval_expr as sync_eval, trampoline as _trampoline
return _trampoline(sync_eval(body, env))
finally:
_ASYNC_RESET_RESUME.pop()
diff --git a/shared/sx/evaluator.py b/shared/sx/evaluator.py
deleted file mode 100644
index 4e703528..00000000
--- a/shared/sx/evaluator.py
+++ /dev/null
@@ -1,94 +0,0 @@
-"""
-S-expression evaluator — thin shim over bootstrapped sx_ref.py.
-
-All evaluation logic lives in the spec (shared/sx/ref/eval.sx) and is
-bootstrapped to Python (shared/sx/ref/sx_ref.py). This module re-exports
-the public API and internal helpers under their historical names so that
-existing callers don't need updating.
-
-Imports are lazy (inside functions/properties) to avoid circular imports
-during bootstrapping: bootstrap_py.py → parser → __init__ → evaluator → sx_ref.
-"""
-
-from __future__ import annotations
-
-
-def _ref():
- """Lazy import of the bootstrapped evaluator."""
- from .ref import sx_ref
- return sx_ref
-
-
-# ---------------------------------------------------------------------------
-# Public API — these are the most used, so we make them importable directly
-# ---------------------------------------------------------------------------
-
-class EvalError(Exception):
- """Error during expression evaluation.
-
- Delegates to the bootstrapped EvalError at runtime but is defined here
- so imports don't fail during bootstrapping.
- """
- pass
-
-
-def evaluate(expr, env=None):
- return _ref().evaluate(expr, env)
-
-
-def make_env(**kwargs):
- return _ref().make_env(**kwargs)
-
-
-# ---------------------------------------------------------------------------
-# Internal helpers — used by html.py, async_eval.py, handlers.py, etc.
-# ---------------------------------------------------------------------------
-
-def _eval(expr, env):
- return _ref().eval_expr(expr, env)
-
-
-def _trampoline(val):
- return _ref().trampoline(val)
-
-
-def _call_lambda(fn, args, caller_env):
- return _ref().call_lambda(fn, args, caller_env)
-
-
-def _call_component(comp, raw_args, env):
- return _ref().call_component(comp, raw_args, env)
-
-
-def _expand_macro(macro, raw_args, env):
- return _ref().expand_macro(macro, raw_args, env)
-
-
-# ---------------------------------------------------------------------------
-# Special-form wrappers: callers pass (expr, env) with expr[0] = head symbol.
-# sx_ref.py special forms take (args, env) where args = expr[1:].
-# ---------------------------------------------------------------------------
-
-def _sf_defcomp(expr, env):
- return _ref().sf_defcomp(expr[1:], env)
-
-def _sf_defisland(expr, env):
- return _ref().sf_defisland(expr[1:], env)
-
-def _sf_defstyle(expr, env):
- return _ref().sf_defstyle(expr[1:], env)
-
-def _sf_defmacro(expr, env):
- return _ref().sf_defmacro(expr[1:], env)
-
-def _sf_defhandler(expr, env):
- return _ref().sf_defhandler(expr[1:], env)
-
-def _sf_defpage(expr, env):
- return _ref().sf_defpage(expr[1:], env)
-
-def _sf_defquery(expr, env):
- return _ref().sf_defquery(expr[1:], env)
-
-def _sf_defaction(expr, env):
- return _ref().sf_defaction(expr[1:], env)
diff --git a/shared/sx/handlers.py b/shared/sx/handlers.py
index 0aabd8a8..1a775d43 100644
--- a/shared/sx/handlers.py
+++ b/shared/sx/handlers.py
@@ -70,10 +70,7 @@ def load_handler_file(filepath: str, service_name: str) -> list[HandlerDef]:
"""Parse an .sx file, evaluate it, and register any HandlerDef values."""
from .parser import parse_all
import os
- if os.environ.get("SX_USE_REF") == "1":
- from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
- else:
- from .evaluator import _eval as _raw_eval, _trampoline
+ from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .jinja_bridge import get_component_env
diff --git a/shared/sx/html.py b/shared/sx/html.py
index 2341bd98..694a03fc 100644
--- a/shared/sx/html.py
+++ b/shared/sx/html.py
@@ -28,7 +28,7 @@ import contextvars
from typing import Any
from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
-from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline
+from .ref.sx_ref import eval_expr as _raw_eval, call_component as _raw_call_component, expand_macro as _expand_macro, trampoline as _trampoline
def _eval(expr, env):
"""Evaluate and unwrap thunks — all html.py _eval calls are non-tail."""
@@ -414,10 +414,10 @@ def _render_component(comp: Component, args: list, env: dict[str, Any]) -> str:
def _render_island(island: Island, args: list, env: dict[str, Any]) -> str:
"""Render an island as static HTML with hydration attributes.
- Produces: body HTML
- The client hydrates this into a reactive island.
+ Produces: body HTML
+ The client hydrates this into a reactive island via sx-parse (not JSON).
"""
- import json as _json
+ from .parser import serialize as _sx_serialize
kwargs: dict[str, Any] = {}
children: list[Any] = []
@@ -443,26 +443,13 @@ def _render_island(island: Island, args: list, env: dict[str, Any]) -> str:
body_html = _render(island.body, local)
- # Serialize state for hydration — only keyword args
- state = {}
- for k, v in kwargs.items():
- if isinstance(v, (str, int, float, bool)):
- state[k] = v
- elif v is NIL or v is None:
- state[k] = None
- elif isinstance(v, list):
- state[k] = v
- elif isinstance(v, dict):
- state[k] = v
- else:
- state[k] = str(v)
-
- state_json = _escape_attr(_json.dumps(state, separators=(",", ":"))) if state else ""
+ # Serialize state for hydration — SX format (not JSON)
+ state_sx = _escape_attr(_sx_serialize(kwargs)) if kwargs else ""
island_name = _escape_attr(island.name)
parts = [f'")
parts.append(body_html)
parts.append("")
diff --git a/shared/sx/jinja_bridge.py b/shared/sx/jinja_bridge.py
index 9da4a6c4..167eac57 100644
--- a/shared/sx/jinja_bridge.py
+++ b/shared/sx/jinja_bridge.py
@@ -229,10 +229,7 @@ def register_components(sx_source: str) -> None:
(div :class "..." (div :class "..." title)))))
''')
"""
- if _os.environ.get("SX_USE_REF") == "1":
- from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
- else:
- from .evaluator import _eval as _raw_eval, _trampoline
+ from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .parser import parse_all
from .css_registry import scan_classes_from_sx
diff --git a/shared/sx/pages.py b/shared/sx/pages.py
index f0df5ce0..2050a5b2 100644
--- a/shared/sx/pages.py
+++ b/shared/sx/pages.py
@@ -127,7 +127,7 @@ def get_page_helpers(service: str) -> dict[str, Any]:
def load_page_file(filepath: str, service_name: str) -> list[PageDef]:
"""Parse an .sx file, evaluate it, and register any PageDef values."""
from .parser import parse_all
- from .evaluator import _eval as _raw_eval, _trampoline
+ from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .jinja_bridge import get_component_env
@@ -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/parser.py b/shared/sx/parser.py
index c4ac622c..f4092936 100644
--- a/shared/sx/parser.py
+++ b/shared/sx/parser.py
@@ -41,7 +41,7 @@ def _resolve_sx_reader_macro(name: str):
"""
try:
from .jinja_bridge import get_component_env
- from .evaluator import _trampoline, _call_lambda
+ from .ref.sx_ref import trampoline as _trampoline, call_lambda as _call_lambda
from .types import Lambda
except ImportError:
return None
diff --git a/shared/sx/query_registry.py b/shared/sx/query_registry.py
index e6d63e35..3edda12e 100644
--- a/shared/sx/query_registry.py
+++ b/shared/sx/query_registry.py
@@ -78,7 +78,7 @@ def clear(service: str | None = None) -> None:
def load_query_file(filepath: str, service_name: str) -> list[QueryDef]:
"""Parse an .sx file and register any defquery definitions."""
from .parser import parse_all
- from .evaluator import _eval as _raw_eval, _trampoline
+ from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .jinja_bridge import get_component_env
@@ -103,7 +103,7 @@ def load_query_file(filepath: str, service_name: str) -> list[QueryDef]:
def load_action_file(filepath: str, service_name: str) -> list[ActionDef]:
"""Parse an .sx file and register any defaction definitions."""
from .parser import parse_all
- from .evaluator import _eval as _raw_eval, _trampoline
+ from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .jinja_bridge import get_component_env
diff --git a/shared/sx/ref/adapter-async.sx b/shared/sx/ref/adapter-async.sx
new file mode 100644
index 00000000..c2f093db
--- /dev/null
+++ b/shared/sx/ref/adapter-async.sx
@@ -0,0 +1,1262 @@
+;; ==========================================================================
+;; 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)
+ ;; env-extend (not merge) — Env is not a dict subclass, so merge()
+ ;; returns an empty dict, losing all parent scope bindings.
+ (let ((local (env-extend 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)))
+ ;; apply directly — async-invoke takes &rest so passing a list
+ ;; would wrap it in another list
+ (let ((r (apply f evaled-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 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)))
+
+
+;; --------------------------------------------------------------------------
+;; async-eval-slot-inner — server-side slot expansion for aser mode
+;; --------------------------------------------------------------------------
+;;
+;; Coordinates component expansion for server-rendered pages:
+;; 1. If expression is a direct component call (~name ...), expand it
+;; 2. Otherwise aser the expression, then check if result is a (~...)
+;; call that should be re-expanded
+;;
+;; Platform primitives required:
+;; (sx-parse src) — parse SX source string
+;; (make-sx-expr s) — wrap as SxExpr
+;; (sx-expr? x) — check if SxExpr
+;; (set-expand-components!) — enable component expansion context var
+
+(define-async async-eval-slot-inner
+ (fn (expr env ctx)
+ ;; NOTE: Uses statement-form let + set! to avoid expression-context
+ ;; let (IIFE lambdas) which can't contain await in Python.
+ (let ((result nil))
+ (if (and (list? expr) (not (empty? expr)))
+ (let ((head (first expr)))
+ (if (and (= (type-of head) "symbol")
+ (starts-with? (symbol-name head) "~"))
+ (let ((name (symbol-name head))
+ (val (if (env-has? env name) (env-get env name) nil)))
+ (if (component? val)
+ (set! result (async-aser-component val (rest expr) env ctx))
+ (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx))))
+ (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx))))
+ (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx)))
+ ;; Normalize result to SxExpr
+ (if (sx-expr? result)
+ result
+ (if (nil? result)
+ (make-sx-expr "")
+ (if (string? result)
+ (make-sx-expr result)
+ (make-sx-expr (serialize result))))))))
+
+
+(define-async async-maybe-expand-result
+ (fn (result env ctx)
+ ;; If the aser result is a component call string like "(~foo ...)",
+ ;; re-parse and expand it. This handles indirect component references
+ ;; (e.g. a let binding that evaluates to a component call).
+ (let ((raw (if (sx-expr? result)
+ (trim (str result))
+ (if (string? result)
+ (trim result)
+ nil))))
+ (if (and raw (starts-with? raw "(~"))
+ (let ((parsed (sx-parse raw)))
+ (if (and parsed (not (empty? parsed)))
+ (async-eval-slot-inner (first parsed) env ctx)
+ result))
+ result))))
+
+
+;; --------------------------------------------------------------------------
+;; 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-html.sx b/shared/sx/ref/adapter-html.sx
index 039090e7..f4719e22 100644
--- a/shared/sx/ref/adapter-html.sx
+++ b/shared/sx/ref/adapter-html.sx
@@ -433,11 +433,11 @@
;; Render the island body as HTML
(let ((body-html (render-to-html (component-body island) local))
- (state-json (serialize-island-state kwargs)))
+ (state-sx (serialize-island-state kwargs)))
;; Wrap in container with hydration attributes
(str ""
body-html
@@ -445,17 +445,17 @@
;; --------------------------------------------------------------------------
-;; serialize-island-state — serialize kwargs to JSON for hydration
+;; serialize-island-state — serialize kwargs to SX for hydration
;; --------------------------------------------------------------------------
;;
-;; Only serializes simple values (numbers, strings, booleans, nil, lists, dicts).
-;; Functions, components, and other non-serializable values are skipped.
+;; Uses the SX serializer (not JSON) so the client can parse with sx-parse.
+;; Handles all SX types natively: numbers, strings, booleans, nil, lists, dicts.
(define serialize-island-state
(fn (kwargs)
(if (empty-dict? kwargs)
nil
- (json-serialize kwargs))))
+ (sx-serialize kwargs))))
;; --------------------------------------------------------------------------
@@ -476,8 +476,8 @@
;; Raw HTML construction:
;; (make-raw-html s) → wrap string as raw HTML (not double-escaped)
;;
-;; JSON serialization (for island state):
-;; (json-serialize dict) → JSON string
+;; Island state serialization:
+;; (sx-serialize val) → SX source string (from parser.sx)
;; (empty-dict? d) → boolean
;; (escape-attr s) → HTML attribute escape
;;
diff --git a/shared/sx/ref/adapter-sx.sx b/shared/sx/ref/adapter-sx.sx
index b045faa3..5bc388f0 100644
--- a/shared/sx/ref/adapter-sx.sx
+++ b/shared/sx/ref/adapter-sx.sx
@@ -106,35 +106,56 @@
(define aser-fragment
(fn (children env)
;; Serialize (<> child1 child2 ...) to sx source string
- (let ((parts (filter
- (fn (x) (not (nil? x)))
- (map (fn (c) (aser c env)) children))))
+ ;; Must flatten list results (e.g. from map/filter) to avoid nested parens
+ (let ((parts (list)))
+ (for-each
+ (fn (c)
+ (let ((result (aser c env)))
+ (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)
""
- (str "(<> " (join " " (map serialize parts)) ")")))))
+ (str "(<> " (join " " parts) ")")))))
(define aser-call
(fn (name args env)
;; Serialize (name :key val child ...) — evaluate args but keep as sx
- (let ((parts (list name)))
- (reduce
- (fn (state arg)
- (let ((skip (get state "skip")))
- (if skip
- (assoc state "skip" false "i" (inc (get state "i")))
- (if (and (= (type-of arg) "keyword")
- (< (inc (get state "i")) (len args)))
- (let ((val (aser (nth args (inc (get state "i"))) env)))
- (when (not (nil? val))
- (append! parts (str ":" (keyword-name arg)))
- (append! parts (serialize val)))
- (assoc state "skip" true "i" (inc (get state "i"))))
- (let ((val (aser arg env)))
- (when (not (nil? val))
- (append! parts (serialize val)))
- (assoc state "i" (inc (get state "i"))))))))
- (dict "i" 0 "skip" false)
+ ;; Uses for-each + mutable state (not reduce) so bootstrapper emits for-loops
+ ;; that can contain nested for-each for list flattening.
+ (let ((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 (aser (nth args (inc i)) env)))
+ (when (not (nil? val))
+ (append! parts (str ":" (keyword-name arg)))
+ (append! parts (serialize val)))
+ (set! skip true)
+ (set! i (inc i)))
+ (let ((val (aser arg env)))
+ (when (not (nil? val))
+ (if (= (type-of val) "list")
+ (for-each
+ (fn (item)
+ (when (not (nil? item))
+ (append! parts (serialize item))))
+ val)
+ (append! parts (serialize val))))
+ (set! i (inc i))))))
args)
(str "(" (join " " parts) ")"))))
diff --git a/shared/sx/ref/async_eval_ref.py b/shared/sx/ref/async_eval_ref.py
index af9c9037..96a79d56 100644
--- a/shared/sx/ref/async_eval_ref.py
+++ b/shared/sx/ref/async_eval_ref.py
@@ -1,993 +1,22 @@
-"""Async evaluation wrapper for the transpiled reference evaluator.
+"""Async evaluation — thin re-export from bootstrapped sx_ref.py.
-Wraps the sync sx_ref.py evaluator with async I/O support, mirroring
-the hand-written async_eval.py. Provides the same public API:
+The async adapter (adapter-async.sx) is now bootstrapped directly into
+sx_ref.py alongside the sync evaluator. This file re-exports the public
+API so existing imports keep working.
- async_eval() — evaluate with I/O primitives
- async_render() — render to HTML with I/O
- async_eval_to_sx() — evaluate to SX wire format with I/O
- async_eval_slot_to_sx() — expand components server-side, then serialize
+All async rendering, serialization, and evaluation logic lives in the spec:
+ - shared/sx/ref/adapter-async.sx (canonical SX source)
+ - shared/sx/ref/sx_ref.py (bootstrapped Python)
-The sync transpiled evaluator handles all control flow, special forms,
-and lambda/component dispatch. This wrapper adds:
-
- - RequestContext threading
- - I/O primitive interception (query, service, request-arg, etc.)
- - Async trampoline for thunks
- - SxExpr wrapping for wire format output
-
-DO NOT EDIT by hand — this is a thin wrapper; the actual eval logic
-lives in sx_ref.py (generated) and the I/O primitives in primitives_io.py.
+Platform async primitives (I/O dispatch, context vars, RequestContext)
+are in shared/sx/ref/platform_py.py → PLATFORM_ASYNC_PY.
"""
-from __future__ import annotations
-
-import contextvars
-import inspect
-from typing import Any
-
-from ..types import Component, Keyword, Lambda, Macro, NIL, Symbol
-from ..parser import SxExpr, serialize
-from ..primitives_io import IO_PRIMITIVES, RequestContext, execute_io
-from ..html import (
- HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,
- escape_text, escape_attr, _RawHTML, css_class_collector, _svg_context,
-)
-
from . import sx_ref
-# Re-export EvalError from sx_ref
+# Re-export the public API used by handlers.py, helpers.py, pages.py, etc.
EvalError = sx_ref.EvalError
-
-# When True, _aser expands known components server-side
-_expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar(
- "_expand_components_ref", default=False
-)
-
-
-# ---------------------------------------------------------------------------
-# Async TCO
-# ---------------------------------------------------------------------------
-
-class _AsyncThunk:
- __slots__ = ("expr", "env", "ctx")
- def __init__(self, expr, env, ctx):
- self.expr = expr
- self.env = env
- self.ctx = ctx
-
-
-async def _async_trampoline(val):
- while isinstance(val, _AsyncThunk):
- val = await _async_eval(val.expr, val.env, val.ctx)
- return val
-
-
-# ---------------------------------------------------------------------------
-# Async evaluate — wraps transpiled sync eval with I/O support
-# ---------------------------------------------------------------------------
-
-async def async_eval(expr, env, ctx=None):
- """Public entry point: evaluate with I/O primitives."""
- if ctx is None:
- ctx = RequestContext()
- result = await _async_eval(expr, env, ctx)
- while isinstance(result, _AsyncThunk):
- result = await _async_eval(result.expr, result.env, result.ctx)
- return result
-
-
-async def _async_eval(expr, env, ctx):
- """Internal async evaluator. Intercepts I/O primitives,
- delegates everything else to the sync transpiled evaluator."""
- # Intercept I/O primitive calls
- if isinstance(expr, list) and expr:
- head = expr[0]
- if isinstance(head, Symbol) and head.name in IO_PRIMITIVES:
- args, kwargs = await _parse_io_args(expr[1:], env, ctx)
- return await execute_io(head.name, args, kwargs, ctx)
-
- # Check if this is a render expression (HTML tag, component, fragment)
- # so we can wrap the result in _RawHTML to prevent double-escaping.
- # The sync evaluator returns plain strings from render_list_to_html;
- # the async renderer would HTML-escape those without this wrapper.
- is_render = isinstance(expr, list) and sx_ref.is_render_expr(expr)
-
- # For everything else, use the sync transpiled evaluator
- result = sx_ref.eval_expr(expr, env)
- result = sx_ref.trampoline(result)
-
- if is_render and isinstance(result, str):
- return _RawHTML(result)
- return result
-
-
-async def _parse_io_args(exprs, env, ctx):
- """Parse and evaluate I/O node args (keyword + positional)."""
- args = []
- kwargs = {}
- i = 0
- while i < len(exprs):
- item = exprs[i]
- if isinstance(item, Keyword) and i + 1 < len(exprs):
- kwargs[item.name] = await async_eval(exprs[i + 1], env, ctx)
- i += 2
- else:
- args.append(await async_eval(item, env, ctx))
- i += 1
- return args, kwargs
-
-
-# ---------------------------------------------------------------------------
-# Async HTML renderer
-# ---------------------------------------------------------------------------
-
-async def async_render(expr, env, ctx=None):
- """Render to HTML, awaiting I/O primitives inline."""
- if ctx is None:
- ctx = RequestContext()
- return await _arender(expr, env, ctx)
-
-
-async def _arender(expr, env, ctx):
- if expr is None or expr is NIL or expr is False or expr is True:
- return ""
- if isinstance(expr, _RawHTML):
- return expr.html
- # Also handle sx_ref._RawHTML from the sync evaluator
- if isinstance(expr, sx_ref._RawHTML):
- return expr.html
- if isinstance(expr, str):
- return escape_text(expr)
- if isinstance(expr, (int, float)):
- return escape_text(str(expr))
- if isinstance(expr, Symbol):
- val = await async_eval(expr, env, ctx)
- return await _arender(val, env, ctx)
- if isinstance(expr, Keyword):
- return escape_text(expr.name)
- if isinstance(expr, list):
- if not expr:
- return ""
- return await _arender_list(expr, env, ctx)
- if isinstance(expr, dict):
- return ""
- return escape_text(str(expr))
-
-
-async def _arender_list(expr, env, ctx):
- head = expr[0]
- if isinstance(head, Symbol):
- name = head.name
-
- # I/O primitive
- if name in IO_PRIMITIVES:
- result = await async_eval(expr, env, ctx)
- return await _arender(result, env, ctx)
-
- # raw!
- if name == "raw!":
- parts = []
- for arg in expr[1:]:
- val = await async_eval(arg, env, ctx)
- if isinstance(val, _RawHTML):
- parts.append(val.html)
- elif isinstance(val, str):
- parts.append(val)
- elif val is not None and val is not NIL:
- parts.append(str(val))
- return "".join(parts)
-
- # Fragment
- if name == "<>":
- parts = [await _arender(c, env, ctx) for c in expr[1:]]
- return "".join(parts)
-
- # html: prefix
- if name.startswith("html:"):
- return await _arender_element(name[5:], expr[1:], env, ctx)
-
- # Render-aware special forms
- arsf = _ASYNC_RENDER_FORMS.get(name)
- if arsf is not None:
- if name in HTML_TAGS and (
- (len(expr) > 1 and isinstance(expr[1], Keyword))
- or _svg_context.get(False)
- ):
- return await _arender_element(name, expr[1:], env, ctx)
- return await arsf(expr, env, ctx)
-
- # Macro expansion
- if name in env:
- val = env[name]
- if isinstance(val, Macro):
- expanded = sx_ref.trampoline(
- sx_ref.expand_macro(val, expr[1:], env)
- )
- return await _arender(expanded, env, ctx)
-
- # HTML tag
- if name in HTML_TAGS:
- return await _arender_element(name, expr[1:], env, ctx)
-
- # Component
- if name.startswith("~"):
- val = env.get(name)
- if isinstance(val, Component):
- return await _arender_component(val, expr[1:], env, ctx)
-
- # Custom element
- if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword):
- return await _arender_element(name, expr[1:], env, ctx)
-
- # SVG context
- if _svg_context.get(False):
- return await _arender_element(name, expr[1:], env, ctx)
-
- # Fallback — evaluate then render
- result = await async_eval(expr, env, ctx)
- return await _arender(result, env, ctx)
-
- if isinstance(head, (Lambda, list)):
- result = await async_eval(expr, env, ctx)
- return await _arender(result, env, ctx)
-
- # Data list
- parts = [await _arender(item, env, ctx) for item in expr]
- return "".join(parts)
-
-
-async def _arender_element(tag, args, env, ctx):
- attrs = {}
- children = []
- i = 0
- while i < len(args):
- arg = args[i]
- if isinstance(arg, Keyword) and i + 1 < len(args):
- attrs[arg.name] = await async_eval(args[i + 1], env, ctx)
- i += 2
- else:
- children.append(arg)
- i += 1
-
- class_val = attrs.get("class")
- if class_val is not None and class_val is not NIL and class_val is not False:
- collector = css_class_collector.get(None)
- if collector is not None:
- collector.update(str(class_val).split())
-
- parts = [f"<{tag}"]
- for attr_name, attr_val in attrs.items():
- if attr_val is None or attr_val is NIL or attr_val is False:
- continue
- if attr_name in BOOLEAN_ATTRS:
- if attr_val:
- parts.append(f" {attr_name}")
- elif attr_val is True:
- parts.append(f" {attr_name}")
- else:
- parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"')
- parts.append(">")
- opening = "".join(parts)
-
- if tag in VOID_ELEMENTS:
- return opening
-
- token = None
- if tag in ("svg", "math"):
- token = _svg_context.set(True)
- try:
- child_parts = [await _arender(c, env, ctx) for c in children]
- finally:
- if token is not None:
- _svg_context.reset(token)
-
- return f"{opening}{''.join(child_parts)}{tag}>"
-
-
-async def _arender_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 async_eval(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_html = [await _arender(c, env, ctx) for c in children]
- local["children"] = _RawHTML("".join(child_html))
- return await _arender(comp.body, local, ctx)
-
-
-async def _arender_lambda(fn, args, env, ctx):
- local = dict(fn.closure)
- local.update(env)
- for p, v in zip(fn.params, args):
- local[p] = v
- return await _arender(fn.body, local, ctx)
-
-
-# ---------------------------------------------------------------------------
-# Render-aware special forms
-# ---------------------------------------------------------------------------
-
-async def _arsf_if(expr, env, ctx):
- cond = await async_eval(expr[1], env, ctx)
- if cond and cond is not NIL:
- return await _arender(expr[2], env, ctx)
- return await _arender(expr[3], env, ctx) if len(expr) > 3 else ""
-
-
-async def _arsf_when(expr, env, ctx):
- cond = await async_eval(expr[1], env, ctx)
- if cond and cond is not NIL:
- return "".join([await _arender(b, env, ctx) for b in expr[2:]])
- return ""
-
-
-async def _arsf_cond(expr, env, ctx):
- clauses = expr[1:]
- if not clauses:
- return ""
- if isinstance(clauses[0], list) and len(clauses[0]) == 2:
- for clause in clauses:
- test = clause[0]
- if isinstance(test, Symbol) and test.name in ("else", ":else"):
- return await _arender(clause[1], env, ctx)
- if isinstance(test, Keyword) and test.name == "else":
- return await _arender(clause[1], env, ctx)
- if await async_eval(test, env, ctx):
- return await _arender(clause[1], env, ctx)
- else:
- i = 0
- while i < len(clauses) - 1:
- test, result = clauses[i], clauses[i + 1]
- if isinstance(test, Keyword) and test.name == "else":
- return await _arender(result, env, ctx)
- if isinstance(test, Symbol) and test.name in (":else", "else"):
- return await _arender(result, env, ctx)
- if await async_eval(test, env, ctx):
- return await _arender(result, env, ctx)
- i += 2
- return ""
-
-
-async def _arsf_let(expr, env, ctx):
- bindings = expr[1]
- local = dict(env)
- if isinstance(bindings, list):
- if bindings and isinstance(bindings[0], list):
- for b in bindings:
- var = b[0]
- vname = var.name if isinstance(var, Symbol) else var
- local[vname] = await async_eval(b[1], local, ctx)
- elif len(bindings) % 2 == 0:
- for i in range(0, len(bindings), 2):
- var = bindings[i]
- vname = var.name if isinstance(var, Symbol) else var
- local[vname] = await async_eval(bindings[i + 1], local, ctx)
- return "".join([await _arender(b, local, ctx) for b in expr[2:]])
-
-
-async def _arsf_begin(expr, env, ctx):
- return "".join([await _arender(sub, env, ctx) for sub in expr[1:]])
-
-
-async def _arsf_define(expr, env, ctx):
- await async_eval(expr, env, ctx)
- return ""
-
-
-async def _arsf_map(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- parts = []
- for item in coll:
- if isinstance(fn, Lambda):
- parts.append(await _arender_lambda(fn, (item,), env, ctx))
- elif callable(fn):
- r = fn(item)
- if inspect.iscoroutine(r):
- r = await r
- parts.append(await _arender(r, env, ctx))
- else:
- parts.append(await _arender(item, env, ctx))
- return "".join(parts)
-
-
-async def _arsf_map_indexed(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- parts = []
- for i, item in enumerate(coll):
- if isinstance(fn, Lambda):
- parts.append(await _arender_lambda(fn, (i, item), env, ctx))
- elif callable(fn):
- r = fn(i, item)
- if inspect.iscoroutine(r):
- r = await r
- parts.append(await _arender(r, env, ctx))
- else:
- parts.append(await _arender(item, env, ctx))
- return "".join(parts)
-
-
-async def _arsf_filter(expr, env, ctx):
- result = await async_eval(expr, env, ctx)
- return await _arender(result, env, ctx)
-
-
-async def _arsf_for_each(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- parts = []
- for item in coll:
- if isinstance(fn, Lambda):
- parts.append(await _arender_lambda(fn, (item,), env, ctx))
- elif callable(fn):
- r = fn(item)
- if inspect.iscoroutine(r):
- r = await r
- parts.append(await _arender(r, env, ctx))
- else:
- parts.append(await _arender(item, env, ctx))
- return "".join(parts)
-
-
-_ASYNC_RENDER_FORMS = {
- "if": _arsf_if,
- "when": _arsf_when,
- "cond": _arsf_cond,
- "let": _arsf_let,
- "let*": _arsf_let,
- "begin": _arsf_begin,
- "do": _arsf_begin,
- "define": _arsf_define,
- "defstyle": _arsf_define,
- "defcomp": _arsf_define,
- "defmacro": _arsf_define,
- "defhandler": _arsf_define,
- "map": _arsf_map,
- "map-indexed": _arsf_map_indexed,
- "filter": _arsf_filter,
- "for-each": _arsf_for_each,
-}
-
-
-# ---------------------------------------------------------------------------
-# Async SX wire format (aser)
-# ---------------------------------------------------------------------------
-
-async def async_eval_to_sx(expr, env, ctx=None):
- """Evaluate and produce SX source string (wire format)."""
- if ctx is None:
- ctx = RequestContext()
- result = await _aser(expr, env, ctx)
- if isinstance(result, SxExpr):
- return result
- if result is None or result is NIL:
- return SxExpr("")
- if isinstance(result, str):
- return SxExpr(result)
- return SxExpr(serialize(result))
-
-
-async def async_eval_slot_to_sx(expr, env, ctx=None):
- """Like async_eval_to_sx but expands component calls server-side."""
- if ctx is None:
- ctx = RequestContext()
- token = _expand_components.set(True)
- try:
- return await _eval_slot_inner(expr, env, ctx)
- finally:
- _expand_components.reset(token)
-
-
-async def _eval_slot_inner(expr, env, ctx):
- if isinstance(expr, list) and expr:
- head = expr[0]
- if isinstance(head, Symbol) and head.name.startswith("~"):
- comp = env.get(head.name)
- if isinstance(comp, Component):
- result = await _aser_component(comp, expr[1:], env, ctx)
- if isinstance(result, SxExpr):
- return result
- if result is None or result is NIL:
- return SxExpr("")
- if isinstance(result, str):
- return SxExpr(result)
- return SxExpr(serialize(result))
- result = await _aser(expr, env, ctx)
- result = await _maybe_expand_component_result(result, env, ctx)
- if isinstance(result, SxExpr):
- return result
- if result is None or result is NIL:
- return SxExpr("")
- if isinstance(result, str):
- return SxExpr(result)
- return SxExpr(serialize(result))
-
-
-async def _maybe_expand_component_result(result, env, ctx):
- raw = None
- if isinstance(result, SxExpr):
- raw = str(result).strip()
- elif isinstance(result, str):
- raw = result.strip()
- if raw and raw.startswith("(~"):
- from ..parser import parse_all
- parsed = parse_all(raw)
- if parsed:
- return await async_eval_slot_to_sx(parsed[0], env, ctx)
- return result
-
-
-async def _aser(expr, env, ctx):
- """Evaluate for SX wire format — serialize rendering forms, evaluate control flow."""
- if isinstance(expr, (int, float, bool)):
- return expr
- if isinstance(expr, SxExpr):
- return expr
- if isinstance(expr, str):
- return expr
- if expr is None or expr is NIL:
- return NIL
-
- if isinstance(expr, Symbol):
- name = expr.name
- if name in env:
- return env[name]
- if sx_ref.is_primitive(name):
- return sx_ref.get_primitive(name)
- if name == "true":
- return True
- if name == "false":
- return False
- if name == "nil":
- return NIL
- raise EvalError(f"Undefined symbol: {name}")
-
- if isinstance(expr, Keyword):
- return expr.name
-
- if isinstance(expr, dict):
- return {k: await _aser(v, env, ctx) for k, v in expr.items()}
-
- if not isinstance(expr, list):
- return expr
- if not expr:
- return []
-
- head = expr[0]
- if not isinstance(head, (Symbol, Lambda, list)):
- return [await _aser(x, env, ctx) for x in expr]
-
- if isinstance(head, Symbol):
- name = head.name
-
- # I/O primitives
- if name in IO_PRIMITIVES:
- args, kwargs = await _parse_io_args(expr[1:], env, ctx)
- return await execute_io(name, args, kwargs, ctx)
-
- # Fragment
- if name == "<>":
- return await _aser_fragment(expr[1:], env, ctx)
-
- # raw!
- if name == "raw!":
- return await _aser_call("raw!", expr[1:], env, ctx)
-
- # html: prefix
- if name.startswith("html:"):
- return await _aser_call(name[5:], expr[1:], env, ctx)
-
- # Component call
- if name.startswith("~"):
- val = env.get(name)
- if isinstance(val, Macro):
- expanded = sx_ref.trampoline(
- sx_ref.expand_macro(val, expr[1:], env)
- )
- return await _aser(expanded, env, ctx)
- if isinstance(val, Component) and _expand_components.get():
- return await _aser_component(val, expr[1:], env, ctx)
- return await _aser_call(name, expr[1:], env, ctx)
-
- # Serialize-mode special/HO forms
- sf = _ASER_FORMS.get(name)
- if sf is not None:
- if name in HTML_TAGS and (
- (len(expr) > 1 and isinstance(expr[1], Keyword))
- or _svg_context.get(False)
- ):
- return await _aser_call(name, expr[1:], env, ctx)
- return await sf(expr, env, ctx)
-
- # HTML tag
- if name in HTML_TAGS:
- return await _aser_call(name, expr[1:], env, ctx)
-
- # Macro
- if name in env:
- val = env[name]
- if isinstance(val, Macro):
- expanded = sx_ref.trampoline(
- sx_ref.expand_macro(val, expr[1:], env)
- )
- return await _aser(expanded, env, ctx)
-
- # Custom element
- if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword):
- return await _aser_call(name, expr[1:], env, ctx)
-
- # SVG context
- if _svg_context.get(False):
- return await _aser_call(name, expr[1:], env, ctx)
-
- # Function/lambda call
- 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)):
- result = fn(*args)
- if inspect.iscoroutine(result):
- return await result
- return result
- if isinstance(fn, Lambda):
- local = dict(fn.closure)
- local.update(env)
- for p, v in zip(fn.params, args):
- local[p] = v
- 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}")
-
-
-async def _aser_fragment(children, env, ctx):
- parts = []
- for child in children:
- result = await _aser(child, env, ctx)
- if isinstance(result, list):
- for item in result:
- if item is not NIL and item is not None:
- parts.append(serialize(item))
- elif result is not NIL and result is not None:
- parts.append(serialize(result))
- if not parts:
- return SxExpr("")
- return SxExpr("(<> " + " ".join(parts) + ")")
-
-
-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)
-
-
-async def _aser_call(name, args, env, ctx):
- token = None
- if name in ("svg", "math"):
- token = _svg_context.set(True)
- try:
- parts = [name]
- extra_class = None
- i = 0
- while i < len(args):
- arg = args[i]
- if isinstance(arg, Keyword) and i + 1 < len(args):
- val = await _aser(args[i + 1], env, ctx)
- if val is not NIL and val is not None:
- parts.append(f":{arg.name}")
- if isinstance(val, list):
- live = [v for v in val if v is not NIL and v is not None]
- items = [serialize(v) for v in live]
- if not items:
- parts.append("nil")
- elif any(isinstance(v, SxExpr) for v in live):
- parts.append("(<> " + " ".join(items) + ")")
- else:
- parts.append("(list " + " ".join(items) + ")")
- else:
- parts.append(serialize(val))
- i += 2
- else:
- result = await _aser(arg, env, ctx)
- if result is not NIL and result is not None:
- if isinstance(result, list):
- for item in result:
- if item is not NIL and item is not None:
- parts.append(serialize(item))
- else:
- parts.append(serialize(result))
- i += 1
- if extra_class:
- _merge_class_into_parts(parts, extra_class)
- return SxExpr("(" + " ".join(parts) + ")")
- finally:
- if token is not None:
- _svg_context.reset(token)
-
-
-def _merge_class_into_parts(parts, class_name):
- for i, p in enumerate(parts):
- if p == ":class" and i + 1 < len(parts):
- existing = parts[i + 1]
- if existing.startswith('"') and existing.endswith('"'):
- parts[i + 1] = existing[:-1] + " " + class_name + '"'
- else:
- parts[i + 1] = f'(str {existing} " {class_name}")'
- return
- parts.insert(1, f'"{class_name}"')
- parts.insert(1, ":class")
-
-
-# ---------------------------------------------------------------------------
-# Aser-mode special forms
-# ---------------------------------------------------------------------------
-
-async def _assf_if(expr, env, ctx):
- cond = await async_eval(expr[1], env, ctx)
- if cond and cond is not NIL:
- return await _aser(expr[2], env, ctx)
- return await _aser(expr[3], env, ctx) if len(expr) > 3 else NIL
-
-
-async def _assf_when(expr, env, ctx):
- cond = await async_eval(expr[1], env, ctx)
- if cond and cond is not NIL:
- result = NIL
- for body_expr in expr[2:]:
- result = await _aser(body_expr, env, ctx)
- return result
- return NIL
-
-
-async def _assf_let(expr, env, ctx):
- bindings = expr[1]
- local = dict(env)
- if isinstance(bindings, list):
- if bindings and isinstance(bindings[0], list):
- for b in bindings:
- var = b[0]
- vname = var.name if isinstance(var, Symbol) else var
- local[vname] = await _aser(b[1], local, ctx)
- elif len(bindings) % 2 == 0:
- for i in range(0, len(bindings), 2):
- var = bindings[i]
- vname = var.name if isinstance(var, Symbol) else var
- local[vname] = await _aser(bindings[i + 1], local, ctx)
- result = NIL
- for body_expr in expr[2:]:
- result = await _aser(body_expr, local, ctx)
- return result
-
-
-async def _assf_cond(expr, env, ctx):
- clauses = expr[1:]
- if not clauses:
- return NIL
- if isinstance(clauses[0], list) and len(clauses[0]) == 2:
- for clause in clauses:
- test = clause[0]
- if isinstance(test, Symbol) and test.name in ("else", ":else"):
- return await _aser(clause[1], env, ctx)
- if isinstance(test, Keyword) and test.name == "else":
- return await _aser(clause[1], env, ctx)
- if await async_eval(test, env, ctx):
- return await _aser(clause[1], env, ctx)
- else:
- i = 0
- while i < len(clauses) - 1:
- test, result = clauses[i], clauses[i + 1]
- if isinstance(test, Keyword) and test.name == "else":
- return await _aser(result, env, ctx)
- if isinstance(test, Symbol) and test.name in (":else", "else"):
- return await _aser(result, env, ctx)
- if await async_eval(test, env, ctx):
- return await _aser(result, env, ctx)
- i += 2
- return NIL
-
-
-async def _assf_case(expr, env, ctx):
- match_val = await async_eval(expr[1], env, ctx)
- clauses = expr[2:]
- i = 0
- while i < len(clauses) - 1:
- test, result = clauses[i], clauses[i + 1]
- if isinstance(test, Keyword) and test.name == "else":
- return await _aser(result, env, ctx)
- if isinstance(test, Symbol) and test.name in (":else", "else"):
- return await _aser(result, env, ctx)
- if match_val == await async_eval(test, env, ctx):
- return await _aser(result, env, ctx)
- i += 2
- return NIL
-
-
-async def _assf_begin(expr, env, ctx):
- result = NIL
- for sub in expr[1:]:
- result = await _aser(sub, env, ctx)
- return result
-
-
-async def _assf_define(expr, env, ctx):
- await async_eval(expr, env, ctx)
- return NIL
-
-
-async def _assf_and(expr, env, ctx):
- result = True
- for arg in expr[1:]:
- result = await async_eval(arg, env, ctx)
- if not result:
- return result
- return result
-
-
-async def _assf_or(expr, env, ctx):
- result = False
- for arg in expr[1:]:
- result = await async_eval(arg, env, ctx)
- if result:
- return result
- return result
-
-
-async def _assf_lambda(expr, env, ctx):
- params_expr = expr[1]
- param_names = []
- for p in params_expr:
- if isinstance(p, Symbol):
- param_names.append(p.name)
- elif isinstance(p, str):
- param_names.append(p)
- return Lambda(param_names, expr[2], dict(env))
-
-
-async def _assf_quote(expr, env, ctx):
- return expr[1] if len(expr) > 1 else NIL
-
-
-async def _assf_thread_first(expr, env, ctx):
- result = await async_eval(expr[1], env, ctx)
- for form in expr[2:]:
- if isinstance(form, list):
- fn = await async_eval(form[0], env, ctx)
- fn_args = [result] + [await async_eval(a, env, ctx) for a in form[1:]]
- else:
- fn = await async_eval(form, env, ctx)
- fn_args = [result]
- if callable(fn) and not isinstance(fn, (Lambda, Component)):
- result = fn(*fn_args)
- if inspect.iscoroutine(result):
- result = await result
- elif isinstance(fn, Lambda):
- local = dict(fn.closure)
- local.update(env)
- for p, v in zip(fn.params, fn_args):
- local[p] = v
- result = await async_eval(fn.body, local, ctx)
- else:
- raise EvalError(f"-> form not callable: {fn!r}")
- return result
-
-
-async def _assf_set_bang(expr, env, ctx):
- value = await async_eval(expr[2], env, ctx)
- env[expr[1].name] = value
- return value
-
-
-# Aser-mode HO forms
-
-async def _asho_map(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- results = []
- for item in coll:
- if isinstance(fn, Lambda):
- local = dict(fn.closure)
- local.update(env)
- local[fn.params[0]] = item
- results.append(await _aser(fn.body, local, ctx))
- elif callable(fn):
- r = fn(item)
- results.append(await r if inspect.iscoroutine(r) else r)
- else:
- raise EvalError(f"map requires callable, got {type(fn).__name__}")
- return results
-
-
-async def _asho_map_indexed(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- results = []
- for i, item in enumerate(coll):
- if isinstance(fn, Lambda):
- local = dict(fn.closure)
- local.update(env)
- local[fn.params[0]] = i
- local[fn.params[1]] = item
- results.append(await _aser(fn.body, local, ctx))
- elif callable(fn):
- r = fn(i, item)
- results.append(await r if inspect.iscoroutine(r) else r)
- else:
- raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}")
- return results
-
-
-async def _asho_filter(expr, env, ctx):
- return await async_eval(expr, env, ctx)
-
-
-async def _asho_for_each(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- results = []
- for item in coll:
- if isinstance(fn, Lambda):
- local = dict(fn.closure)
- local.update(env)
- local[fn.params[0]] = item
- results.append(await _aser(fn.body, local, ctx))
- elif callable(fn):
- r = fn(item)
- results.append(await r if inspect.iscoroutine(r) else r)
- return results
-
-
-_ASER_FORMS = {
- "if": _assf_if,
- "when": _assf_when,
- "cond": _assf_cond,
- "case": _assf_case,
- "and": _assf_and,
- "or": _assf_or,
- "let": _assf_let,
- "let*": _assf_let,
- "lambda": _assf_lambda,
- "fn": _assf_lambda,
- "define": _assf_define,
- "defstyle": _assf_define,
- "defcomp": _assf_define,
- "defmacro": _assf_define,
- "defhandler": _assf_define,
- "begin": _assf_begin,
- "do": _assf_begin,
- "quote": _assf_quote,
- "->": _assf_thread_first,
- "set!": _assf_set_bang,
- "map": _asho_map,
- "map-indexed": _asho_map_indexed,
- "filter": _asho_filter,
- "for-each": _asho_for_each,
-}
+async_eval = sx_ref.async_eval
+async_render = sx_ref.async_render
+async_eval_to_sx = sx_ref.async_eval_to_sx
+async_eval_slot_to_sx = sx_ref.async_eval_slot_to_sx
diff --git a/shared/sx/ref/boot.sx b/shared/sx/ref/boot.sx
index afb9d55d..30a2e634 100644
--- a/shared/sx/ref/boot.sx
+++ b/shared/sx/ref/boot.sx
@@ -344,15 +344,15 @@
(define hydrate-island
(fn (el)
(let ((name (dom-get-attr el "data-sx-island"))
- (state-json (or (dom-get-attr el "data-sx-state") "{}")))
+ (state-sx (or (dom-get-attr el "data-sx-state") "{}")))
(let ((comp-name (str "~" name))
(env (get-render-env nil)))
(let ((comp (env-get env comp-name)))
(if (not (or (component? comp) (island? comp)))
(log-warn (str "hydrate-island: unknown island " comp-name))
- ;; Parse state and build keyword args
- (let ((kwargs (json-parse state-json))
+ ;; Parse state and build keyword args — SX format, not JSON
+ (let ((kwargs (or (first (sx-parse state-sx)) {}))
(disposers (list))
(local (env-merge (component-closure comp) env)))
@@ -494,8 +494,8 @@
;; (log-info msg) → void (console.log with prefix)
;; (log-parse-error label text err) → void (diagnostic parse error)
;;
-;; === JSON ===
-;; (json-parse str) → dict/list/value (JSON.parse)
+;; === Parsing (island state) ===
+;; (sx-parse str) → list of AST expressions (from parser.sx)
;;
;; === Processing markers ===
;; (mark-processed! el key) → void
diff --git a/shared/sx/ref/bootstrap_py.py b/shared/sx/ref/bootstrap_py.py
index 20c2383b..4cd506b8 100644
--- a/shared/sx/ref/bootstrap_py.py
+++ b/shared/sx/ref/bootstrap_py.py
@@ -49,6 +49,8 @@ class PyEmitter:
def __init__(self):
self.indent = 0
+ self._async_names: set[str] = set() # SX names of define-async functions
+ self._in_async: bool = False # Currently emitting async def body?
def emit(self, expr) -> str:
"""Emit a Python expression from an SX AST node."""
@@ -80,6 +82,8 @@ class PyEmitter:
name = head.name
if name == "define":
return self._emit_define(expr, indent)
+ if name == "define-async":
+ return self._emit_define_async(expr, indent)
if name == "set!":
return f"{pad}{self._mangle(expr[1].name)} = {self.emit(expr[2])}"
if name == "when":
@@ -275,6 +279,19 @@ class PyEmitter:
"sf-defisland": "sf_defisland",
# adapter-sx.sx
"render-to-sx": "render_to_sx",
+ # adapter-async.sx platform primitives
+ "svg-context-set!": "svg_context_set",
+ "svg-context-reset!": "svg_context_reset",
+ "css-class-collect!": "css_class_collect",
+ "is-raw-html?": "is_raw_html",
+ "async-coroutine?": "is_async_coroutine",
+ "async-await!": "async_await",
+ "is-sx-expr?": "is_sx_expr",
+ "sx-expr?": "is_sx_expr",
+ "io-primitive?": "io_primitive_p",
+ "expand-components?": "expand_components_p",
+ "svg-context?": "svg_context_p",
+ "make-sx-expr": "make_sx_expr",
"aser": "aser",
"eval-case-aser": "eval_case_aser",
"sx-serialize": "sx_serialize",
@@ -417,6 +434,8 @@ class PyEmitter:
# Regular function call
fn_name = self._mangle(name)
args = ", ".join(self.emit(x) for x in expr[1:])
+ if self._in_async and name in self._async_names:
+ return f"(await {fn_name}({args}))"
return f"{fn_name}({args})"
# --- Special form emitters ---
@@ -513,13 +532,16 @@ class PyEmitter:
body_parts = expr[2:]
lines = [f"{pad}if sx_truthy({cond}):"]
for b in body_parts:
- lines.append(self.emit_statement(b, indent + 1))
+ self._emit_stmt_recursive(b, lines, indent + 1)
return "\n".join(lines)
def _emit_cond(self, expr) -> str:
clauses = expr[1:]
if not clauses:
return "NIL"
+ # Check ALL clauses are 2-element lists (scheme-style).
+ # Checking only the first is ambiguous — (nil? x) is a 2-element
+ # function call, not a scheme clause ((test body)).
is_scheme = (
all(isinstance(c, list) and len(c) == 2 for c in clauses)
and not any(isinstance(c, Keyword) for c in clauses)
@@ -642,6 +664,16 @@ class PyEmitter:
val = self.emit(val_expr)
return f"{pad}{self._mangle(name)} = {val}"
+ def _emit_define_async(self, expr, indent: int = 0) -> str:
+ """Emit a define-async form as an async def statement."""
+ name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
+ val_expr = expr[2]
+ if (isinstance(val_expr, list) and val_expr and
+ isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")):
+ return self._emit_define_as_def(name, val_expr, indent, is_async=True)
+ # Shouldn't happen — define-async should always wrap fn/lambda
+ return self._emit_define(expr, indent)
+
def _body_uses_set(self, fn_expr) -> bool:
"""Check if a fn expression's body (recursively) uses set!."""
def _has_set(node):
@@ -654,12 +686,16 @@ class PyEmitter:
body = fn_expr[2:]
return any(_has_set(b) for b in body)
- def _emit_define_as_def(self, name: str, fn_expr, indent: int = 0) -> str:
+ def _emit_define_as_def(self, name: str, fn_expr, indent: int = 0,
+ is_async: bool = False) -> str:
"""Emit a define with fn value as a proper def statement.
This is used for functions that contain set! — Python closures can't
rebind outer lambda params, so we need proper def + local variables.
Variables mutated by set! from nested lambdas use a _cells dict.
+
+ When is_async=True, emits 'async def' and sets _in_async so that
+ calls to other async functions receive 'await'.
"""
pad = " " * indent
params = fn_expr[1]
@@ -686,14 +722,19 @@ class PyEmitter:
py_name = self._mangle(name)
# Find set! target variables that are used from nested lambda scopes
nested_set_vars = self._find_nested_set_vars(body)
- lines = [f"{pad}def {py_name}({params_str}):"]
+ def_kw = "async def" if is_async else "def"
+ lines = [f"{pad}{def_kw} {py_name}({params_str}):"]
if nested_set_vars:
lines.append(f"{pad} _cells = {{}}")
- # Emit body with cell var tracking
+ # Emit body with cell var tracking (and async context if needed)
old_cells = getattr(self, '_current_cell_vars', set())
+ old_async = self._in_async
self._current_cell_vars = nested_set_vars
+ if is_async:
+ self._in_async = True
self._emit_body_stmts(body, lines, indent + 1)
self._current_cell_vars = old_cells
+ self._in_async = old_async
return "\n".join(lines)
def _find_nested_set_vars(self, body) -> set[str]:
@@ -750,7 +791,7 @@ class PyEmitter:
if is_last:
self._emit_return_expr(expr, lines, indent)
else:
- lines.append(self.emit_statement(expr, indent))
+ self._emit_stmt_recursive(expr, lines, indent)
def _emit_return_expr(self, expr, lines: list, indent: int) -> None:
"""Emit an expression in return position, flattening control flow."""
@@ -775,6 +816,11 @@ class PyEmitter:
if name in ("do", "begin"):
self._emit_body_stmts(expr[1:], lines, indent)
return
+ if name == "for-each":
+ # for-each in return position: emit as statement, return NIL
+ lines.append(self._emit_for_each_stmt(expr, indent))
+ lines.append(f"{pad}return NIL")
+ return
lines.append(f"{pad}return {self.emit(expr)}")
def _emit_if_return(self, expr, lines: list, indent: int) -> None:
@@ -1034,12 +1080,15 @@ class PyEmitter:
# ---------------------------------------------------------------------------
def extract_defines(source: str) -> list[tuple[str, list]]:
- """Parse .sx source, return list of (name, define-expr) for top-level defines."""
+ """Parse .sx source, return list of (name, define-expr) for top-level defines.
+
+ Extracts both (define ...) and (define-async ...) forms.
+ """
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":
+ if expr[0].name in ("define", "define-async"):
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
defines.append((name, expr))
return defines
@@ -1196,6 +1245,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
@@ -1210,6 +1261,28 @@ def compile_ref_to_py(
for name in sorted(spec_mod_set):
sx_files.append(SPEC_MODULES[name])
+ # Pre-scan define-async names (needed before transpilation so emitter
+ # knows which calls require 'await')
+ has_async = "async" in adapter_set
+ if has_async:
+ async_filename = ADAPTER_FILES["async"][0]
+ async_filepath = os.path.join(ref_dir, async_filename)
+ if os.path.exists(async_filepath):
+ with open(async_filepath) as f:
+ async_src = f.read()
+ for aexpr in parse_all(async_src):
+ if (isinstance(aexpr, list) and aexpr
+ and isinstance(aexpr[0], Symbol)
+ and aexpr[0].name == "define-async"):
+ aname = aexpr[1].name if isinstance(aexpr[1], Symbol) else str(aexpr[1])
+ emitter._async_names.add(aname)
+ # Platform async primitives (provided by host, also need await)
+ emitter._async_names.update({
+ "async-eval", "execute-io", "async-await!",
+ })
+ # Async adapter is transpiled last (after sync adapters)
+ sx_files.append(ADAPTER_FILES["async"])
+
all_sections = []
for filename, label in sx_files:
filepath = os.path.join(ref_dir, filename)
@@ -1246,6 +1319,9 @@ def compile_ref_to_py(
if has_deps:
parts.append(PLATFORM_DEPS_PY)
+ if has_async:
+ parts.append(PLATFORM_ASYNC_PY)
+
for label, defines in all_sections:
parts.append(f"\n# === Transpiled from {label} ===\n")
for name, expr in defines:
@@ -1256,7 +1332,7 @@ def compile_ref_to_py(
parts.append(FIXUPS_PY)
if has_continuations:
parts.append(CONTINUATIONS_PY)
- parts.append(public_api_py(has_html, has_sx, has_deps))
+ parts.append(public_api_py(has_html, has_sx, has_deps, has_async))
return "\n".join(parts)
diff --git a/shared/sx/ref/bootstrap_test.py b/shared/sx/ref/bootstrap_test.py
index 08eb64bc..0f1ef43c 100644
--- a/shared/sx/ref/bootstrap_test.py
+++ b/shared/sx/ref/bootstrap_test.py
@@ -143,7 +143,7 @@ def _emit_py(suites: list[dict], preamble: list) -> str:
lines.append('')
lines.append('import pytest')
lines.append('from shared.sx.parser import parse_all')
- lines.append('from shared.sx.evaluator import _eval, _trampoline')
+ lines.append('from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline')
lines.append('')
lines.append('')
lines.append(f"_PREAMBLE = '''{preamble_escaped}'''")
diff --git a/shared/sx/ref/engine.sx b/shared/sx/ref/engine.sx
index e626d56d..cdbfb140 100644
--- a/shared/sx/ref/engine.sx
+++ b/shared/sx/ref/engine.sx
@@ -34,11 +34,12 @@
(define parse-time
(fn (s)
;; Parse time string: "2s" → 2000, "500ms" → 500
- (cond
- (nil? s) 0
- (ends-with? s "ms") (parse-int s 0)
- (ends-with? s "s") (* (parse-int (replace s "s" "") 0) 1000)
- :else (parse-int s 0))))
+ ;; Uses nested if (not cond) because cond misclassifies 2-element
+ ;; function calls like (nil? s) as scheme-style ((test body)) clauses.
+ (if (nil? s) 0
+ (if (ends-with? s "ms") (parse-int s 0)
+ (if (ends-with? s "s") (* (parse-int (replace s "s" "") 0) 1000)
+ (parse-int s 0))))))
(define parse-trigger-spec
@@ -219,20 +220,19 @@
;; Filter form parameters by sx-params spec.
;; all-params is a list of (key value) pairs.
;; Returns filtered list of (key value) pairs.
- (cond
- (nil? params-spec) all-params
- (= params-spec "none") (list)
- (= params-spec "*") all-params
- (starts-with? params-spec "not ")
- (let ((excluded (map trim (split (slice params-spec 4) ","))))
- (filter
- (fn (p) (not (contains? excluded (first p))))
- all-params))
- :else
- (let ((allowed (map trim (split params-spec ","))))
- (filter
- (fn (p) (contains? allowed (first p)))
- all-params)))))
+ ;; Uses nested if (not cond) — see parse-time comment.
+ (if (nil? params-spec) all-params
+ (if (= params-spec "none") (list)
+ (if (= params-spec "*") all-params
+ (if (starts-with? params-spec "not ")
+ (let ((excluded (map trim (split (slice params-spec 4) ","))))
+ (filter
+ (fn (p) (not (contains? excluded (first p))))
+ all-params))
+ (let ((allowed (map trim (split params-spec ","))))
+ (filter
+ (fn (p) (contains? allowed (first p)))
+ all-params))))))))
;; --------------------------------------------------------------------------
diff --git a/shared/sx/ref/eval.sx b/shared/sx/ref/eval.sx
index 1aa55dbd..b8ea21fe 100644
--- a/shared/sx/ref/eval.sx
+++ b/shared/sx/ref/eval.sx
@@ -309,14 +309,18 @@
nil))))
+;; cond-scheme? — check if ALL clauses are 2-element lists (scheme-style).
+;; Checking only the first arg is ambiguous — (nil? x) is a 2-element
+;; function call, not a scheme clause ((test body)).
+(define cond-scheme?
+ (fn (clauses)
+ (every? (fn (c) (and (= (type-of c) "list") (= (len c) 2)))
+ clauses)))
+
(define sf-cond
(fn (args env)
- ;; Detect scheme-style: first arg is a 2-element list
- (if (and (= (type-of (first args)) "list")
- (= (len (first args)) 2))
- ;; Scheme-style: ((test body) ...)
+ (if (cond-scheme? args)
(sf-cond-scheme args env)
- ;; Clojure-style: test body test body ...
(sf-cond-clojure args env))))
(define sf-cond-scheme
diff --git a/shared/sx/ref/js.sx b/shared/sx/ref/js.sx
index cea6379b..3c35b73b 100644
--- a/shared/sx/ref/js.sx
+++ b/shared/sx/ref/js.sx
@@ -1290,8 +1290,9 @@
(= name "append!")
(str (js-expr (nth expr 1)) ".push(" (js-expr (nth expr 2)) ");")
(= name "env-set!")
- (str (js-expr (nth expr 1)) "[" (js-expr (nth expr 2))
- "] = " (js-expr (nth expr 3)) ";")
+ (str "envSet(" (js-expr (nth expr 1))
+ ", " (js-expr (nth expr 2))
+ ", " (js-expr (nth expr 3)) ");")
(= name "set-lambda-name!")
(str (js-expr (nth expr 1)) ".name = " (js-expr (nth expr 2)) ";")
:else
diff --git a/shared/sx/ref/page-helpers.sx b/shared/sx/ref/page-helpers.sx
new file mode 100644
index 00000000..69e2ba1c
--- /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 00000000..ad5e759e
--- /dev/null
+++ b/shared/sx/ref/platform_js.py
@@ -0,0 +1,3189 @@
+"""
+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) {
+ return JSON.stringify(obj);
+ }
+ 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) {
+ // Walk prototype chain to find where the variable is defined (for set!)
+ var obj = env;
+ while (obj !== null && obj !== Object.prototype) {
+ if (obj.hasOwnProperty(name)) { obj[name] = val; return; }
+ obj = Object.getPrototypeOf(obj);
+ }
+ // Not found in any parent scope — set on the immediate env
+ env[name] = val;
+ }
+ function envExtend(env) { return Object.create(env); }
+ function envMerge(base, overlay) {
+ // Same env or overlay is descendant of base — just extend, no copy.
+ // This prevents set! inside lambdas from modifying shadow copies.
+ if (base === overlay) return Object.create(base);
+ var p = overlay;
+ for (var d = 0; p && p !== Object.prototype && d < 100; d++) {
+ if (p === base) return Object.create(base);
+ p = Object.getPrototypeOf(p);
+ }
+ // General case: extend base, copy ONLY overlay properties that don't
+ // exist in the base chain (avoids shadowing closure bindings).
+ var child = Object.create(base);
+ if (overlay) {
+ for (var k in overlay) {
+ if (overlay.hasOwnProperty(k) && !(k in base)) 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
+ // If lambda takes 0 params, call without event arg (convenience for on-click handlers)
+ var wrapped = isLambda(handler)
+ ? (lambdaParams(handler).length === 0
+ ? function(e) { try { invoke(handler); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
+ : 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(/