Merge branch 'main' into worktree-typed-sx

# Conflicts:
#	shared/sx/ref/platform_py.py
#	shared/sx/ref/sx_ref.py
This commit is contained in:
2026-03-11 17:06:30 +00:00
47 changed files with 7522 additions and 1702 deletions

View File

@@ -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("<span data-sx-island=\"") + String(escapeAttr(islandName)) + String("\"") + String((isSxTruthy(stateJson) ? (String(" data-sx-state=\"") + String(escapeAttr(stateJson)) + String("\"")) : "")) + String(">") + String(bodyHtml) + String("</span>"));
var stateSx = serializeIslandState(kwargs);
return (String("<span data-sx-island=\"") + String(escapeAttr(islandName)) + String("\"") + String((isSxTruthy(stateSx) ? (String(" data-sx-state=\"") + String(escapeAttr(stateSx)) + String("\"")) : "")) + String(">") + String(bodyHtml) + String("</span>"));
})();
})();
})(); };
// 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,

View File

@@ -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,

View File

@@ -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()

View File

@@ -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)

View File

@@ -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
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .jinja_bridge import get_component_env

View File

@@ -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: <span data-sx-island="name" data-sx-state='{"k":"v",...}'>body HTML</span>
The client hydrates this into a reactive island.
Produces: <span data-sx-island="name" data-sx-state="{:k &quot;v&quot;}">body HTML</span>
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'<span data-sx-island="{island_name}"']
if state_json:
parts.append(f' data-sx-state="{state_json}"')
if state_sx:
parts.append(f' data-sx-state="{state_sx}"')
parts.append(">")
parts.append(body_html)
parts.append("</span>")

View File

@@ -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
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .parser import parse_all
from .css_registry import scan_classes_from_sx

View File

@@ -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,6 +170,10 @@ 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.
"""
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)

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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 "<span data-sx-island=\"" (escape-attr island-name) "\""
(if state-json
(str " data-sx-state=\"" (escape-attr state-json) "\"")
(if state-sx
(str " data-sx-state=\"" (escape-attr state-sx) "\"")
"")
">"
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
;;

View File

@@ -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")))
;; 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
(assoc state "skip" false "i" (inc (get state "i")))
(do (set! skip false)
(set! i (inc i)))
(if (and (= (type-of arg) "keyword")
(< (inc (get state "i")) (len args)))
(let ((val (aser (nth args (inc (get state "i"))) env)))
(< (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)))
(assoc state "skip" true "i" (inc (get state "i"))))
(set! skip true)
(set! i (inc 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)
(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) ")"))))

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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)

View File

@@ -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}'''")

View File

@@ -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 ")
;; 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))
:else
(let ((allowed (map trim (split params-spec ","))))
(filter
(fn (p) (contains? allowed (first p)))
all-params)))))
all-params))))))))
;; --------------------------------------------------------------------------

View File

@@ -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

View File

@@ -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

View File

@@ -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}))

3189
shared/sx/ref/platform_js.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -470,10 +470,7 @@ def invoke(f, *args):
def json_serialize(obj):
import json
try:
return json.dumps(obj)
except (TypeError, ValueError):
return "{}"
def is_empty_dict(d):
@@ -608,7 +605,7 @@ def sx_expr_source(x):
try:
from shared.sx.evaluator import EvalError
from shared.sx.types import EvalError
except ImportError:
class EvalError(Exception):
pass
@@ -1075,10 +1072,19 @@ import inspect
from shared.sx.primitives_io import (
IO_PRIMITIVES, RequestContext, execute_io,
css_class_collector as _css_class_collector_cv,
_svg_context as _svg_context_cv,
)
# Lazy imports to avoid circular dependency (html.py imports sx_ref.py)
_css_class_collector_cv = None
_svg_context_cv = None
def _ensure_html_imports():
global _css_class_collector_cv, _svg_context_cv
if _css_class_collector_cv is None:
from shared.sx.html import css_class_collector, _svg_context
_css_class_collector_cv = css_class_collector
_svg_context_cv = _svg_context
# When True, async_aser expands known components server-side
_expand_components_cv: contextvars.ContextVar[bool] = contextvars.ContextVar(
"_expand_components_ref", default=False
@@ -1102,18 +1108,22 @@ def expand_components_p():
def svg_context_p():
_ensure_html_imports()
return _svg_context_cv.get(False)
def svg_context_set(val):
_ensure_html_imports()
return _svg_context_cv.set(val)
def svg_context_reset(token):
_ensure_html_imports()
_svg_context_cv.reset(token)
def css_class_collect(val):
_ensure_html_imports()
collector = _css_class_collector_cv.get(None)
if collector is not None:
collector.update(str(val).split())
@@ -1131,6 +1141,25 @@ def is_sx_expr(x):
return isinstance(x, SxExpr)
# Predicate helpers used by adapter-async (these are in PRIMITIVES but
# the bootstrapped code calls them as plain functions)
def string_p(x):
return isinstance(x, str)
def list_p(x):
return isinstance(x, _b_list)
def number_p(x):
return isinstance(x, (int, float)) and not isinstance(x, bool)
def sx_parse(src):
from shared.sx.parser import parse_all
return parse_all(src)
def is_async_coroutine(x):
return inspect.iscoroutine(x)
@@ -1207,48 +1236,16 @@ async def async_eval_slot_to_sx(expr, env, ctx=None):
ctx = RequestContext()
token = _expand_components_cv.set(True)
try:
return await _eval_slot_inner(expr, env, ctx)
result = await async_eval_slot_inner(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(sx_serialize(result))
finally:
_expand_components_cv.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 async_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(sx_serialize(result))
result = await async_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(sx_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 shared.sx.parser import parse_all as _pa
parsed = _pa(raw)
if parsed:
return await async_eval_slot_to_sx(parsed[0], env, ctx)
return result
'''
# ---------------------------------------------------------------------------
@@ -1374,7 +1371,8 @@ aser_special = _aser_special_with_continuations
# Public API generator
# ---------------------------------------------------------------------------
def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str:
def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False,
has_async: bool = False) -> str:
lines = [
'',
'# =========================================================================',
@@ -1429,6 +1427,7 @@ def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str:
ADAPTER_FILES = {
"html": ("adapter-html.sx", "adapter-html"),
"sx": ("adapter-sx.sx", "adapter-sx"),
"async": ("adapter-async.sx", "adapter-async"),
}
SPEC_MODULES = {
@@ -1436,6 +1435,7 @@ SPEC_MODULES = {
"router": ("router.sx", "router (client-side route matching)"),
"engine": ("engine.sx", "engine (fetch/swap/trigger pure logic)"),
"signals": ("signals.sx", "signals (reactive signal runtime)"),
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
"types": ("types.sx", "types (gradual type system)"),
}

View File

@@ -39,7 +39,7 @@ def _get_z3_env() -> dict[str, Any]:
return _z3_env
from shared.sx.parser import parse_all
from shared.sx.evaluator import make_env, _eval, _trampoline
from shared.sx.ref.sx_ref import make_env, eval_expr as _eval, trampoline as _trampoline
env = make_env()
z3_path = os.path.join(os.path.dirname(__file__), "z3.sx")
@@ -60,7 +60,7 @@ def z3_translate(expr: Any) -> str:
Delegates to z3-translate defined in z3.sx.
"""
from shared.sx.evaluator import _trampoline, _call_lambda
from shared.sx.ref.sx_ref import trampoline as _trampoline, call_lambda as _call_lambda
env = _get_z3_env()
return _trampoline(_call_lambda(env["z3-translate"], [expr], env))
@@ -72,7 +72,7 @@ def z3_translate_file(source: str) -> str:
Delegates to z3-translate-file defined in z3.sx.
"""
from shared.sx.parser import parse_all
from shared.sx.evaluator import _trampoline, _call_lambda
from shared.sx.ref.sx_ref import trampoline as _trampoline, call_lambda as _call_lambda
env = _get_z3_env()
exprs = parse_all(source)

View File

@@ -134,12 +134,8 @@
;; (test body test body ...).
(define eval-cond
(fn (clauses env)
(if (and (not (empty? clauses))
(= (type-of (first clauses)) "list")
(= (len (first clauses)) 2))
;; Scheme-style
(if (cond-scheme? clauses)
(eval-cond-scheme clauses env)
;; Clojure-style
(eval-cond-clojure clauses env))))
(define eval-cond-scheme
@@ -178,7 +174,9 @@
;; bindings = ((name1 expr1) (name2 expr2) ...)
(define process-bindings
(fn (bindings env)
(let ((local (merge env)))
;; 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)))
(for-each
(fn (pair)
(when (and (= (type-of pair) "list") (>= (len pair) 2))

View File

@@ -49,7 +49,7 @@ def load_js_sx() -> dict:
exprs = parse_all(source)
from shared.sx.evaluator import evaluate, make_env
from shared.sx.ref.sx_ref import evaluate, make_env
env = make_env()
for expr in exprs:
@@ -74,7 +74,7 @@ def compile_ref_to_js(
spec_modules: List of spec modules (deps, router, signals). None = auto.
"""
from datetime import datetime, timezone
from shared.sx.evaluator import evaluate
from shared.sx.ref.sx_ref import evaluate
ref_dir = _HERE
env = load_js_sx()
@@ -103,8 +103,11 @@ def compile_ref_to_js(
if "boot" in adapter_set:
spec_mod_set.add("router")
spec_mod_set.add("deps")
if "page-helpers" in SPEC_MODULES:
spec_mod_set.add("page-helpers")
has_deps = "deps" in spec_mod_set
has_router = "router" in spec_mod_set
has_page_helpers = "page-helpers" in spec_mod_set
# Resolve extensions
ext_set = set()
@@ -198,12 +201,12 @@ def compile_ref_to_js(
if name in adapter_set and name in adapter_platform:
parts.append(adapter_platform[name])
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps))
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers))
if has_continuations:
parts.append(CONTINUATIONS_JS)
if has_dom:
parts.append(ASYNC_IO_JS)
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals))
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals, has_page_helpers))
parts.append(EPILOGUE)
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

View File

@@ -38,7 +38,7 @@ def load_py_sx(evaluator_env: dict) -> dict:
exprs = parse_all(source)
# Import the evaluator
from shared.sx.evaluator import evaluate, make_env
from shared.sx.ref.sx_ref import evaluate, make_env
env = make_env()
for expr in exprs:
@@ -60,7 +60,7 @@ def extract_defines(source: str) -> list[tuple[str, list]]:
def main():
from shared.sx.evaluator import evaluate
from shared.sx.ref.sx_ref import evaluate
# Load py.sx into evaluator
env = load_py_sx({})

File diff suppressed because it is too large Load Diff

272
shared/sx/ref/test-aser.sx Normal file
View File

@@ -0,0 +1,272 @@
;; ==========================================================================
;; test-aser.sx — Tests for the SX wire format (aser) adapter
;;
;; Requires: test-framework.sx loaded first.
;; Modules tested: adapter-sx.sx (aser, aser-call, aser-fragment, aser-special)
;;
;; Platform functions required (beyond test framework):
;; render-sx (sx-source) -> SX wire format string
;; Parses the sx-source string, evaluates via aser in a
;; fresh env, and returns the resulting SX wire format string.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Basic serialization
;; --------------------------------------------------------------------------
(defsuite "aser-basics"
(deftest "number literal passes through"
(assert-equal "42"
(render-sx "42")))
(deftest "string literal passes through"
;; aser returns the raw string value; render-sx concatenates it directly
(assert-equal "hello"
(render-sx "\"hello\"")))
(deftest "boolean true passes through"
(assert-equal "true"
(render-sx "true")))
(deftest "boolean false passes through"
(assert-equal "false"
(render-sx "false")))
(deftest "nil produces empty"
(assert-equal ""
(render-sx "nil"))))
;; --------------------------------------------------------------------------
;; HTML tag serialization
;; --------------------------------------------------------------------------
(defsuite "aser-tags"
(deftest "simple div"
(assert-equal "(div \"hello\")"
(render-sx "(div \"hello\")")))
(deftest "nested tags"
(assert-equal "(div (span \"hi\"))"
(render-sx "(div (span \"hi\"))")))
(deftest "multiple children"
(assert-equal "(div (p \"a\") (p \"b\"))"
(render-sx "(div (p \"a\") (p \"b\"))")))
(deftest "attributes serialize"
(assert-equal "(div :class \"foo\" \"bar\")"
(render-sx "(div :class \"foo\" \"bar\")")))
(deftest "multiple attributes"
(assert-equal "(a :href \"/home\" :class \"link\" \"Home\")"
(render-sx "(a :href \"/home\" :class \"link\" \"Home\")")))
(deftest "void elements"
(assert-equal "(br)"
(render-sx "(br)")))
(deftest "void element with attrs"
(assert-equal "(img :src \"pic.jpg\")"
(render-sx "(img :src \"pic.jpg\")"))))
;; --------------------------------------------------------------------------
;; Fragment serialization
;; --------------------------------------------------------------------------
(defsuite "aser-fragments"
(deftest "simple fragment"
(assert-equal "(<> (p \"a\") (p \"b\"))"
(render-sx "(<> (p \"a\") (p \"b\"))")))
(deftest "empty fragment"
(assert-equal ""
(render-sx "(<>)")))
(deftest "single-child fragment"
(assert-equal "(<> (div \"x\"))"
(render-sx "(<> (div \"x\"))"))))
;; --------------------------------------------------------------------------
;; Control flow in aser mode
;; --------------------------------------------------------------------------
(defsuite "aser-control-flow"
(deftest "if true branch"
(assert-equal "(p \"yes\")"
(render-sx "(if true (p \"yes\") (p \"no\"))")))
(deftest "if false branch"
(assert-equal "(p \"no\")"
(render-sx "(if false (p \"yes\") (p \"no\"))")))
(deftest "when true"
(assert-equal "(p \"ok\")"
(render-sx "(when true (p \"ok\"))")))
(deftest "when false"
(assert-equal ""
(render-sx "(when false (p \"ok\"))")))
(deftest "cond serializes matching branch"
(assert-equal "(p \"two\")"
(render-sx "(cond false (p \"one\") true (p \"two\") :else (p \"three\"))")))
(deftest "cond with 2-element predicate test"
;; Regression: cond misclassifies (nil? x) as scheme-style clause.
(assert-equal "(p \"yes\")"
(render-sx "(cond (nil? nil) (p \"yes\") :else (p \"no\"))"))
(assert-equal "(p \"no\")"
(render-sx "(cond (nil? \"x\") (p \"yes\") :else (p \"no\"))")))
(deftest "let binds then serializes"
(assert-equal "(p \"hello\")"
(render-sx "(let ((x \"hello\")) (p x))")))
(deftest "let preserves outer scope bindings"
;; Regression: process-bindings must preserve parent env scope chain.
;; Using merge() instead of env-extend loses parent scope items.
(assert-equal "(p \"outer\")"
(render-sx "(do (define theme \"outer\") (let ((x 1)) (p theme)))")))
(deftest "nested let preserves outer scope"
(assert-equal "(div (span \"hello\") (span \"world\"))"
(render-sx "(do (define a \"hello\")
(define b \"world\")
(div (let ((x 1)) (span a))
(let ((y 2)) (span b))))")))
(deftest "begin serializes last"
(assert-equal "(p \"last\")"
(render-sx "(begin (p \"first\") (p \"last\"))"))))
;; --------------------------------------------------------------------------
;; THE BUG — map/filter list flattening in children (critical regression)
;; --------------------------------------------------------------------------
(defsuite "aser-list-flattening"
(deftest "map inside tag flattens children"
(assert-equal "(div (span \"a\") (span \"b\") (span \"c\"))"
(render-sx "(do (define items (list \"a\" \"b\" \"c\"))
(div (map (fn (x) (span x)) items)))")))
(deftest "map inside tag with other children"
(assert-equal "(ul (li \"first\") (li \"a\") (li \"b\"))"
(render-sx "(do (define items (list \"a\" \"b\"))
(ul (li \"first\") (map (fn (x) (li x)) items)))")))
(deftest "filter result via let binding as children"
;; Note: (filter ...) is treated as an SVG tag in aser dispatch (SVG has <filter>),
;; so we evaluate filter via let binding + map to serialize children
(assert-equal "(ul (li \"a\") (li \"b\"))"
(render-sx "(do (define items (list \"a\" nil \"b\"))
(define kept (filter (fn (x) (not (nil? x))) items))
(ul (map (fn (x) (li x)) kept)))")))
(deftest "map inside fragment flattens"
(assert-equal "(<> (p \"a\") (p \"b\"))"
(render-sx "(do (define items (list \"a\" \"b\"))
(<> (map (fn (x) (p x)) items)))")))
(deftest "nested map does not double-wrap"
(assert-equal "(div (span \"1\") (span \"2\"))"
(render-sx "(do (define nums (list 1 2))
(div (map (fn (n) (span (str n))) nums)))")))
(deftest "map with component-like output flattens"
(assert-equal "(div (li \"x\") (li \"y\"))"
(render-sx "(do (define items (list \"x\" \"y\"))
(div (map (fn (x) (li x)) items)))"))))
;; --------------------------------------------------------------------------
;; Component serialization (NOT expanded in basic aser mode)
;; --------------------------------------------------------------------------
(defsuite "aser-components"
(deftest "unknown component serializes as-is"
(assert-equal "(~foo :title \"bar\")"
(render-sx "(~foo :title \"bar\")")))
(deftest "defcomp then unexpanded component call"
(assert-equal "(~card :title \"Hi\")"
(render-sx "(do (defcomp ~card (&key title) (h1 title)) (~card :title \"Hi\"))")))
(deftest "component with children serializes unexpanded"
(assert-equal "(~box (p \"inside\"))"
(render-sx "(do (defcomp ~box (&key &rest children) (div children))
(~box (p \"inside\")))"))))
;; --------------------------------------------------------------------------
;; Definition forms in aser mode
;; --------------------------------------------------------------------------
(defsuite "aser-definitions"
(deftest "define evaluates for side effects, returns nil"
(assert-equal "(p 42)"
(render-sx "(do (define x 42) (p x))")))
(deftest "defcomp evaluates and returns nil"
(assert-equal "(~tag :x 1)"
(render-sx "(do (defcomp ~tag (&key x) (span x)) (~tag :x 1))")))
(deftest "defisland evaluates AND serializes"
(let ((result (render-sx "(defisland ~counter (&key count) (span count))")))
(assert-true (string-contains? result "defisland")))))
;; --------------------------------------------------------------------------
;; Function calls in aser mode
;; --------------------------------------------------------------------------
(defsuite "aser-function-calls"
(deftest "named function call evaluates fully"
(assert-equal "3"
(render-sx "(do (define inc1 (fn (x) (+ x 1))) (inc1 2))")))
(deftest "define + call"
(assert-equal "10"
(render-sx "(do (define double (fn (x) (* x 2))) (double 5))")))
(deftest "native callable with multiple args"
;; Regression: async-aser-eval-call passed evaled-args list to
;; async-invoke (&rest), wrapping it in another list. apply(f, [list])
;; calls f(list) instead of f(*list).
(assert-equal "3"
(render-sx "(do (define my-add +) (my-add 1 2))")))
(deftest "native callable with two args via alias"
(assert-equal "hello world"
(render-sx "(do (define my-join str) (my-join \"hello\" \" world\"))")))
(deftest "higher-order: map returns list"
(let ((result (render-sx "(map (fn (x) (+ x 1)) (list 1 2 3))")))
;; map at top level returns a list, not serialized tags
(assert-true (not (nil? result))))))
;; --------------------------------------------------------------------------
;; and/or short-circuit in aser mode
;; --------------------------------------------------------------------------
(defsuite "aser-logic"
(deftest "and short-circuits on false"
(assert-equal "false"
(render-sx "(and true false true)")))
(deftest "and returns last truthy"
(assert-equal "3"
(render-sx "(and 1 2 3)")))
(deftest "or short-circuits on true"
(assert-equal "1"
(render-sx "(or 1 2 3)")))
(deftest "or returns last falsy"
(assert-equal "false"
(render-sx "(or false false)"))))

View File

@@ -277,6 +277,29 @@
false "b"
:else "c")))
(deftest "cond with 2-element predicate as first test"
;; Regression: cond misclassifies Clojure-style as scheme-style when
;; the first test is a 2-element list like (nil? x) or (empty? x).
;; The evaluator checks: is first arg a 2-element list? If yes, treats
;; as scheme-style ((test body) ...) — returning the arg instead of
;; evaluating the predicate call.
(assert-equal 0 (cond (nil? nil) 0 :else 1))
(assert-equal 1 (cond (nil? "x") 0 :else 1))
(assert-equal "empty" (cond (empty? (list)) "empty" :else "not-empty"))
(assert-equal "not-empty" (cond (empty? (list 1)) "empty" :else "not-empty"))
(assert-equal "yes" (cond (not false) "yes" :else "no"))
(assert-equal "no" (cond (not true) "yes" :else "no")))
(deftest "cond with 2-element predicate and no :else"
;; Same bug, but without :else — this is the worst case because the
;; bootstrapper heuristic also breaks (all clauses are 2-element lists).
(assert-equal "found"
(cond (nil? nil) "found"
(nil? "x") "other"))
(assert-equal "b"
(cond (nil? "x") "a"
(not false) "b")))
(deftest "and"
(assert-true (and true true))
(assert-false (and true false))

View File

@@ -149,7 +149,27 @@
(deftest "let in render context"
(assert-equal "<p>hello</p>"
(render-html "(let ((x \"hello\")) (p x))"))))
(render-html "(let ((x \"hello\")) (p x))")))
(deftest "cond with 2-element predicate test"
;; Regression: cond misclassifies (nil? x) as scheme-style clause.
(assert-equal "<p>yes</p>"
(render-html "(cond (nil? nil) (p \"yes\") :else (p \"no\"))"))
(assert-equal "<p>no</p>"
(render-html "(cond (nil? \"x\") (p \"yes\") :else (p \"no\"))")))
(deftest "let preserves outer scope bindings"
;; Regression: process-bindings must preserve parent env scope chain.
;; Using merge() on Env objects returns empty dict (Env is not dict subclass).
(assert-equal "<p>outer</p>"
(render-html "(do (define theme \"outer\") (let ((x 1)) (p theme)))")))
(deftest "nested let preserves outer scope"
(assert-equal "<div><span>hello</span><span>world</span></div>"
(render-html "(do (define a \"hello\")
(define b \"world\")
(div (let ((x 1)) (span a))
(let ((y 2)) (span b))))"))))
;; --------------------------------------------------------------------------
@@ -165,3 +185,46 @@
(let ((html (render-html "(do (defcomp ~box (&key &rest children) (div :class \"box\" children)) (~box (p \"inside\")))")))
(assert-true (string-contains? html "class=\"box\""))
(assert-true (string-contains? html "<p>inside</p>")))))
;; --------------------------------------------------------------------------
;; Map/filter producing multiple children (aser-adjacent regression tests)
;; --------------------------------------------------------------------------
(defsuite "render-map-children"
(deftest "map producing multiple children inside tag"
(assert-equal "<ul><li>a</li><li>b</li><li>c</li></ul>"
(render-html "(do (define items (list \"a\" \"b\" \"c\"))
(ul (map (fn (x) (li x)) items)))")))
(deftest "map with other siblings"
(assert-equal "<ul><li>first</li><li>a</li><li>b</li></ul>"
(render-html "(do (define items (list \"a\" \"b\"))
(ul (li \"first\") (map (fn (x) (li x)) items)))")))
(deftest "filter with nil results inside tag"
(assert-equal "<ul><li>a</li><li>c</li></ul>"
(render-html "(do (define items (list \"a\" nil \"c\"))
(ul (map (fn (x) (li x))
(filter (fn (x) (not (nil? x))) items))))")))
(deftest "nested map inside let"
(assert-equal "<div><span>1</span><span>2</span></div>"
(render-html "(let ((nums (list 1 2)))
(div (map (fn (n) (span n)) nums)))")))
(deftest "component with &rest receiving mapped results"
(let ((html (render-html "(do (defcomp ~list-box (&key &rest children) (div :class \"lb\" children))
(define items (list \"x\" \"y\"))
(~list-box (map (fn (x) (p x)) items)))")))
(assert-true (string-contains? html "class=\"lb\""))
(assert-true (string-contains? html "<p>x</p>"))
(assert-true (string-contains? html "<p>y</p>"))))
(deftest "map-indexed renders with index"
(assert-equal "<li>0: a</li><li>1: b</li>"
(render-html "(map-indexed (fn (i x) (li (str i \": \" x))) (list \"a\" \"b\"))")))
(deftest "for-each renders each item"
(assert-equal "<p>1</p><p>2</p>"
(render-html "(for-each (fn (x) (p x)) (list 1 2))"))))

View File

@@ -31,7 +31,7 @@ import asyncio
from typing import Any
from .types import Component, Keyword, Lambda, NIL, Symbol
from .evaluator import _eval as _raw_eval, _trampoline
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
def _eval(expr, env):
"""Evaluate and unwrap thunks — all resolver.py _eval calls are non-tail."""

View File

@@ -20,7 +20,7 @@ _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.evaluator import _eval, _trampoline, _call_lambda
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline, call_lambda as _call_lambda
from shared.sx.types import Symbol, Keyword, Lambda, NIL, Component, Island
# --- Test state ---
@@ -127,13 +127,38 @@ def render_html(sx_source):
except ImportError:
raise RuntimeError("render-to-html not available — sx_ref.py not built")
exprs = parse_all(sx_source)
render_env = dict(env)
# Use Env (not flat dict) so tests exercise the real scope chain path.
render_env = _Env(dict(env))
result = ""
for expr in exprs:
result += _render_to_html(expr, render_env)
return result
# --- Render SX (aser) platform function ---
def render_sx(sx_source):
"""Parse SX source and serialize to SX wire format via the bootstrapped evaluator."""
try:
from shared.sx.ref.sx_ref import aser as _aser, serialize as _serialize
except ImportError:
raise RuntimeError("aser not available — sx_ref.py not built")
exprs = parse_all(sx_source)
# Use Env (not flat dict) so tests exercise the real scope chain path.
# Using dict(env) hides bugs where merge() drops Env parent scopes.
render_env = _Env(dict(env))
result = ""
for expr in exprs:
val = _aser(expr, render_env)
if isinstance(val, str):
result += val
elif val is None or val is NIL:
pass
else:
result += _serialize(val)
return result
# --- Signal platform primitives ---
# Implements the signal runtime platform interface for testing signals.sx
@@ -258,6 +283,7 @@ SPECS = {
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
"router": {"file": "test-router.sx", "needs": []},
"render": {"file": "test-render.sx", "needs": ["render-html"]},
"aser": {"file": "test-aser.sx", "needs": ["render-sx"]},
"deps": {"file": "test-deps.sx", "needs": []},
"engine": {"file": "test-engine.sx", "needs": []},
"orchestration": {"file": "test-orchestration.sx", "needs": []},
@@ -297,8 +323,9 @@ env = _Env({
"make-keyword": make_keyword,
"symbol-name": symbol_name,
"keyword-name": keyword_name,
# Render platform function
# Render platform functions
"render-html": render_html,
"render-sx": render_sx,
# Extra primitives needed by spec modules (router.sx, deps.sx)
"for-each-indexed": "_deferred", # replaced below
"dict-set!": "_deferred",
@@ -848,9 +875,9 @@ def main():
print(f"# --- {spec_name} ---")
eval_file(spec["file"], env)
# Reset render state after render tests to avoid leaking
# Reset render state after render/aser tests to avoid leaking
# into subsequent specs (bootstrapped evaluator checks render_active)
if spec_name == "render":
if spec_name in ("render", "aser"):
try:
from shared.sx.ref.sx_ref import set_render_active_b
set_render_active_b(False)

View File

@@ -21,7 +21,7 @@ class TestJsSxTranslation:
def _translate(self, sx_source: str) -> str:
"""Translate a single SX expression to JS using js.sx."""
from shared.sx.evaluator import evaluate
from shared.sx.ref.sx_ref import evaluate
env = load_js_sx()
expr = parse(sx_source)
env["_def_expr"] = expr

View File

@@ -18,7 +18,7 @@ from shared.sx.deps import (
def make_env(*sx_sources: str) -> dict:
"""Parse and evaluate component definitions into an env dict."""
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env: dict = {}
for source in sx_sources:
exprs = parse_all(source)

View File

@@ -23,7 +23,7 @@ from shared.sx.deps import (
def make_env(*sx_sources: str) -> dict:
"""Parse and evaluate component definitions into an env dict."""
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env: dict = {}
for source in sx_sources:
exprs = parse_all(source)

View File

@@ -20,7 +20,7 @@ from shared.sx.deps import (
def make_env(*sx_sources: str) -> dict:
"""Parse and evaluate component definitions into an env dict."""
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env: dict = {}
for source in sx_sources:
exprs = parse_all(source)
@@ -282,7 +282,7 @@ class TestIoRoutingLogic:
"""
def _eval(self, src, env):
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
result = None
for expr in parse_all(src):
result = _trampoline(_eval(expr, env))

View File

@@ -156,7 +156,7 @@ class TestDataPageDeps:
def test_deps_computed_for_data_page(self):
from shared.sx.deps import components_needed
from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
# Define a component
env = {}
@@ -172,7 +172,7 @@ class TestDataPageDeps:
def test_deps_transitive_for_data_page(self):
from shared.sx.deps import components_needed
from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env = {}
source = """
@@ -205,7 +205,7 @@ class TestDataPipelineSimulation:
def test_full_pipeline(self):
from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
# 1. Define a component that uses only pure primitives
env = {}
@@ -236,7 +236,7 @@ class TestDataPipelineSimulation:
def test_pipeline_with_list_data(self):
from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env = {}
for expr in pa('''
@@ -262,7 +262,7 @@ class TestDataPipelineSimulation:
def test_pipeline_data_isolation(self):
"""Different data for the same content produces different results."""
from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env = {}
for expr in pa('(defcomp ~page (&key title count) (str title ": " count))'):
@@ -298,7 +298,7 @@ class TestDataCache:
def _make_env(self, current_time_ms=1000):
"""Create an env with cache functions and a controllable now-ms."""
from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
env = {}
# Mock now-ms as a callable that returns current_time_ms
@@ -344,7 +344,7 @@ class TestDataCache:
def _eval(self, src, env):
from shared.sx.parser import parse_all as pa
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
result = None
for expr in pa(src):
result = _trampoline(_eval(expr, env))

View File

@@ -18,7 +18,7 @@ from shared.sx.types import Symbol, Keyword, Lambda, Component, Macro, NIL
def hw_eval(text, env=None):
"""Evaluate via hand-written evaluator.py."""
from shared.sx.evaluator import evaluate as _evaluate, EvalError
from shared.sx.ref.sx_ref import evaluate as _evaluate
if env is None:
env = {}
return _evaluate(parse(text), env)
@@ -50,7 +50,7 @@ def ref_render(text, env=None):
def hw_eval_multi(text, env=None):
"""Evaluate multiple expressions (e.g. defines then call)."""
from shared.sx.evaluator import evaluate as _evaluate
from shared.sx.ref.sx_ref import evaluate as _evaluate
if env is None:
env = {}
result = None
@@ -736,7 +736,7 @@ class TestParityDeps:
class TestParityErrors:
def test_undefined_symbol(self):
from shared.sx.evaluator import EvalError as HwError
from shared.sx.types import EvalError as HwError
from shared.sx.ref.sx_ref import EvalError as RefError
with pytest.raises(HwError):
hw_eval("undefined_var")

View File

@@ -12,7 +12,7 @@ import pytest
from shared.sx.parser import parse, parse_all
from shared.sx.html import render as py_render
from shared.sx.evaluator import evaluate
from shared.sx.ref.sx_ref import evaluate
SX_JS = Path(__file__).resolve().parents[2] / "static" / "scripts" / "sx.js"
SX_TEST_JS = Path(__file__).resolve().parents[2] / "static" / "scripts" / "sx-test.js"

View File

@@ -7,7 +7,7 @@ from __future__ import annotations
import pytest
from shared.sx.parser import parse_all
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
_PREAMBLE = '''(define assert-equal (fn (expected actual) (assert (equal? expected actual) (str "Expected " (str expected) " but got " (str actual)))))

View File

@@ -376,6 +376,15 @@ class _ShiftSignal(BaseException):
self.env = env
# ---------------------------------------------------------------------------
# EvalError
# ---------------------------------------------------------------------------
class EvalError(Exception):
"""Error during expression evaluation."""
pass
# ---------------------------------------------------------------------------
# Type alias
# ---------------------------------------------------------------------------

View File

@@ -104,3 +104,8 @@
:params ()
:returns "dict"
:service "sx")
(define-page-helper "page-helpers-demo-data"
:params ()
:returns "dict"
:service "sx")

View File

@@ -227,7 +227,8 @@
(dict :label "JavaScript" :href "/bootstrappers/javascript")
(dict :label "Python" :href "/bootstrappers/python")
(dict :label "Self-Hosting (py.sx)" :href "/bootstrappers/self-hosting")
(dict :label "Self-Hosting JS (js.sx)" :href "/bootstrappers/self-hosting-js")))
(dict :label "Self-Hosting JS (js.sx)" :href "/bootstrappers/self-hosting-js")
(dict :label "Page Helpers" :href "/bootstrappers/page-helpers")))
;; Spec file registry — canonical metadata for spec viewer pages.
;; Python only handles file I/O (read-spec-file); all metadata lives here.

264
sx/sx/page-helpers-demo.sx Normal file
View File

@@ -0,0 +1,264 @@
;; page-helpers-demo.sx — Demo: same SX spec functions on server and client
;;
;; Shows page-helpers.sx functions running on Python (server-side, via sx_ref.py)
;; and JavaScript (client-side, via sx-browser.js) with identical results.
;; Server renders with render-to-html. Client runs as a defisland — pure SX,
;; no JavaScript file. The button click triggers spec functions via signals.
;; ---------------------------------------------------------------------------
;; Shared card component — used by both server and client results
;; ---------------------------------------------------------------------------
(defcomp ~demo-result-card (&key title ms desc theme &rest children)
(let ((border (if (= theme "blue") "border-blue-200 bg-blue-50/30" "border-stone-200"))
(title-c (if (= theme "blue") "text-blue-700" "text-stone-700"))
(badge-c (if (= theme "blue") "text-blue-400" "text-stone-400"))
(desc-c (if (= theme "blue") "text-blue-500" "text-stone-500"))
(body-c (if (= theme "blue") "text-blue-600" "text-stone-600")))
(div :class (str "rounded-lg border p-4 " border)
(h4 :class (str "font-semibold text-sm mb-1 " title-c)
title " "
(span :class (str "text-xs " badge-c) (str ms "ms")))
(p :class (str "text-xs mb-2 " desc-c) desc)
(div :class (str "text-xs space-y-0.5 " body-c)
children))))
;; ---------------------------------------------------------------------------
;; Client-side island — runs spec functions in the browser on button click
;; ---------------------------------------------------------------------------
(defisland ~demo-client-runner (&key sf-source attr-detail req-attrs attr-keys)
(let ((results (signal nil))
(running (signal false))
(run-demo (fn (e)
(reset! running true)
(let* ((t0 (now-ms))
;; 1. categorize-special-forms
(t1 (now-ms))
(sf-exprs (sx-parse sf-source))
(sf-result (categorize-special-forms sf-exprs))
(sf-ms (- (now-ms) t1))
(sf-cats {})
(sf-total 0)
;; 2. build-reference-data
(t2 (now-ms))
(ref-result (build-reference-data "attributes"
{"req-attrs" req-attrs "beh-attrs" (list) "uniq-attrs" (list)}
attr-keys))
(ref-ms (- (now-ms) t2))
(ref-sample (slice (or (get ref-result "req-attrs") (list)) 0 3))
;; 3. build-attr-detail
(t3 (now-ms))
(attr-result (build-attr-detail "sx-get" attr-detail))
(attr-ms (- (now-ms) t3))
;; 4. build-component-source
(t4 (now-ms))
(comp-result (build-component-source
{"type" "component" "name" "~demo-card"
"params" (list "title" "subtitle")
"has-children" true
"body-sx" "(div :class \"card\"\n (h2 title)\n (when subtitle (p subtitle))\n children)"
"affinity" "auto"}))
(comp-ms (- (now-ms) t4))
;; 5. build-routing-analysis
(t5 (now-ms))
(routing-result (build-routing-analysis (list
{"name" "home" "path" "/" "has-data" false "content-src" "(~home-content)"}
{"name" "dashboard" "path" "/dash" "has-data" true "content-src" "(~dashboard)"}
{"name" "about" "path" "/about" "has-data" false "content-src" "(~about-content)"}
{"name" "settings" "path" "/settings" "has-data" true "content-src" "(~settings)"})))
(routing-ms (- (now-ms) t5))
(total-ms (- (now-ms) t0)))
;; Post-process sf-result: count forms per category
(for-each (fn (k)
(let ((count (len (get sf-result k))))
(set! sf-cats (assoc sf-cats k count))
(set! sf-total (+ sf-total count))))
(keys sf-result))
(reset! results
{"sf-cats" sf-cats "sf-total" sf-total "sf-ms" sf-ms
"ref-sample" ref-sample "ref-ms" ref-ms
"attr-result" attr-result "attr-ms" attr-ms
"comp-result" comp-result "comp-ms" comp-ms
"routing-result" routing-result "routing-ms" routing-ms
"total-ms" total-ms})))))
(<>
(button
:class (if (deref running)
"px-4 py-2 rounded-md bg-blue-600 text-white font-medium text-sm cursor-default mb-4"
"px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 transition-colors mb-4")
:on-click run-demo
(if (deref running)
(str "Done (" (get (deref results) "total-ms") "ms total)")
"Run in Browser"))
(when (deref results)
(let ((r (deref results)))
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(~demo-result-card
:title "categorize-special-forms"
:ms (get r "sf-ms") :theme "blue"
:desc "Parses special-forms.sx and classifies each form by category (control flow, binding, quoting, etc)."
(p :class "text-sm mb-1"
(str (get r "sf-total") " forms in "
(len (keys (get r "sf-cats"))) " categories"))
(map (fn (k)
(div (str k ": " (get (get r "sf-cats") k))))
(keys (get r "sf-cats"))))
(~demo-result-card
:title "build-reference-data"
:ms (get r "ref-ms") :theme "blue"
:desc "Takes raw attribute tuples and generates reference table rows with documentation hrefs."
(p :class "text-sm mb-1"
(str (len (get r "ref-sample")) " attributes with detail page links"))
(map (fn (item)
(div (str (get item "name") " → "
(or (get item "href") "no detail page"))))
(get r "ref-sample")))
(~demo-result-card
:title "build-attr-detail"
:ms (get r "attr-ms") :theme "blue"
:desc "Builds a detail page data structure for a single attribute (sx-get): title, wire ID, handler status."
(div (str "title: " (get (get r "attr-result") "attr-title")))
(div (str "wire-id: " (or (get (get r "attr-result") "attr-wire-id") "none")))
(div (str "has handler: " (if (get (get r "attr-result") "attr-handler") "yes" "no"))))
(~demo-result-card
:title "build-component-source"
:ms (get r "comp-ms") :theme "blue"
:desc "Reconstructs a defcomp source definition from a component metadata dict (name, params, body)."
(pre :class "bg-blue-50 p-2 rounded overflow-x-auto"
(get r "comp-result")))
(div :class "rounded-lg border border-blue-200 bg-blue-50/30 p-4 md:col-span-2"
(h4 :class "font-semibold text-blue-700 text-sm mb-1"
"build-routing-analysis "
(span :class "text-xs text-blue-400" (str (get r "routing-ms") "ms")))
(p :class "text-xs text-blue-500 mb-2"
"Classifies pages as client-routable or server-only based on whether they have data dependencies.")
(div :class "text-xs text-blue-600"
(p :class "text-sm mb-1"
(str (get (get r "routing-result") "total-pages") " pages: "
(get (get r "routing-result") "client-count") " client-routable, "
(get (get r "routing-result") "server-count") " server-only"))
(div :class "space-y-0.5"
(map (fn (pg)
(div (str (get pg "name") " → " (get pg "mode")
(when (not (empty? (get pg "reason")))
(str " (" (get pg "reason") ")")))))
(get (get r "routing-result") "pages")))))))))))
;; ---------------------------------------------------------------------------
;; Main page component — server-rendered content + client island
;; ---------------------------------------------------------------------------
(defcomp ~page-helpers-demo-content (&key
sf-categories sf-total sf-ms
ref-sample ref-ms
attr-result attr-ms
comp-source comp-ms
routing-result routing-ms
server-total-ms
sf-source
attr-detail req-attrs attr-keys)
(div :class "max-w-3xl mx-auto px-4"
(div :class "mb-8"
(h2 :class "text-2xl font-bold text-stone-800 mb-2" "Bootstrapped Page Helpers")
(p :class "text-stone-600 mb-4"
"These functions are defined once in "
(code :class "text-violet-700" "page-helpers.sx")
" and bootstrapped to both Python ("
(code :class "text-violet-700" "sx_ref.py")
") and JavaScript ("
(code :class "text-violet-700" "sx-browser.js")
"). The server ran them in Python during this page load. Click the button below to run the identical functions client-side in the browser — same spec, same inputs, same results."))
;; Server results
(div :class "mb-8"
(h3 :class "text-lg font-semibold text-stone-700 mb-3"
"Server Results "
(span :class "text-sm font-normal text-stone-500"
(str "(Python, " server-total-ms "ms total)")))
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
(~demo-result-card
:title "categorize-special-forms"
:ms sf-ms :theme "stone"
:desc "Parses special-forms.sx and classifies each form by category (control flow, binding, quoting, etc)."
(p :class "text-sm mb-1"
(str sf-total " forms in "
(len (keys sf-categories)) " categories"))
(map (fn (k)
(div (str k ": " (get sf-categories k))))
(keys sf-categories)))
(~demo-result-card
:title "build-reference-data"
:ms ref-ms :theme "stone"
:desc "Takes raw attribute tuples and generates reference table rows with documentation hrefs."
(p :class "text-sm mb-1"
(str (len ref-sample) " attributes with detail page links"))
(map (fn (item)
(div (str (get item "name") " → "
(or (get item "href") "no detail page"))))
ref-sample))
(~demo-result-card
:title "build-attr-detail"
:ms attr-ms :theme "stone"
:desc "Builds a detail page data structure for a single attribute (sx-get): title, wire ID, handler status."
(div (str "title: " (get attr-result "attr-title")))
(div (str "wire-id: " (or (get attr-result "attr-wire-id") "none")))
(div (str "has handler: " (if (get attr-result "attr-handler") "yes" "no"))))
(~demo-result-card
:title "build-component-source"
:ms comp-ms :theme "stone"
:desc "Reconstructs a defcomp source definition from a component metadata dict (name, params, body)."
(pre :class "bg-stone-50 p-2 rounded overflow-x-auto"
comp-source))
(div :class "rounded-lg border border-stone-200 p-4 md:col-span-2"
(h4 :class "font-semibold text-stone-700 text-sm mb-1"
"build-routing-analysis "
(span :class "text-xs text-stone-400" (str routing-ms "ms")))
(p :class "text-xs text-stone-500 mb-2"
"Classifies pages as client-routable or server-only based on whether they have data dependencies.")
(div :class "text-xs text-stone-600"
(p :class "text-sm mb-1"
(str (get routing-result "total-pages") " pages: "
(get routing-result "client-count") " client-routable, "
(get routing-result "server-count") " server-only"))
(div :class "space-y-0.5"
(map (fn (pg)
(div (str (get pg "name") " → " (get pg "mode")
(when (not (empty? (get pg "reason")))
(str " (" (get pg "reason") ")")))))
(get routing-result "pages")))))))
;; Client execution area — pure SX island, no JavaScript file
(div :class "mb-8"
(h3 :class "text-lg font-semibold text-stone-700 mb-3"
"Client Results "
(span :class "text-sm font-normal text-stone-500" "(JavaScript, sx-browser.js)"))
(~demo-client-runner
:sf-source sf-source
:attr-detail attr-detail
:req-attrs req-attrs
:attr-keys attr-keys))))

View File

@@ -553,6 +553,28 @@
"phase2" (~reactive-islands-phase2-content)
:else (~reactive-islands-index-content))))
;; ---------------------------------------------------------------------------
;; Bootstrapped page helpers demo
;; ---------------------------------------------------------------------------
(defpage page-helpers-demo
:path "/bootstrappers/page-helpers"
:auth :public
:layout :sx-docs
:data (page-helpers-demo-data)
:content (~sx-doc :path "/bootstrappers/page-helpers"
(~page-helpers-demo-content
:sf-categories sf-categories :sf-total sf-total :sf-ms sf-ms
:ref-sample ref-sample :ref-ms ref-ms
:attr-result attr-result :attr-ms attr-ms
:comp-source comp-source :comp-ms comp-ms
:routing-result routing-result :routing-ms routing-ms
:server-total-ms server-total-ms
:sf-source sf-source
:attr-detail attr-detail
:req-attrs req-attrs
:attr-keys attr-keys)))
;; ---------------------------------------------------------------------------
;; Testing section
;; ---------------------------------------------------------------------------

View File

@@ -33,6 +33,7 @@ def _register_sx_helpers() -> None:
"action:add-demo-item": _add_demo_item,
"offline-demo-data": _offline_demo_data,
"prove-data": _prove_data,
"page-helpers-demo-data": _page_helpers_demo_data,
})
@@ -41,26 +42,29 @@ def _component_source(name: str) -> str:
from shared.sx.jinja_bridge import get_component_env
from shared.sx.parser import serialize
from shared.sx.types import Component, Island
from shared.sx.ref.sx_ref import build_component_source
comp = get_component_env().get(name)
if isinstance(comp, Island):
param_strs = (["&key"] + list(comp.params)) if comp.params else []
if comp.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(comp.body, pretty=True)
return f"(defisland {name} {params_sx}\n {body_sx})"
return build_component_source({
"type": "island", "name": name,
"params": list(comp.params) if comp.params else [],
"has-children": comp.has_children,
"body-sx": serialize(comp.body, pretty=True),
"affinity": None,
})
if not isinstance(comp, Component):
return f";; component {name} not found"
param_strs = ["&key"] + list(comp.params)
if comp.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(comp.body, pretty=True)
affinity = ""
if comp.render_target == "server":
affinity = " :affinity :server"
return f"(defcomp {name} {params_sx}{affinity}\n {body_sx})"
return build_component_source({
"type": "not-found", "name": name,
"params": [], "has-children": False, "body-sx": "", "affinity": None,
})
return build_component_source({
"type": "component", "name": name,
"params": list(comp.params),
"has-children": comp.has_children,
"body-sx": serialize(comp.body, pretty=True),
"affinity": comp.affinity,
})
def _primitives_data() -> dict:
@@ -70,168 +74,57 @@ def _primitives_data() -> dict:
def _special_forms_data() -> dict:
"""Parse special-forms.sx and return categorized form data.
Returns a dict of category → list of form dicts, each with:
name, syntax, doc, tail_position, example
"""
"""Parse special-forms.sx and return categorized form data."""
import os
from shared.sx.parser import parse_all, serialize
from shared.sx.types import Symbol, Keyword
from shared.sx.parser import parse_all
from shared.sx.ref.sx_ref import categorize_special_forms
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
if not os.path.isdir(ref_dir):
ref_dir = "/app/shared/sx/ref"
ref_dir = _ref_dir()
spec_path = os.path.join(ref_dir, "special-forms.sx")
with open(spec_path) as f:
exprs = parse_all(f.read())
# Categories inferred from comment sections in the file.
# We assign forms to categories based on their order in the spec.
categories: dict[str, list[dict]] = {}
current_category = "Other"
# Map form names to categories
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",
}
for expr in exprs:
if not isinstance(expr, list) or len(expr) < 2:
continue
head = expr[0]
if not isinstance(head, Symbol) or head.name != "define-special-form":
continue
name = expr[1]
# Extract keyword args
kwargs: dict[str, str] = {}
i = 2
while i < len(expr) - 1:
if isinstance(expr[i], Keyword):
key = expr[i].name
val = expr[i + 1]
if isinstance(val, list):
# For :syntax, avoid quote sugar (quasiquote → `x)
items = [serialize(item) for item in val]
kwargs[key] = "(" + " ".join(items) + ")"
else:
kwargs[key] = str(val)
i += 2
else:
i += 1
category = category_map.get(name, "Other")
if category not in categories:
categories[category] = []
categories[category].append({
"name": name,
"syntax": kwargs.get("syntax", ""),
"doc": kwargs.get("doc", ""),
"tail-position": kwargs.get("tail-position", ""),
"example": kwargs.get("example", ""),
})
return categories
return categorize_special_forms(exprs)
def _reference_data(slug: str) -> dict:
"""Return reference table data for a given slug.
Returns a dict whose keys become SX env bindings:
- attributes: req-attrs, beh-attrs, uniq-attrs
- headers: req-headers, resp-headers
- events: events-list
- js-api: js-api-list
"""
"""Return reference table data for a given slug."""
from content.pages import (
REQUEST_ATTRS, BEHAVIOR_ATTRS, SX_UNIQUE_ATTRS,
REQUEST_HEADERS, RESPONSE_HEADERS,
EVENTS, JS_API, ATTR_DETAILS, HEADER_DETAILS,
)
from shared.sx.ref.sx_ref import build_reference_data
# Build raw data dict and detail keys based on slug
if slug == "attributes":
return {
"req-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in REQUEST_ATTRS
],
"beh-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in BEHAVIOR_ATTRS
],
"uniq-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in SX_UNIQUE_ATTRS
],
raw = {
"req-attrs": [list(t) for t in REQUEST_ATTRS],
"beh-attrs": [list(t) for t in BEHAVIOR_ATTRS],
"uniq-attrs": [list(t) for t in SX_UNIQUE_ATTRS],
}
detail_keys = list(ATTR_DETAILS.keys())
elif slug == "headers":
return {
"req-headers": [
{"name": n, "value": v, "desc": d,
"href": f"/hypermedia/reference/headers/{n}" if n in HEADER_DETAILS else None}
for n, v, d in REQUEST_HEADERS
],
"resp-headers": [
{"name": n, "value": v, "desc": d,
"href": f"/hypermedia/reference/headers/{n}" if n in HEADER_DETAILS else None}
for n, v, d in RESPONSE_HEADERS
],
raw = {
"req-headers": [list(t) for t in REQUEST_HEADERS],
"resp-headers": [list(t) for t in RESPONSE_HEADERS],
}
detail_keys = list(HEADER_DETAILS.keys())
elif slug == "events":
from content.pages import EVENT_DETAILS
return {
"events-list": [
{"name": n, "desc": d,
"href": f"/hypermedia/reference/events/{n}" if n in EVENT_DETAILS else None}
for n, d in EVENTS
],
}
raw = {"events-list": [list(t) for t in EVENTS]}
detail_keys = list(EVENT_DETAILS.keys())
elif slug == "js-api":
return {
"js-api-list": [
{"name": n, "desc": d}
for n, d in JS_API
],
}
# Default — return attrs data for fallback
return {
"req-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in REQUEST_ATTRS
],
"beh-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in BEHAVIOR_ATTRS
],
"uniq-attrs": [
{"name": a, "desc": d, "exists": e,
"href": f"/hypermedia/reference/attributes/{a}" if e and a in ATTR_DETAILS else None}
for a, d, e in SX_UNIQUE_ATTRS
],
raw = {"js-api-list": [list(t) for t in JS_API]}
detail_keys = []
else:
raw = {
"req-attrs": [list(t) for t in REQUEST_ATTRS],
"beh-attrs": [list(t) for t in BEHAVIOR_ATTRS],
"uniq-attrs": [list(t) for t in SX_UNIQUE_ATTRS],
}
detail_keys = list(ATTR_DETAILS.keys())
return build_reference_data(slug, raw, detail_keys)
def _read_spec_file(filename: str) -> str:
@@ -314,7 +207,7 @@ def _self_hosting_data(ref_dir: str) -> dict:
import os
from shared.sx.parser import parse_all
from shared.sx.types import Symbol
from shared.sx.evaluator import evaluate, make_env
from shared.sx.ref.sx_ref import evaluate, make_env
from shared.sx.ref.bootstrap_py import extract_defines, compile_ref_to_py, PyEmitter
try:
@@ -387,7 +280,7 @@ def _js_self_hosting_data(ref_dir: str) -> dict:
"""Run js.sx live: load into evaluator, translate all spec defines."""
import os
from shared.sx.types import Symbol
from shared.sx.evaluator import evaluate
from shared.sx.ref.sx_ref import evaluate
from shared.sx.ref.run_js_sx import load_js_sx
from shared.sx.ref.platform_js import extract_defines
@@ -425,6 +318,7 @@ def _js_self_hosting_data(ref_dir: str) -> dict:
return {
"bootstrapper-not-found": None,
"js-sx-source": js_sx_source,
"defines-matched": str(total),
"defines-total": str(total),
"js-sx-lines": str(len(js_sx_source.splitlines())),
"verification-status": status,
@@ -438,6 +332,7 @@ def _bundle_analyzer_data() -> dict:
from shared.sx.deps import components_needed, scan_components_from_sx
from shared.sx.parser import serialize
from shared.sx.types import Component, Macro
from shared.sx.ref.sx_ref import build_bundle_analysis
env = get_component_env()
total_components = sum(1 for v in env.values() if isinstance(v, Component))
@@ -445,68 +340,47 @@ def _bundle_analyzer_data() -> dict:
pure_count = sum(1 for v in env.values() if isinstance(v, Component) and v.is_pure)
io_count = total_components - pure_count
pages_data = []
# Extract raw data at I/O edge — Python accesses Component objects, serializes bodies
pages_raw = []
components_raw: dict[str, dict] = {}
for name, page_def in sorted(get_all_pages("sx").items()):
content_sx = serialize(page_def.content_expr)
direct = scan_components_from_sx(content_sx)
needed = components_needed(content_sx, env)
n = len(needed)
pct = round(n / total_components * 100) if total_components else 0
savings = 100 - pct
needed = sorted(components_needed(content_sx, env))
# IO classification + component details for this page
pure_in_page = 0
io_in_page = 0
page_io_refs: set[str] = set()
comp_details = []
for comp_name in sorted(needed):
for comp_name in needed:
if comp_name not in components_raw:
val = env.get(comp_name)
if isinstance(val, Component):
is_pure = val.is_pure
if is_pure:
pure_in_page += 1
else:
io_in_page += 1
page_io_refs.update(val.io_refs)
# Reconstruct defcomp source
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(val.body, pretty=True)
source = f"(defcomp ~{val.name} {params_sx}\n {body_sx})"
comp_details.append({
"name": comp_name,
"is-pure": is_pure,
components_raw[comp_name] = {
"is-pure": val.is_pure,
"affinity": val.affinity,
"render-target": val.render_target,
"io-refs": sorted(val.io_refs),
"deps": sorted(val.deps),
"source": source,
})
"source": f"(defcomp ~{val.name} {params_sx}\n {body_sx})",
}
pages_data.append({
pages_raw.append({
"name": name,
"path": page_def.path,
"direct": len(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,
"needed-names": needed,
})
pages_data.sort(key=lambda p: p["needed"], reverse=True)
return {
"pages": pages_data,
"total-components": total_components,
"total-macros": total_macros,
"pure-count": pure_count,
"io-count": io_count,
}
# Pure data transformation in SX spec
result = build_bundle_analysis(
pages_raw, components_raw,
total_components, total_macros, pure_count, io_count,
)
# Sort pages by needed count (descending) — SX has no sort primitive
result["pages"] = sorted(result["pages"], key=lambda p: p["needed"], reverse=True)
return result
def _routing_analyzer_data() -> dict:
@@ -514,12 +388,11 @@ def _routing_analyzer_data() -> dict:
from shared.sx.pages import get_all_pages
from shared.sx.parser import serialize as sx_serialize
from shared.sx.helpers import _sx_literal
from shared.sx.ref.sx_ref import build_routing_analysis
pages_data = []
full_content: list[tuple[str, str, bool]] = [] # (name, full_content, has_data)
client_count = 0
server_count = 0
# I/O edge: extract page data from page registry
pages_raw = []
full_content: list[tuple[str, str, bool]] = []
for name, page_def in sorted(get_all_pages("sx").items()):
has_data = page_def.data_expr is not None
content_src = ""
@@ -528,37 +401,21 @@ def _routing_analyzer_data() -> dict:
content_src = sx_serialize(page_def.content_expr)
except Exception:
pass
pages_raw.append({
"name": name, "path": page_def.path,
"has-data": has_data, "content-src": content_src,
})
full_content.append((name, content_src, has_data))
# Determine routing mode and reason
if has_data:
mode = "server"
reason = "Has :data expression — needs server IO"
server_count += 1
elif not content_src:
mode = "server"
reason = "No content expression"
server_count += 1
else:
mode = "client"
reason = ""
client_count += 1
# Pure classification in SX spec
result = build_routing_analysis(pages_raw)
# Sort: client pages first, then server (SX has no sort primitive)
result["pages"] = sorted(
result["pages"],
key=lambda p: (0 if p["mode"] == "client" else 1, p["name"]),
)
pages_data.append({
"name": name,
"path": page_def.path,
"mode": mode,
"has-data": has_data,
"content-expr": content_src[:80] + ("..." if len(content_src) > 80 else ""),
"reason": reason,
})
# Sort: client pages first, then server
pages_data.sort(key=lambda p: (0 if p["mode"] == "client" else 1, p["name"]))
# Build a sample of the SX page registry format (use full content, first 3)
total = client_count + server_count
# Build registry sample (uses _sx_literal which is Python string escaping)
sample_entries = []
sorted_full = sorted(full_content, key=lambda x: x[0])
for name, csrc, hd in sorted_full[:3]:
@@ -574,86 +431,50 @@ def _routing_analyzer_data() -> dict:
+ "\n :closure {}}"
)
sample_entries.append(entry)
registry_sample = "\n\n".join(sample_entries)
result["registry-sample"] = "\n\n".join(sample_entries)
return {
"pages": pages_data,
"total-pages": total,
"client-count": client_count,
"server-count": server_count,
"registry-sample": registry_sample,
}
return result
def _attr_detail_data(slug: str) -> dict:
"""Return attribute detail data for a specific attribute slug.
Returns a dict whose keys become SX env bindings:
- attr-title, attr-description, attr-example, attr-handler
- attr-demo (component call or None)
- attr-wire-id (wire placeholder id or None)
- attr-not-found (truthy if not found)
"""
"""Return attribute detail data for a specific attribute slug."""
from content.pages import ATTR_DETAILS
from shared.sx.helpers import sx_call
from shared.sx.ref.sx_ref import build_attr_detail
detail = ATTR_DETAILS.get(slug)
if not detail:
return {"attr-not-found": True}
demo_name = detail.get("demo")
wire_id = None
if "handler" in detail:
wire_id = f"ref-wire-{slug.replace(':', '-').replace('*', 'star')}"
return {
"attr-not-found": None,
"attr-title": slug,
"attr-description": detail["description"],
"attr-example": detail["example"],
"attr-handler": detail.get("handler"),
"attr-demo": sx_call(demo_name) if demo_name else None,
"attr-wire-id": wire_id,
}
result = build_attr_detail(slug, detail)
# Convert demo name to sx_call if present
demo_name = result.get("attr-demo")
if demo_name:
result["attr-demo"] = sx_call(demo_name)
return result
def _header_detail_data(slug: str) -> dict:
"""Return header detail data for a specific header slug."""
from content.pages import HEADER_DETAILS
from shared.sx.helpers import sx_call
from shared.sx.ref.sx_ref import build_header_detail
detail = HEADER_DETAILS.get(slug)
if not detail:
return {"header-not-found": True}
demo_name = detail.get("demo")
return {
"header-not-found": None,
"header-title": slug,
"header-direction": detail["direction"],
"header-description": detail["description"],
"header-example": detail.get("example"),
"header-demo": sx_call(demo_name) if demo_name else None,
}
result = build_header_detail(slug, HEADER_DETAILS.get(slug))
demo_name = result.get("header-demo")
if demo_name:
result["header-demo"] = sx_call(demo_name)
return result
def _event_detail_data(slug: str) -> dict:
"""Return event detail data for a specific event slug."""
from content.pages import EVENT_DETAILS
from shared.sx.helpers import sx_call
from shared.sx.ref.sx_ref import build_event_detail
detail = EVENT_DETAILS.get(slug)
if not detail:
return {"event-not-found": True}
demo_name = detail.get("demo")
return {
"event-not-found": None,
"event-title": slug,
"event-description": detail["description"],
"event-example": detail.get("example"),
"event-demo": sx_call(demo_name) if demo_name else None,
}
result = build_event_detail(slug, EVENT_DETAILS.get(slug))
demo_name = result.get("event-demo")
if demo_name:
result["event-demo"] = sx_call(demo_name)
return result
def _run_spec_tests() -> dict:
@@ -661,7 +482,7 @@ def _run_spec_tests() -> dict:
import os
import time
from shared.sx.parser import parse_all
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
if not os.path.isdir(ref_dir):
@@ -735,7 +556,7 @@ def _run_modular_tests(spec_name: str) -> dict:
import os
import time
from shared.sx.parser import parse_all
from shared.sx.evaluator import _eval, _trampoline
from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline
from shared.sx.types import Symbol, Keyword, Lambda, NIL
ref_dir = os.path.join(os.path.dirname(__file__), "..", "..", "shared", "sx", "ref")
@@ -817,7 +638,7 @@ def _run_modular_tests(spec_name: str) -> dict:
def _call_sx(fn, args, caller_env):
if isinstance(fn, Lambda):
from shared.sx.evaluator import _call_lambda
from shared.sx.ref.sx_ref import call_lambda as _call_lambda
return _trampoline(_call_lambda(fn, list(args), caller_env))
return fn(*args)
@@ -1089,35 +910,30 @@ def _affinity_demo_data() -> dict:
from shared.sx.jinja_bridge import get_component_env
from shared.sx.types import Component
from shared.sx.pages import get_all_pages
from shared.sx.ref.sx_ref import build_affinity_analysis
# I/O edge: extract component data and page render plans
env = get_component_env()
demo_names = [
"~aff-demo-auto",
"~aff-demo-client",
"~aff-demo-server",
"~aff-demo-io-auto",
"~aff-demo-io-client",
"~aff-demo-auto", "~aff-demo-client", "~aff-demo-server",
"~aff-demo-io-auto", "~aff-demo-io-client",
]
components = []
for name in demo_names:
val = env.get(name)
if isinstance(val, Component):
components.append({
"name": name,
"affinity": val.affinity,
"name": name, "affinity": val.affinity,
"render-target": val.render_target,
"io-refs": sorted(val.io_refs),
"is-pure": val.is_pure,
"io-refs": sorted(val.io_refs), "is-pure": val.is_pure,
})
# Collect render plans from all sx service pages
page_plans = []
for page_def in get_all_pages("sx").values():
plan = page_def.render_plan
if plan:
page_plans.append({
"name": page_def.name,
"path": page_def.path,
"name": page_def.name, "path": page_def.path,
"server-count": len(plan.get("server", [])),
"client-count": len(plan.get("client", [])),
"server": plan.get("server", []),
@@ -1125,7 +941,7 @@ def _affinity_demo_data() -> dict:
"io-deps": plan.get("io-deps", []),
})
return {"components": components, "page-plans": page_plans}
return build_affinity_analysis(components, page_plans)
def _optimistic_demo_data() -> dict:
@@ -1165,9 +981,9 @@ def _prove_data() -> dict:
"""
import time
from shared.sx.parser import parse_all
from shared.sx.evaluator import evaluate
from shared.sx.ref.sx_ref import evaluate
from shared.sx.primitives import all_primitives
from shared.sx.evaluator import _trampoline, _call_lambda
from shared.sx.ref.sx_ref import trampoline as _trampoline, call_lambda as _call_lambda
env = all_primitives()
@@ -1271,3 +1087,84 @@ def _offline_demo_data() -> dict:
],
"server-time": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
def _page_helpers_demo_data() -> dict:
"""Run page-helpers.sx functions server-side, return results for comparison with client."""
import os
import time
from shared.sx.parser import parse_all
from shared.sx.ref.sx_ref import (
categorize_special_forms, build_reference_data,
build_attr_detail, build_component_source,
build_routing_analysis,
)
ref_dir = _ref_dir()
results = {}
# 1. categorize-special-forms
t0 = time.monotonic()
with open(os.path.join(ref_dir, "special-forms.sx")) as f:
sf_exprs = parse_all(f.read())
sf_result = categorize_special_forms(sf_exprs)
sf_ms = round((time.monotonic() - t0) * 1000, 1)
sf_summary = {cat: len(forms) for cat, forms in sf_result.items()}
results["sf-categories"] = sf_summary
results["sf-total"] = sum(sf_summary.values())
results["sf-ms"] = sf_ms
# 2. build-reference-data
from content.pages import REQUEST_ATTRS, ATTR_DETAILS
t1 = time.monotonic()
ref_result = build_reference_data("attributes", {
"req-attrs": [list(t) for t in REQUEST_ATTRS[:5]],
"beh-attrs": [], "uniq-attrs": [],
}, list(ATTR_DETAILS.keys()))
ref_ms = round((time.monotonic() - t1) * 1000, 1)
results["ref-sample"] = ref_result.get("req-attrs", [])[:3]
results["ref-ms"] = ref_ms
# 3. build-attr-detail
t2 = time.monotonic()
detail = ATTR_DETAILS.get("sx-get")
attr_result = build_attr_detail("sx-get", detail)
attr_ms = round((time.monotonic() - t2) * 1000, 1)
results["attr-result"] = attr_result
results["attr-ms"] = attr_ms
# 4. build-component-source
t3 = time.monotonic()
comp_result = build_component_source({
"type": "component", "name": "~demo-card",
"params": ["title", "subtitle"],
"has-children": True,
"body-sx": "(div :class \"card\"\n (h2 title)\n (when subtitle (p subtitle))\n children)",
"affinity": "auto",
})
comp_ms = round((time.monotonic() - t3) * 1000, 1)
results["comp-source"] = comp_result
results["comp-ms"] = comp_ms
# 5. build-routing-analysis
t4 = time.monotonic()
routing_result = build_routing_analysis([
{"name": "home", "path": "/", "has-data": False, "content-src": "(~home-content)"},
{"name": "dashboard", "path": "/dash", "has-data": True, "content-src": "(~dashboard)"},
{"name": "about", "path": "/about", "has-data": False, "content-src": "(~about-content)"},
{"name": "settings", "path": "/settings", "has-data": True, "content-src": "(~settings)"},
])
routing_ms = round((time.monotonic() - t4) * 1000, 1)
results["routing-result"] = routing_result
results["routing-ms"] = routing_ms
# Total
results["server-total-ms"] = round(sf_ms + ref_ms + attr_ms + comp_ms + routing_ms, 1)
# Pass raw inputs for client-side island (serialized as data-sx-state)
results["sf-source"] = open(os.path.join(ref_dir, "special-forms.sx")).read()
results["attr-detail"] = detail
results["req-attrs"] = [list(t) for t in REQUEST_ATTRS[:5]]
results["attr-keys"] = list(ATTR_DETAILS.keys())
return results