diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 1a36e680..a5cf2edc 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-11T04:41:27Z"; + var SX_VERSION = "2026-03-11T16:56:11Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -204,7 +204,7 @@ // JSON / dict helpers for island state serialization function jsonSerialize(obj) { - try { return JSON.stringify(obj); } catch(e) { return "{}"; } + return JSON.stringify(obj); } function isEmptyDict(d) { if (!d || typeof d !== "object") return true; @@ -214,11 +214,34 @@ function envHas(env, name) { return name in env; } function envGet(env, name) { return env[name]; } - function envSet(env, name, val) { env[name] = val; } + function envSet(env, name, val) { + // Walk prototype chain to find where the variable is defined (for set!) + var obj = env; + while (obj !== null && obj !== Object.prototype) { + if (obj.hasOwnProperty(name)) { obj[name] = val; return; } + obj = Object.getPrototypeOf(obj); + } + // Not found in any parent scope — set on the immediate env + env[name] = val; + } function envExtend(env) { return Object.create(env); } function envMerge(base, overlay) { + // Same env or overlay is descendant of base — just extend, no copy. + // This prevents set! inside lambdas from modifying shadow copies. + if (base === overlay) return Object.create(base); + var p = overlay; + for (var d = 0; p && p !== Object.prototype && d < 100; d++) { + if (p === base) return Object.create(base); + p = Object.getPrototypeOf(p); + } + // General case: extend base, copy ONLY overlay properties that don't + // exist in the base chain (avoids shadowing closure bindings). var child = Object.create(base); - if (overlay) for (var k in overlay) if (overlay.hasOwnProperty(k)) child[k] = overlay[k]; + if (overlay) { + for (var k in overlay) { + if (overlay.hasOwnProperty(k) && !(k in base)) child[k] = overlay[k]; + } + } return child; } @@ -732,9 +755,9 @@ var kwargs = first(parsed); var children = nth(parsed, 1); var local = envMerge(componentClosure(comp), env); - { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = sxOr(dictGet(kwargs, p), NIL); } } + { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, sxOr(dictGet(kwargs, p), NIL)); } } if (isSxTruthy(componentHasChildren(comp))) { - local["children"] = children; + envSet(local, "children", children); } return makeThunk(componentBody(comp), local); })(); }; @@ -764,8 +787,11 @@ return (isSxTruthy((isSxTruthy(condition) && !isSxTruthy(isNil(condition)))) ? (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 1, (len(args) - 1))), makeThunk(last(args), env)) : NIL); })(); }; + // cond-scheme? + var condScheme_p = function(clauses) { return isEvery(function(c) { return (isSxTruthy((typeOf(c) == "list")) && (len(c) == 2)); }, clauses); }; + // sf-cond - var sfCond = function(args, env) { return (isSxTruthy((isSxTruthy((typeOf(first(args)) == "list")) && (len(first(args)) == 2))) ? sfCondScheme(args, env) : sfCondClojure(args, env)); }; + var sfCond = function(args, env) { return (isSxTruthy(condScheme_p(args)) ? sfCondScheme(args, env) : sfCondClojure(args, env)); }; // sf-cond-scheme var sfCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() { @@ -841,7 +867,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai var loopBody = (isSxTruthy((len(body) == 1)) ? first(body) : cons(makeSymbol("begin"), body)); var loopFn = makeLambda(params, loopBody, env); loopFn.name = loopName; - lambdaClosure(loopFn)[loopName] = loopFn; + envSet(lambdaClosure(loopFn), loopName, loopFn); return (function() { var initVals = map(function(e) { return trampoline(evalExpr(e, env)); }, inits); return callLambda(loopFn, initVals, env); @@ -865,7 +891,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai if (isSxTruthy((isSxTruthy(isLambda(value)) && isNil(lambdaName(value))))) { value.name = symbolName(nameSym); } - env[symbolName(nameSym)] = value; + envSet(env, symbolName(nameSym), value); return value; })(); }; @@ -881,7 +907,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai var affinity = defcompKwarg(args, "affinity", "auto"); return (function() { var comp = makeComponent(compName, params, hasChildren, body, env, affinity); - env[symbolName(nameSym)] = comp; + envSet(env, symbolName(nameSym), comp); return comp; })(); })(); }; @@ -924,7 +950,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai var hasChildren = nth(parsed, 1); return (function() { var island = makeIsland(compName, params, hasChildren, body, env); - env[symbolName(nameSym)] = island; + envSet(env, symbolName(nameSym), island); return island; })(); })(); }; @@ -939,7 +965,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai var restParam = nth(parsed, 1); return (function() { var mac = makeMacro(params, restParam, body, env, symbolName(nameSym)); - env[symbolName(nameSym)] = mac; + envSet(env, symbolName(nameSym), mac); return mac; })(); })(); }; @@ -956,7 +982,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai var sfDefstyle = function(args, env) { return (function() { var nameSym = first(args); var value = trampoline(evalExpr(nth(args, 1), env)); - env[symbolName(nameSym)] = value; + envSet(env, symbolName(nameSym), value); return value; })(); }; @@ -974,8 +1000,8 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai var head = first(template); return (isSxTruthy((isSxTruthy((typeOf(head) == "symbol")) && (symbolName(head) == "unquote"))) ? trampoline(evalExpr(nth(template, 1), env)) : reduce(function(result, item) { return (isSxTruthy((isSxTruthy((typeOf(item) == "list")) && isSxTruthy((len(item) == 2)) && isSxTruthy((typeOf(first(item)) == "symbol")) && (symbolName(first(item)) == "splice-unquote"))) ? (function() { var spliced = trampoline(evalExpr(nth(item, 1), env)); - return (isSxTruthy((typeOf(spliced) == "list")) ? concat(result, spliced) : (isSxTruthy(isNil(spliced)) ? result : append(result, spliced))); -})() : append(result, qqExpand(item, env))); }, [], template)); + return (isSxTruthy((typeOf(spliced) == "list")) ? concat(result, spliced) : (isSxTruthy(isNil(spliced)) ? result : concat(result, [spliced]))); +})() : concat(result, [qqExpand(item, env)])); }, [], template)); })())); }; // sf-thread-first @@ -996,7 +1022,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai var sfSetBang = function(args, env) { return (function() { var name = symbolName(first(args)); var value = trampoline(evalExpr(nth(args, 1), env)); - env[name] = value; + envSet(env, name, value); return value; })(); }; @@ -1021,7 +1047,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai })(); }, NIL, range(0, (len(bindings) / 2)))); (function() { var values = map(function(e) { return trampoline(evalExpr(e, local)); }, valExprs); - { var _c = zip(names, values); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = nth(pair, 1); } } + { var _c = zip(names, values); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; envSet(local, first(pair), nth(pair, 1)); } } return forEach(function(val) { return (isSxTruthy(isLambda(val)) ? forEach(function(n) { return envSet(lambdaClosure(val), n, envGet(local, n)); }, names) : NIL); }, values); })(); { var _c = slice(body, 0, (len(body) - 1)); for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; trampoline(evalExpr(e, local)); } } @@ -1046,9 +1072,9 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai // expand-macro var expandMacro = function(mac, rawArgs, env) { return (function() { var local = envMerge(macroClosure(mac), env); - { var _c = mapIndexed(function(i, p) { return [p, i]; }, macroParams(mac)); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = (isSxTruthy((nth(pair, 1) < len(rawArgs))) ? nth(rawArgs, nth(pair, 1)) : NIL); } } + { var _c = mapIndexed(function(i, p) { return [p, i]; }, macroParams(mac)); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; envSet(local, first(pair), (isSxTruthy((nth(pair, 1) < len(rawArgs))) ? nth(rawArgs, nth(pair, 1)) : NIL)); } } if (isSxTruthy(macroRestParam(mac))) { - local[macroRestParam(mac)] = slice(rawArgs, len(macroParams(mac))); + envSet(local, macroRestParam(mac), slice(rawArgs, len(macroParams(mac)))); } return trampoline(evalExpr(macroBody(mac), local)); })(); }; @@ -1143,7 +1169,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai })(); }, keys(attrs))); }; // eval-cond - var evalCond = function(clauses, env) { return (isSxTruthy((isSxTruthy(!isSxTruthy(isEmpty(clauses))) && isSxTruthy((typeOf(first(clauses)) == "list")) && (len(first(clauses)) == 2))) ? evalCondScheme(clauses, env) : evalCondClojure(clauses, env)); }; + var evalCond = function(clauses, env) { return (isSxTruthy(condScheme_p(clauses)) ? evalCondScheme(clauses, env) : evalCondClojure(clauses, env)); }; // eval-cond-scheme var evalCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() { @@ -1162,7 +1188,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai // process-bindings var processBindings = function(bindings, env) { return (function() { - var local = merge(env); + var local = envExtend(env); { var _c = bindings; for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(pair) == "list")) && (len(pair) >= 2)))) { (function() { var name = (isSxTruthy((typeOf(first(pair)) == "symbol")) ? symbolName(first(pair)) : (String(first(pair)))); @@ -1392,9 +1418,9 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = })(); }, {["i"]: 0, ["skip"]: false}, args); return (function() { var local = envMerge(componentClosure(comp), env); - { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } } + { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } } if (isSxTruthy(componentHasChildren(comp))) { - local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))); + envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)))); } return renderToHtml(componentBody(comp), local); })(); @@ -1458,20 +1484,20 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m = return (function() { var local = envMerge(componentClosure(island), env); var islandName = componentName(island); - { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } } + { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } } if (isSxTruthy(componentHasChildren(island))) { - local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))); + envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)))); } return (function() { var bodyHtml = renderToHtml(componentBody(island), local); - var stateJson = serializeIslandState(kwargs); - return (String("") + String(bodyHtml) + String("")); + var stateSx = serializeIslandState(kwargs); + return (String("") + String(bodyHtml) + String("")); })(); })(); })(); }; // serialize-island-state - var serializeIslandState = function(kwargs) { return (isSxTruthy(isEmptyDict(kwargs)) ? NIL : jsonSerialize(kwargs)); }; + var serializeIslandState = function(kwargs) { return (isSxTruthy(isEmptyDict(kwargs)) ? NIL : sxSerialize(kwargs)); }; // === Transpiled from adapter-sx === @@ -1505,30 +1531,34 @@ return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if // aser-fragment var aserFragment = function(children, env) { return (function() { - var parts = filter(function(x) { return !isSxTruthy(isNil(x)); }, map(function(c) { return aser(c, env); }, children)); - return (isSxTruthy(isEmpty(parts)) ? "" : (String("(<> ") + String(join(" ", map(serialize, parts))) + String(")"))); + var parts = []; + { var _c = children; for (var _i = 0; _i < _c.length; _i++) { var c = _c[_i]; (function() { + var result = aser(c, env); + return (isSxTruthy((typeOf(result) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, result) : (isSxTruthy(!isSxTruthy(isNil(result))) ? append_b(parts, serialize(result)) : NIL)); +})(); } } + return (isSxTruthy(isEmpty(parts)) ? "" : (String("(<> ") + String(join(" ", parts)) + String(")"))); })(); }; // aser-call var aserCall = function(name, args, env) { return (function() { var parts = [name]; - reduce(function(state, arg) { return (function() { - var skip = get(state, "skip"); - return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() { - var val = aser(nth(args, (get(state, "i") + 1)), env); + var skip = false; + var i = 0; + { var _c = args; for (var _i = 0; _i < _c.length; _i++) { var arg = _c[_i]; (isSxTruthy(skip) ? ((skip = false), (i = (i + 1))) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((i + 1) < len(args)))) ? (function() { + var val = aser(nth(args, (i + 1)), env); if (isSxTruthy(!isSxTruthy(isNil(val)))) { parts.push((String(":") + String(keywordName(arg)))); parts.push(serialize(val)); } - return assoc(state, "skip", true, "i", (get(state, "i") + 1)); + skip = true; + return (i = (i + 1)); })() : (function() { var val = aser(arg, env); if (isSxTruthy(!isSxTruthy(isNil(val)))) { - parts.push(serialize(val)); + (isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, val) : append_b(parts, serialize(val))); } - return assoc(state, "i", (get(state, "i") + 1)); -})())); -})(); }, {["i"]: 0, ["skip"]: false}, args); + return (i = (i + 1)); +})())); } } return (String("(") + String(join(" ", parts)) + String(")")); })(); }; @@ -1582,7 +1612,7 @@ return result; }, args); var coll = trampoline(evalExpr(nth(args, 1), env)); return map(function(item) { return (isSxTruthy(isLambda(f)) ? (function() { var local = envMerge(lambdaClosure(f), env); - local[first(lambdaParams(f))] = item; + envSet(local, first(lambdaParams(f)), item); return aser(lambdaBody(f), local); })() : invoke(f, item)); }, coll); })() : (isSxTruthy((name == "map-indexed")) ? (function() { @@ -1590,8 +1620,8 @@ return result; }, args); var coll = trampoline(evalExpr(nth(args, 1), env)); return mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? (function() { var local = envMerge(lambdaClosure(f), env); - local[first(lambdaParams(f))] = i; - local[nth(lambdaParams(f), 1)] = item; + envSet(local, first(lambdaParams(f)), i); + envSet(local, nth(lambdaParams(f), 1), item); return aser(lambdaBody(f), local); })() : invoke(f, i, item)); }, coll); })() : (isSxTruthy((name == "for-each")) ? (function() { @@ -1600,7 +1630,7 @@ return result; }, args); var results = []; { var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var item = _c[_i]; (isSxTruthy(isLambda(f)) ? (function() { var local = envMerge(lambdaClosure(f), env); - local[first(lambdaParams(f))] = item; + envSet(local, first(lambdaParams(f)), item); return append_b(results, aser(lambdaBody(f), local)); })() : invoke(f, item)); } } return (isSxTruthy(isEmpty(results)) ? NIL : results); @@ -1692,7 +1722,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme })(); }, {["i"]: 0, ["skip"]: false}, args); return (function() { var local = envMerge(componentClosure(comp), env); - { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } } + { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } } if (isSxTruthy(componentHasChildren(comp))) { (function() { var childFrag = createFragment(); @@ -1883,7 +1913,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme return (function() { var local = envMerge(componentClosure(island), env); var islandName = componentName(island); - { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } } + { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } } if (isSxTruthy(componentHasChildren(island))) { (function() { var childFrag = createFragment(); @@ -2998,9 +3028,9 @@ return postSwap(target); }))) : NIL); var exprs = sxParse(body); return domListen(el, eventName, function(e) { return (function() { var handlerEnv = envExtend({}); - handlerEnv["event"] = e; - handlerEnv["this"] = el; - handlerEnv["detail"] = eventDetail(e); + envSet(handlerEnv, "event", e); + envSet(handlerEnv, "this", el); + envSet(handlerEnv, "detail", eventDetail(e)); return forEach(function(expr) { return evalExpr(expr, handlerEnv); }, exprs); })(); }); })()) : NIL); @@ -3229,17 +3259,17 @@ callExpr.push(dictGet(kwargs, k)); } } // hydrate-island var hydrateIsland = function(el) { return (function() { var name = domGetAttr(el, "data-sx-island"); - var stateJson = sxOr(domGetAttr(el, "data-sx-state"), "{}"); + var stateSx = sxOr(domGetAttr(el, "data-sx-state"), "{}"); return (function() { var compName = (String("~") + String(name)); var env = getRenderEnv(NIL); return (function() { var comp = envGet(env, compName); return (isSxTruthy(!isSxTruthy(sxOr(isComponent(comp), isIsland(comp)))) ? logWarn((String("hydrate-island: unknown island ") + String(compName))) : (function() { - var kwargs = jsonParse(stateJson); + var kwargs = sxOr(first(sxParse(stateSx)), {}); var disposers = []; var local = envMerge(componentClosure(comp), env); - { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } } + { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } } return (function() { var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); }); domSetTextContent(el, ""); @@ -3433,6 +3463,125 @@ callExpr.push(dictGet(kwargs, k)); } } })(); }, keys(env)); }; + // === Transpiled from page-helpers (pure data transformation helpers) === + + // special-form-category-map + var specialFormCategoryMap = {"if": "Control Flow", "when": "Control Flow", "cond": "Control Flow", "case": "Control Flow", "and": "Control Flow", "or": "Control Flow", "let": "Binding", "let*": "Binding", "letrec": "Binding", "define": "Binding", "set!": "Binding", "lambda": "Functions & Components", "fn": "Functions & Components", "defcomp": "Functions & Components", "defmacro": "Functions & Components", "begin": "Sequencing & Threading", "do": "Sequencing & Threading", "->": "Sequencing & Threading", "quote": "Quoting", "quasiquote": "Quoting", "reset": "Continuations", "shift": "Continuations", "dynamic-wind": "Guards", "map": "Higher-Order Forms", "map-indexed": "Higher-Order Forms", "filter": "Higher-Order Forms", "reduce": "Higher-Order Forms", "some": "Higher-Order Forms", "every?": "Higher-Order Forms", "for-each": "Higher-Order Forms", "defstyle": "Domain Definitions", "defhandler": "Domain Definitions", "defpage": "Domain Definitions", "defquery": "Domain Definitions", "defaction": "Domain Definitions"}; + + // extract-define-kwargs + var extractDefineKwargs = function(expr) { return (function() { + var result = {}; + var items = slice(expr, 2); + var n = len(items); + { var _c = range(0, n); for (var _i = 0; _i < _c.length; _i++) { var idx = _c[_i]; if (isSxTruthy((isSxTruthy(((idx + 1) < n)) && (typeOf(nth(items, idx)) == "keyword")))) { + (function() { + var key = keywordName(nth(items, idx)); + var val = nth(items, (idx + 1)); + return dictSet(result, key, (isSxTruthy((typeOf(val) == "list")) ? (String("(") + String(join(" ", map(serialize, val))) + String(")")) : (String(val)))); +})(); +} } } + return result; +})(); }; + + // categorize-special-forms + var categorizeSpecialForms = function(parsedExprs) { return (function() { + var categories = {}; + { var _c = parsedExprs; for (var _i = 0; _i < _c.length; _i++) { var expr = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(expr) == "list")) && isSxTruthy((len(expr) >= 2)) && isSxTruthy((typeOf(first(expr)) == "symbol")) && (symbolName(first(expr)) == "define-special-form")))) { + (function() { + var name = nth(expr, 1); + var kwargs = extractDefineKwargs(expr); + var category = sxOr(get(specialFormCategoryMap, name), "Other"); + if (isSxTruthy(!isSxTruthy(dictHas(categories, category)))) { + categories[category] = []; +} + return append_b(get(categories, category), {"name": name, "syntax": sxOr(get(kwargs, "syntax"), ""), "doc": sxOr(get(kwargs, "doc"), ""), "tail-position": sxOr(get(kwargs, "tail-position"), ""), "example": sxOr(get(kwargs, "example"), "")}); +})(); +} } } + return categories; +})(); }; + + // build-ref-items-with-href + var buildRefItemsWithHref = function(items, basePath, detailKeys, nFields) { return map(function(item) { return (isSxTruthy((nFields == 3)) ? (function() { + var name = nth(item, 0); + var field2 = nth(item, 1); + var field3 = nth(item, 2); + return {"name": name, "desc": field2, "exists": field3, "href": (isSxTruthy((isSxTruthy(field3) && some(function(k) { return (k == name); }, detailKeys))) ? (String(basePath) + String(name)) : NIL)}; +})() : (function() { + var name = nth(item, 0); + var desc = nth(item, 1); + return {"name": name, "desc": desc, "href": (isSxTruthy(some(function(k) { return (k == name); }, detailKeys)) ? (String(basePath) + String(name)) : NIL)}; +})()); }, items); }; + + // build-reference-data + var buildReferenceData = function(slug, rawData, detailKeys) { return (function() { var _m = slug; if (_m == "attributes") return {"req-attrs": buildRefItemsWithHref(get(rawData, "req-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "beh-attrs": buildRefItemsWithHref(get(rawData, "beh-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "uniq-attrs": buildRefItemsWithHref(get(rawData, "uniq-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3)}; if (_m == "headers") return {"req-headers": buildRefItemsWithHref(get(rawData, "req-headers"), "/hypermedia/reference/headers/", detailKeys, 3), "resp-headers": buildRefItemsWithHref(get(rawData, "resp-headers"), "/hypermedia/reference/headers/", detailKeys, 3)}; if (_m == "events") return {"events-list": buildRefItemsWithHref(get(rawData, "events-list"), "/hypermedia/reference/events/", detailKeys, 2)}; if (_m == "js-api") return {"js-api-list": map(function(item) { return {"name": nth(item, 0), "desc": nth(item, 1)}; }, get(rawData, "js-api-list"))}; return {"req-attrs": buildRefItemsWithHref(get(rawData, "req-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "beh-attrs": buildRefItemsWithHref(get(rawData, "beh-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3), "uniq-attrs": buildRefItemsWithHref(get(rawData, "uniq-attrs"), "/hypermedia/reference/attributes/", detailKeys, 3)}; })(); }; + + // build-attr-detail + var buildAttrDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"attr-not-found": true} : {"attr-not-found": NIL, "attr-title": slug, "attr-description": get(detail, "description"), "attr-example": get(detail, "example"), "attr-handler": get(detail, "handler"), "attr-demo": get(detail, "demo"), "attr-wire-id": (isSxTruthy(dictHas(detail, "handler")) ? (String("ref-wire-") + String(replace_(replace_(slug, ":", "-"), "*", "star"))) : NIL)}); }; + + // build-header-detail + var buildHeaderDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"header-not-found": true} : {"header-not-found": NIL, "header-title": slug, "header-direction": get(detail, "direction"), "header-description": get(detail, "description"), "header-example": get(detail, "example"), "header-demo": get(detail, "demo")}); }; + + // build-event-detail + var buildEventDetail = function(slug, detail) { return (isSxTruthy(isNil(detail)) ? {"event-not-found": true} : {"event-not-found": NIL, "event-title": slug, "event-description": get(detail, "description"), "event-example": get(detail, "example"), "event-demo": get(detail, "demo")}); }; + + // build-component-source + var buildComponentSource = function(compData) { return (function() { + var compType = get(compData, "type"); + var name = get(compData, "name"); + var params = get(compData, "params"); + var hasChildren = get(compData, "has-children"); + var bodySx = get(compData, "body-sx"); + var affinity = get(compData, "affinity"); + return (isSxTruthy((compType == "not-found")) ? (String(";; component ") + String(name) + String(" not found")) : (function() { + var paramStrs = (isSxTruthy(isEmpty(params)) ? (isSxTruthy(hasChildren) ? ["&rest", "children"] : []) : (isSxTruthy(hasChildren) ? append(cons("&key", params), ["&rest", "children"]) : cons("&key", params))); + var paramsSx = (String("(") + String(join(" ", paramStrs)) + String(")")); + var formName = (isSxTruthy((compType == "island")) ? "defisland" : "defcomp"); + var affinityStr = (isSxTruthy((isSxTruthy((compType == "component")) && isSxTruthy(!isSxTruthy(isNil(affinity))) && !isSxTruthy((affinity == "auto")))) ? (String(" :affinity ") + String(affinity)) : ""); + return (String("(") + String(formName) + String(" ") + String(name) + String(" ") + String(paramsSx) + String(affinityStr) + String("\n ") + String(bodySx) + String(")")); +})()); +})(); }; + + // build-bundle-analysis + var buildBundleAnalysis = function(pagesRaw, componentsRaw, totalComponents, totalMacros, pureCount, ioCount) { return (function() { + var pagesData = []; + { var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() { + var neededNames = get(page, "needed-names"); + var n = len(neededNames); + var pct = (isSxTruthy((totalComponents > 0)) ? round(((n / totalComponents) * 100)) : 0); + var savings = (100 - pct); + var pureInPage = 0; + var ioInPage = 0; + var pageIoRefs = []; + var compDetails = []; + { var _c = neededNames; for (var _i = 0; _i < _c.length; _i++) { var compName = _c[_i]; (function() { + var info = get(componentsRaw, compName); + return (isSxTruthy(!isSxTruthy(isNil(info))) ? ((isSxTruthy(get(info, "is-pure")) ? (pureInPage = (pureInPage + 1)) : ((ioInPage = (ioInPage + 1)), forEach(function(ref) { return (isSxTruthy(!isSxTruthy(some(function(r) { return (r == ref); }, pageIoRefs))) ? append_b(pageIoRefs, ref) : NIL); }, sxOr(get(info, "io-refs"), [])))), append_b(compDetails, {"name": compName, "is-pure": get(info, "is-pure"), "affinity": get(info, "affinity"), "render-target": get(info, "render-target"), "io-refs": sxOr(get(info, "io-refs"), []), "deps": sxOr(get(info, "deps"), []), "source": get(info, "source")})) : NIL); +})(); } } + return append_b(pagesData, {"name": get(page, "name"), "path": get(page, "path"), "direct": get(page, "direct"), "needed": n, "pct": pct, "savings": savings, "io-refs": len(pageIoRefs), "pure-in-page": pureInPage, "io-in-page": ioInPage, "components": compDetails}); +})(); } } + return {"pages": pagesData, "total-components": totalComponents, "total-macros": totalMacros, "pure-count": pureCount, "io-count": ioCount}; +})(); }; + + // build-routing-analysis + var buildRoutingAnalysis = function(pagesRaw) { return (function() { + var pagesData = []; + var clientCount = 0; + var serverCount = 0; + { var _c = pagesRaw; for (var _i = 0; _i < _c.length; _i++) { var page = _c[_i]; (function() { + var hasData = get(page, "has-data"); + var contentSrc = sxOr(get(page, "content-src"), ""); + var mode = NIL; + var reason = ""; + (isSxTruthy(hasData) ? ((mode = "server"), (reason = "Has :data expression — needs server IO"), (serverCount = (serverCount + 1))) : (isSxTruthy(isEmpty(contentSrc)) ? ((mode = "server"), (reason = "No content expression"), (serverCount = (serverCount + 1))) : ((mode = "client"), (clientCount = (clientCount + 1))))); + return append_b(pagesData, {"name": get(page, "name"), "path": get(page, "path"), "mode": mode, "has-data": hasData, "content-expr": (isSxTruthy((len(contentSrc) > 80)) ? (String(slice(contentSrc, 0, 80)) + String("...")) : contentSrc), "reason": reason}); +})(); } } + return {"pages": pagesData, "total-pages": (clientCount + serverCount), "client-count": clientCount, "server-count": serverCount}; +})(); }; + + // build-affinity-analysis + var buildAffinityAnalysis = function(demoComponents, pagePlans) { return {"components": demoComponents, "page-plans": pagePlans}; }; + + // === Transpiled from router (client-side route matching) === // split-path-segments @@ -3853,8 +4002,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { function domListen(el, name, handler) { if (!_hasDom || !el) return function() {}; // Wrap SX lambdas from runtime-evaluated island code into native fns + // If lambda takes 0 params, call without event arg (convenience for on-click handlers) var wrapped = isLambda(handler) - ? function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } } + ? (lambdaParams(handler).length === 0 + ? function(e) { try { invoke(handler); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } } + : function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }) : handler; if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler)); el.addEventListener(name, wrapped); @@ -3947,7 +4099,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { } } - function nowMs() { return Date.now(); } + function nowMs() { return (typeof performance !== "undefined") ? performance.now() : Date.now(); } function parseHeaderValue(s) { if (!s) return null; @@ -5060,6 +5212,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { if (typeof elementValue === "function") PRIMITIVES["element-value"] = elementValue; if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml; if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml; + if (typeof domTextContent === "function") PRIMITIVES["dom-text-content"] = domTextContent; + if (typeof jsonParse === "function") PRIMITIVES["json-parse"] = jsonParse; + if (typeof nowMs === "function") PRIMITIVES["now-ms"] = nowMs; + PRIMITIVES["sx-parse"] = sxParse; // Expose deps module functions as primitives so runtime-evaluated SX code // (e.g. test-deps.sx in browser) can call them @@ -5090,6 +5246,19 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { PRIMITIVES["render-target"] = renderTarget; PRIMITIVES["page-render-plan"] = pageRenderPlan; + // Expose page-helper functions as primitives + PRIMITIVES["categorize-special-forms"] = categorizeSpecialForms; + PRIMITIVES["extract-define-kwargs"] = extractDefineKwargs; + PRIMITIVES["build-reference-data"] = buildReferenceData; + PRIMITIVES["build-ref-items-with-href"] = buildRefItemsWithHref; + PRIMITIVES["build-attr-detail"] = buildAttrDetail; + PRIMITIVES["build-header-detail"] = buildHeaderDetail; + PRIMITIVES["build-event-detail"] = buildEventDetail; + PRIMITIVES["build-component-source"] = buildComponentSource; + PRIMITIVES["build-bundle-analysis"] = buildBundleAnalysis; + PRIMITIVES["build-routing-analysis"] = buildRoutingAnalysis; + PRIMITIVES["build-affinity-analysis"] = buildAffinityAnalysis; + // ========================================================================= // Async IO: Promise-aware rendering for client-side IO primitives // ========================================================================= @@ -5823,6 +5992,15 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { transitiveIoRefs: transitiveIoRefs, computeAllIoRefs: computeAllIoRefs, componentPure_p: componentPure_p, + categorizeSpecialForms: categorizeSpecialForms, + buildReferenceData: buildReferenceData, + buildAttrDetail: buildAttrDetail, + buildHeaderDetail: buildHeaderDetail, + buildEventDetail: buildEventDetail, + buildComponentSource: buildComponentSource, + buildBundleAnalysis: buildBundleAnalysis, + buildRoutingAnalysis: buildRoutingAnalysis, + buildAffinityAnalysis: buildAffinityAnalysis, splitPathSegments: splitPathSegments, parseRoutePattern: parseRoutePattern, matchRoute: matchRoute, diff --git a/shared/sx/__init__.py b/shared/sx/__init__.py index 20d357c8..9322cfb2 100644 --- a/shared/sx/__init__.py +++ b/shared/sx/__init__.py @@ -31,11 +31,8 @@ from .parser import ( parse_all, serialize, ) -from .evaluator import ( - EvalError, - evaluate, - make_env, -) +from .types import EvalError +from .ref.sx_ref import evaluate, make_env from .primitives import ( all_primitives, diff --git a/shared/sx/async_eval.py b/shared/sx/async_eval.py index beccab34..78c8e526 100644 --- a/shared/sx/async_eval.py +++ b/shared/sx/async_eval.py @@ -53,7 +53,8 @@ from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol _expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar( "_expand_components", default=False ) -from .evaluator import _expand_macro, EvalError +from .ref.sx_ref import expand_macro as _expand_macro +from .types import EvalError from .primitives import _PRIMITIVES from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io from .parser import SxExpr, serialize @@ -420,23 +421,23 @@ async def _asf_define(expr, env, ctx): async def _asf_defcomp(expr, env, ctx): - from .evaluator import _sf_defcomp - return _sf_defcomp(expr, env) + from .ref.sx_ref import sf_defcomp + return sf_defcomp(expr[1:], env) async def _asf_defstyle(expr, env, ctx): - from .evaluator import _sf_defstyle - return _sf_defstyle(expr, env) + from .ref.sx_ref import sf_defstyle + return sf_defstyle(expr[1:], env) async def _asf_defmacro(expr, env, ctx): - from .evaluator import _sf_defmacro - return _sf_defmacro(expr, env) + from .ref.sx_ref import sf_defmacro + return sf_defmacro(expr[1:], env) async def _asf_defhandler(expr, env, ctx): - from .evaluator import _sf_defhandler - return _sf_defhandler(expr, env) + from .ref.sx_ref import sf_defhandler + return sf_defhandler(expr[1:], env) async def _asf_begin(expr, env, ctx): @@ -599,7 +600,7 @@ async def _asf_reset(expr, env, ctx): _ASYNC_RESET_RESUME.append(value if value is not None else NIL) try: # Sync re-evaluation; the async caller will trampoline - from .evaluator import _eval as sync_eval, _trampoline + from .ref.sx_ref import eval_expr as sync_eval, trampoline as _trampoline return _trampoline(sync_eval(body, env)) finally: _ASYNC_RESET_RESUME.pop() diff --git a/shared/sx/evaluator.py b/shared/sx/evaluator.py deleted file mode 100644 index 4e703528..00000000 --- a/shared/sx/evaluator.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -S-expression evaluator — thin shim over bootstrapped sx_ref.py. - -All evaluation logic lives in the spec (shared/sx/ref/eval.sx) and is -bootstrapped to Python (shared/sx/ref/sx_ref.py). This module re-exports -the public API and internal helpers under their historical names so that -existing callers don't need updating. - -Imports are lazy (inside functions/properties) to avoid circular imports -during bootstrapping: bootstrap_py.py → parser → __init__ → evaluator → sx_ref. -""" - -from __future__ import annotations - - -def _ref(): - """Lazy import of the bootstrapped evaluator.""" - from .ref import sx_ref - return sx_ref - - -# --------------------------------------------------------------------------- -# Public API — these are the most used, so we make them importable directly -# --------------------------------------------------------------------------- - -class EvalError(Exception): - """Error during expression evaluation. - - Delegates to the bootstrapped EvalError at runtime but is defined here - so imports don't fail during bootstrapping. - """ - pass - - -def evaluate(expr, env=None): - return _ref().evaluate(expr, env) - - -def make_env(**kwargs): - return _ref().make_env(**kwargs) - - -# --------------------------------------------------------------------------- -# Internal helpers — used by html.py, async_eval.py, handlers.py, etc. -# --------------------------------------------------------------------------- - -def _eval(expr, env): - return _ref().eval_expr(expr, env) - - -def _trampoline(val): - return _ref().trampoline(val) - - -def _call_lambda(fn, args, caller_env): - return _ref().call_lambda(fn, args, caller_env) - - -def _call_component(comp, raw_args, env): - return _ref().call_component(comp, raw_args, env) - - -def _expand_macro(macro, raw_args, env): - return _ref().expand_macro(macro, raw_args, env) - - -# --------------------------------------------------------------------------- -# Special-form wrappers: callers pass (expr, env) with expr[0] = head symbol. -# sx_ref.py special forms take (args, env) where args = expr[1:]. -# --------------------------------------------------------------------------- - -def _sf_defcomp(expr, env): - return _ref().sf_defcomp(expr[1:], env) - -def _sf_defisland(expr, env): - return _ref().sf_defisland(expr[1:], env) - -def _sf_defstyle(expr, env): - return _ref().sf_defstyle(expr[1:], env) - -def _sf_defmacro(expr, env): - return _ref().sf_defmacro(expr[1:], env) - -def _sf_defhandler(expr, env): - return _ref().sf_defhandler(expr[1:], env) - -def _sf_defpage(expr, env): - return _ref().sf_defpage(expr[1:], env) - -def _sf_defquery(expr, env): - return _ref().sf_defquery(expr[1:], env) - -def _sf_defaction(expr, env): - return _ref().sf_defaction(expr[1:], env) diff --git a/shared/sx/handlers.py b/shared/sx/handlers.py index 0aabd8a8..1a775d43 100644 --- a/shared/sx/handlers.py +++ b/shared/sx/handlers.py @@ -70,10 +70,7 @@ def load_handler_file(filepath: str, service_name: str) -> list[HandlerDef]: """Parse an .sx file, evaluate it, and register any HandlerDef values.""" from .parser import parse_all import os - if os.environ.get("SX_USE_REF") == "1": - from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline - else: - from .evaluator import _eval as _raw_eval, _trampoline + from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline _eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) from .jinja_bridge import get_component_env diff --git a/shared/sx/html.py b/shared/sx/html.py index 2341bd98..694a03fc 100644 --- a/shared/sx/html.py +++ b/shared/sx/html.py @@ -28,7 +28,7 @@ import contextvars from typing import Any from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol -from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline +from .ref.sx_ref import eval_expr as _raw_eval, call_component as _raw_call_component, expand_macro as _expand_macro, trampoline as _trampoline def _eval(expr, env): """Evaluate and unwrap thunks — all html.py _eval calls are non-tail.""" @@ -414,10 +414,10 @@ def _render_component(comp: Component, args: list, env: dict[str, Any]) -> str: def _render_island(island: Island, args: list, env: dict[str, Any]) -> str: """Render an island as static HTML with hydration attributes. - Produces: body HTML - The client hydrates this into a reactive island. + Produces: body HTML + The client hydrates this into a reactive island via sx-parse (not JSON). """ - import json as _json + from .parser import serialize as _sx_serialize kwargs: dict[str, Any] = {} children: list[Any] = [] @@ -443,26 +443,13 @@ def _render_island(island: Island, args: list, env: dict[str, Any]) -> str: body_html = _render(island.body, local) - # Serialize state for hydration — only keyword args - state = {} - for k, v in kwargs.items(): - if isinstance(v, (str, int, float, bool)): - state[k] = v - elif v is NIL or v is None: - state[k] = None - elif isinstance(v, list): - state[k] = v - elif isinstance(v, dict): - state[k] = v - else: - state[k] = str(v) - - state_json = _escape_attr(_json.dumps(state, separators=(",", ":"))) if state else "" + # Serialize state for hydration — SX format (not JSON) + state_sx = _escape_attr(_sx_serialize(kwargs)) if kwargs else "" island_name = _escape_attr(island.name) parts = [f'") parts.append(body_html) parts.append("") diff --git a/shared/sx/jinja_bridge.py b/shared/sx/jinja_bridge.py index 9da4a6c4..167eac57 100644 --- a/shared/sx/jinja_bridge.py +++ b/shared/sx/jinja_bridge.py @@ -229,10 +229,7 @@ def register_components(sx_source: str) -> None: (div :class "..." (div :class "..." title))))) ''') """ - if _os.environ.get("SX_USE_REF") == "1": - from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline - else: - from .evaluator import _eval as _raw_eval, _trampoline + from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline _eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) from .parser import parse_all from .css_registry import scan_classes_from_sx diff --git a/shared/sx/pages.py b/shared/sx/pages.py index f0df5ce0..2050a5b2 100644 --- a/shared/sx/pages.py +++ b/shared/sx/pages.py @@ -127,7 +127,7 @@ def get_page_helpers(service: str) -> dict[str, Any]: def load_page_file(filepath: str, service_name: str) -> list[PageDef]: """Parse an .sx file, evaluate it, and register any PageDef values.""" from .parser import parse_all - from .evaluator import _eval as _raw_eval, _trampoline + from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline _eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) from .jinja_bridge import get_component_env @@ -170,7 +170,11 @@ async def _eval_slot(expr: Any, env: dict, ctx: Any) -> str: Expands component calls (so IO in the body executes) but serializes the result as SX wire format, not HTML. """ - from .async_eval import async_eval_slot_to_sx + import os + if os.environ.get("SX_USE_REF") == "1": + from .ref.async_eval_ref import async_eval_slot_to_sx + else: + from .async_eval import async_eval_slot_to_sx return await async_eval_slot_to_sx(expr, env, ctx) diff --git a/shared/sx/parser.py b/shared/sx/parser.py index c4ac622c..f4092936 100644 --- a/shared/sx/parser.py +++ b/shared/sx/parser.py @@ -41,7 +41,7 @@ def _resolve_sx_reader_macro(name: str): """ try: from .jinja_bridge import get_component_env - from .evaluator import _trampoline, _call_lambda + from .ref.sx_ref import trampoline as _trampoline, call_lambda as _call_lambda from .types import Lambda except ImportError: return None diff --git a/shared/sx/query_registry.py b/shared/sx/query_registry.py index e6d63e35..3edda12e 100644 --- a/shared/sx/query_registry.py +++ b/shared/sx/query_registry.py @@ -78,7 +78,7 @@ def clear(service: str | None = None) -> None: def load_query_file(filepath: str, service_name: str) -> list[QueryDef]: """Parse an .sx file and register any defquery definitions.""" from .parser import parse_all - from .evaluator import _eval as _raw_eval, _trampoline + from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline _eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) from .jinja_bridge import get_component_env @@ -103,7 +103,7 @@ def load_query_file(filepath: str, service_name: str) -> list[QueryDef]: def load_action_file(filepath: str, service_name: str) -> list[ActionDef]: """Parse an .sx file and register any defaction definitions.""" from .parser import parse_all - from .evaluator import _eval as _raw_eval, _trampoline + from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline _eval = lambda expr, env: _trampoline(_raw_eval(expr, env)) from .jinja_bridge import get_component_env diff --git a/shared/sx/ref/adapter-async.sx b/shared/sx/ref/adapter-async.sx new file mode 100644 index 00000000..c2f093db --- /dev/null +++ b/shared/sx/ref/adapter-async.sx @@ -0,0 +1,1262 @@ +;; ========================================================================== +;; adapter-async.sx — Async rendering and serialization adapter +;; +;; Async versions of adapter-html.sx (render) and adapter-sx.sx (aser) +;; for use with I/O-capable server environments (Python async, JS promises). +;; +;; Structurally identical to the sync adapters but uses async primitives: +;; async-eval — evaluate with I/O interception (platform primitive) +;; async-render — defined here, async HTML rendering +;; async-aser — defined here, async SX wire format +;; +;; All functions in this file are emitted as async by the bootstrapper. +;; Calls to other async functions receive await automatically. +;; +;; Depends on: +;; eval.sx — cond-scheme?, eval-cond-scheme, eval-cond-clojure, +;; expand-macro, env-merge, lambda?, component?, island?, +;; macro?, lambda-closure, lambda-params, lambda-body +;; render.sx — HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, +;; render-attrs, definition-form?, process-bindings, eval-cond +;; +;; Platform primitives (provided by host): +;; (async-eval expr env ctx) — evaluate with I/O interception +;; (io-primitive? name) — check if name is I/O primitive +;; (execute-io name args kw ctx) — execute an I/O primitive +;; (expand-components?) — context var: expand components in aser? +;; (svg-context?) — context var: in SVG rendering context? +;; (svg-context-set! val) — set SVG context +;; (svg-context-reset! token) — reset SVG context +;; (css-class-collect! val) — collect CSS classes for bundling +;; (is-raw-html? x) — check if value is raw HTML marker +;; (raw-html-content x) — extract HTML string from marker +;; (make-raw-html s) — wrap string as raw HTML +;; (async-coroutine? x) — check if value is a coroutine +;; (async-await! x) — await a coroutine value +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; Async HTML renderer +;; -------------------------------------------------------------------------- + +(define-async async-render + (fn (expr env ctx) + (case (type-of expr) + "nil" "" + "boolean" "" + "string" (escape-html expr) + "number" (escape-html (str expr)) + "raw-html" (raw-html-content expr) + "symbol" (let ((val (async-eval expr env ctx))) + (async-render val env ctx)) + "keyword" (escape-html (keyword-name expr)) + "list" (if (empty? expr) "" (async-render-list expr env ctx)) + "dict" "" + :else (escape-html (str expr))))) + + +(define-async async-render-list + (fn (expr env ctx) + (let ((head (first expr))) + (if (not (= (type-of head) "symbol")) + ;; Non-symbol head — data list, render each item + (if (or (lambda? head) (= (type-of head) "list")) + ;; Lambda/list call — eval then render + (async-render (async-eval expr env ctx) env ctx) + ;; Data list + (join "" (async-map-render expr env ctx))) + + ;; Symbol head — dispatch + (let ((name (symbol-name head)) + (args (rest expr))) + (cond + ;; I/O primitive + (io-primitive? name) + (async-render (async-eval expr env ctx) env ctx) + + ;; raw! + (= name "raw!") + (async-render-raw args env ctx) + + ;; Fragment + (= name "<>") + (join "" (async-map-render args env ctx)) + + ;; html: prefix + (starts-with? name "html:") + (async-render-element (slice name 5) args env ctx) + + ;; Render-aware special form (but check HTML tag + keyword first) + (async-render-form? name) + (if (and (contains? HTML_TAGS name) + (or (and (> (len expr) 1) (= (type-of (nth expr 1)) "keyword")) + (svg-context?))) + (async-render-element name args env ctx) + (dispatch-async-render-form name expr env ctx)) + + ;; Macro + (and (env-has? env name) (macro? (env-get env name))) + (async-render + (trampoline (expand-macro (env-get env name) args env)) + env ctx) + + ;; HTML tag + (contains? HTML_TAGS name) + (async-render-element name args env ctx) + + ;; Island (~name) + (and (starts-with? name "~") + (env-has? env name) + (island? (env-get env name))) + (async-render-island (env-get env name) args env ctx) + + ;; Component (~name) + (starts-with? name "~") + (let ((val (if (env-has? env name) (env-get env name) nil))) + (cond + (component? val) (async-render-component val args env ctx) + (macro? val) (async-render (trampoline (expand-macro val args env)) env ctx) + :else (async-render (async-eval expr env ctx) env ctx))) + + ;; Custom element (has - and keyword arg) + (and (> (index-of name "-") 0) + (> (len expr) 1) + (= (type-of (nth expr 1)) "keyword")) + (async-render-element name args env ctx) + + ;; SVG context + (svg-context?) + (async-render-element name args env ctx) + + ;; Fallback — eval then render + :else + (async-render (async-eval expr env ctx) env ctx))))))) + + +;; -------------------------------------------------------------------------- +;; async-render-raw — handle (raw! ...) in async context +;; -------------------------------------------------------------------------- + +(define-async async-render-raw + (fn (args env ctx) + (let ((parts (list))) + (for-each + (fn (arg) + (let ((val (async-eval arg env ctx))) + (cond + (is-raw-html? val) (append! parts (raw-html-content val)) + (= (type-of val) "string") (append! parts val) + (and (not (nil? val)) (not (= val false))) + (append! parts (str val))))) + args) + (join "" parts)))) + + +;; -------------------------------------------------------------------------- +;; async-render-element — render an HTML element with async arg evaluation +;; -------------------------------------------------------------------------- + +(define-async async-render-element + (fn (tag args env ctx) + (let ((attrs (dict)) + (children (list))) + ;; Parse keyword attrs and children + (async-parse-element-args args attrs children env ctx) + ;; Collect CSS classes + (let ((class-val (dict-get attrs "class"))) + (when (and (not (nil? class-val)) (not (= class-val false))) + (css-class-collect! (str class-val)))) + ;; Build opening tag + (let ((opening (str "<" tag (render-attrs attrs) ">"))) + (if (contains? VOID_ELEMENTS tag) + opening + (let ((token (if (or (= tag "svg") (= tag "math")) + (svg-context-set! true) + nil)) + (child-html (join "" (async-map-render children env ctx)))) + (when token (svg-context-reset! token)) + (str opening child-html ""))))))) + + +;; -------------------------------------------------------------------------- +;; async-parse-element-args — parse :key val pairs + children, async eval +;; -------------------------------------------------------------------------- +;; Uses for-each + mutable state instead of reduce, because the bootstrapper +;; compiles inline for-each lambdas as for loops (which can contain await). + +(define-async async-parse-element-args + (fn (args attrs children env ctx) + (let ((skip false) + (i 0)) + (for-each + (fn (arg) + (if skip + (do (set! skip false) + (set! i (inc i))) + (if (and (= (type-of arg) "keyword") + (< (inc i) (len args))) + (let ((val (async-eval (nth args (inc i)) env ctx))) + (dict-set! attrs (keyword-name arg) val) + (set! skip true) + (set! i (inc i))) + (do + (append! children arg) + (set! i (inc i)))))) + args)))) + + +;; -------------------------------------------------------------------------- +;; async-render-component — expand and render a component asynchronously +;; -------------------------------------------------------------------------- + +(define-async async-render-component + (fn (comp args env ctx) + (let ((kwargs (dict)) + (children (list))) + ;; Parse keyword args and children + (async-parse-kw-args args kwargs children env ctx) + ;; Build env: closure + caller env + params + (let ((local (env-merge (component-closure comp) env))) + (for-each + (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) + (component-params comp)) + (when (component-has-children? comp) + (env-set! local "children" + (make-raw-html + (join "" (async-map-render children env ctx))))) + (async-render (component-body comp) local ctx))))) + + +;; -------------------------------------------------------------------------- +;; async-render-island — SSR render of reactive island with hydration markers +;; -------------------------------------------------------------------------- + +(define-async async-render-island + (fn (island args env ctx) + (let ((kwargs (dict)) + (children (list))) + (async-parse-kw-args args kwargs children env ctx) + (let ((local (env-merge (component-closure island) env)) + (island-name (component-name island))) + (for-each + (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) + (component-params island)) + (when (component-has-children? island) + (env-set! local "children" + (make-raw-html + (join "" (async-map-render children env ctx))))) + (let ((body-html (async-render (component-body island) local ctx)) + (state-json (serialize-island-state kwargs))) + (str "" + body-html + "")))))) + + +;; -------------------------------------------------------------------------- +;; async-render-lambda — render lambda body in HTML context +;; -------------------------------------------------------------------------- + +(define-async async-render-lambda + (fn (f args env ctx) + (let ((local (env-merge (lambda-closure f) env))) + (for-each-indexed + (fn (i p) (env-set! local p (nth args i))) + (lambda-params f)) + (async-render (lambda-body f) local ctx)))) + + +;; -------------------------------------------------------------------------- +;; async-parse-kw-args — parse keyword args and children with async eval +;; -------------------------------------------------------------------------- + +(define-async async-parse-kw-args + (fn (args kwargs children env ctx) + (let ((skip false) + (i 0)) + (for-each + (fn (arg) + (if skip + (do (set! skip false) + (set! i (inc i))) + (if (and (= (type-of arg) "keyword") + (< (inc i) (len args))) + (let ((val (async-eval (nth args (inc i)) env ctx))) + (dict-set! kwargs (keyword-name arg) val) + (set! skip true) + (set! i (inc i))) + (do + (append! children arg) + (set! i (inc i)))))) + args)))) + + +;; -------------------------------------------------------------------------- +;; async-map-render — map async-render over a list, return list of strings +;; -------------------------------------------------------------------------- +;; Bootstrapper emits this as: [await async_render(x, env, ctx) for x in exprs] + +(define-async async-map-render + (fn (exprs env ctx) + (let ((results (list))) + (for-each + (fn (x) (append! results (async-render x env ctx))) + exprs) + results))) + + +;; -------------------------------------------------------------------------- +;; Render-aware form classification +;; -------------------------------------------------------------------------- + +(define ASYNC_RENDER_FORMS + (list "if" "when" "cond" "case" "let" "let*" "begin" "do" + "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" + "map" "map-indexed" "filter" "for-each")) + +(define async-render-form? + (fn (name) + (contains? ASYNC_RENDER_FORMS name))) + + +;; -------------------------------------------------------------------------- +;; dispatch-async-render-form — async special form rendering for HTML output +;; -------------------------------------------------------------------------- +;; +;; Uses cond-scheme? from eval.sx (the FIXED version with every? check) +;; and eval-cond from render.sx for correct scheme/clojure classification. + +(define-async dispatch-async-render-form + (fn (name expr env ctx) + (cond + ;; if + (= name "if") + (let ((cond-val (async-eval (nth expr 1) env ctx))) + (if cond-val + (async-render (nth expr 2) env ctx) + (if (> (len expr) 3) + (async-render (nth expr 3) env ctx) + ""))) + + ;; when + (= name "when") + (if (not (async-eval (nth expr 1) env ctx)) + "" + (join "" (async-map-render (slice expr 2) env ctx))) + + ;; cond — uses cond-scheme? (every? check) from eval.sx + (= name "cond") + (let ((clauses (rest expr))) + (if (cond-scheme? clauses) + (async-render-cond-scheme clauses env ctx) + (async-render-cond-clojure clauses env ctx))) + + ;; case + (= name "case") + (async-render (async-eval expr env ctx) env ctx) + + ;; let / let* + (or (= name "let") (= name "let*")) + (let ((local (async-process-bindings (nth expr 1) env ctx))) + (join "" (async-map-render (slice expr 2) local ctx))) + + ;; begin / do + (or (= name "begin") (= name "do")) + (join "" (async-map-render (rest expr) env ctx)) + + ;; Definition forms + (definition-form? name) + (do (async-eval expr env ctx) "") + + ;; map + (= name "map") + (let ((f (async-eval (nth expr 1) env ctx)) + (coll (async-eval (nth expr 2) env ctx))) + (join "" + (async-map-fn-render f coll env ctx))) + + ;; map-indexed + (= name "map-indexed") + (let ((f (async-eval (nth expr 1) env ctx)) + (coll (async-eval (nth expr 2) env ctx))) + (join "" + (async-map-indexed-fn-render f coll env ctx))) + + ;; filter — eval fully then render + (= name "filter") + (async-render (async-eval expr env ctx) env ctx) + + ;; for-each (render variant) + (= name "for-each") + (let ((f (async-eval (nth expr 1) env ctx)) + (coll (async-eval (nth expr 2) env ctx))) + (join "" + (async-map-fn-render f coll env ctx))) + + ;; Fallback + :else + (async-render (async-eval expr env ctx) env ctx)))) + + +;; -------------------------------------------------------------------------- +;; async-render-cond-scheme — scheme-style cond for render mode +;; -------------------------------------------------------------------------- + +(define-async async-render-cond-scheme + (fn (clauses env ctx) + (if (empty? clauses) + "" + (let ((clause (first clauses)) + (test (first clause)) + (body (nth clause 1))) + (if (or (and (= (type-of test) "symbol") + (or (= (symbol-name test) "else") + (= (symbol-name test) ":else"))) + (and (= (type-of test) "keyword") + (= (keyword-name test) "else"))) + (async-render body env ctx) + (if (async-eval test env ctx) + (async-render body env ctx) + (async-render-cond-scheme (rest clauses) env ctx))))))) + + +;; -------------------------------------------------------------------------- +;; async-render-cond-clojure — clojure-style cond for render mode +;; -------------------------------------------------------------------------- + +(define-async async-render-cond-clojure + (fn (clauses env ctx) + (if (< (len clauses) 2) + "" + (let ((test (first clauses)) + (body (nth clauses 1))) + (if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else")) + (and (= (type-of test) "symbol") + (or (= (symbol-name test) "else") + (= (symbol-name test) ":else")))) + (async-render body env ctx) + (if (async-eval test env ctx) + (async-render body env ctx) + (async-render-cond-clojure (slice clauses 2) env ctx))))))) + + +;; -------------------------------------------------------------------------- +;; async-process-bindings — evaluate let-bindings asynchronously +;; -------------------------------------------------------------------------- + +(define-async async-process-bindings + (fn (bindings env ctx) + ;; env-extend (not merge) — Env is not a dict subclass, so merge() + ;; returns an empty dict, losing all parent scope bindings. + (let ((local (env-extend env))) + (if (and (= (type-of bindings) "list") (not (empty? bindings))) + (if (= (type-of (first bindings)) "list") + ;; Scheme-style: ((name val) ...) + (for-each + (fn (pair) + (when (and (= (type-of pair) "list") (>= (len pair) 2)) + (let ((name (if (= (type-of (first pair)) "symbol") + (symbol-name (first pair)) + (str (first pair))))) + (env-set! local name (async-eval (nth pair 1) local ctx))))) + bindings) + ;; Clojure-style: (name val name val ...) + (async-process-bindings-flat bindings local ctx))) + local))) + + +(define-async async-process-bindings-flat + (fn (bindings local ctx) + (let ((skip false) + (i 0)) + (for-each + (fn (item) + (if skip + (do (set! skip false) + (set! i (inc i))) + (do + (let ((name (if (= (type-of item) "symbol") + (symbol-name item) + (str item)))) + (when (< (inc i) (len bindings)) + (env-set! local name + (async-eval (nth bindings (inc i)) local ctx)))) + (set! skip true) + (set! i (inc i))))) + bindings)))) + + +;; -------------------------------------------------------------------------- +;; async-map-fn-render — map a lambda/callable over collection for render +;; -------------------------------------------------------------------------- + +(define-async async-map-fn-render + (fn (f coll env ctx) + (let ((results (list))) + (for-each + (fn (item) + (if (lambda? f) + (append! results (async-render-lambda f (list item) env ctx)) + (let ((r (async-invoke f item))) + (append! results (async-render r env ctx))))) + coll) + results))) + + +;; -------------------------------------------------------------------------- +;; async-map-indexed-fn-render — map-indexed variant for render +;; -------------------------------------------------------------------------- + +(define-async async-map-indexed-fn-render + (fn (f coll env ctx) + (let ((results (list)) + (i 0)) + (for-each + (fn (item) + (if (lambda? f) + (append! results (async-render-lambda f (list i item) env ctx)) + (let ((r (async-invoke f i item))) + (append! results (async-render r env ctx)))) + (set! i (inc i))) + coll) + results))) + + +;; -------------------------------------------------------------------------- +;; async-invoke — call a native callable, await if coroutine +;; -------------------------------------------------------------------------- + +(define-async async-invoke + (fn (f &rest args) + (let ((r (apply f args))) + (if (async-coroutine? r) + (async-await! r) + r)))) + + +;; ========================================================================== +;; Async SX wire format (aser) +;; ========================================================================== + +(define-async async-aser + (fn (expr env ctx) + (case (type-of expr) + "number" expr + "string" expr + "boolean" expr + "nil" nil + + "symbol" + (let ((name (symbol-name expr))) + (cond + (env-has? env name) (env-get env name) + (primitive? name) (get-primitive name) + (= name "true") true + (= name "false") false + (= name "nil") nil + :else (error (str "Undefined symbol: " name)))) + + "keyword" (keyword-name expr) + + "dict" (async-aser-dict expr env ctx) + + "list" + (if (empty? expr) + (list) + (async-aser-list expr env ctx)) + + :else expr))) + + +(define-async async-aser-dict + (fn (expr env ctx) + (let ((result (dict))) + (for-each + (fn (key) + (dict-set! result key (async-aser (dict-get expr key) env ctx))) + (keys expr)) + result))) + + +;; -------------------------------------------------------------------------- +;; async-aser-list — dispatch on list head for aser mode +;; -------------------------------------------------------------------------- + +(define-async async-aser-list + (fn (expr env ctx) + (let ((head (first expr)) + (args (rest expr))) + (if (not (= (type-of head) "symbol")) + ;; Non-symbol head + (if (or (lambda? head) (= (type-of head) "list")) + ;; Function/list call — eval fully + (async-aser-eval-call head args env ctx) + ;; Data list — aser each + (async-aser-map-list expr env ctx)) + + ;; Symbol head — dispatch + (let ((name (symbol-name head))) + (cond + ;; I/O primitive + (io-primitive? name) + (async-eval expr env ctx) + + ;; Fragment + (= name "<>") + (async-aser-fragment args env ctx) + + ;; raw! + (= name "raw!") + (async-aser-call "raw!" args env ctx) + + ;; html: prefix + (starts-with? name "html:") + (async-aser-call (slice name 5) args env ctx) + + ;; Component call (~name) + (starts-with? name "~") + (let ((val (if (env-has? env name) (env-get env name) nil))) + (cond + (macro? val) + (async-aser (trampoline (expand-macro val args env)) env ctx) + (and (component? val) + (or (expand-components?) + (= (component-affinity val) "server"))) + (async-aser-component val args env ctx) + :else + (async-aser-call name args env ctx))) + + ;; Special/HO forms + (or (async-aser-form? name)) + (if (and (contains? HTML_TAGS name) + (or (and (> (len expr) 1) (= (type-of (nth expr 1)) "keyword")) + (svg-context?))) + (async-aser-call name args env ctx) + (dispatch-async-aser-form name expr env ctx)) + + ;; HTML tag + (contains? HTML_TAGS name) + (async-aser-call name args env ctx) + + ;; Macro + (and (env-has? env name) (macro? (env-get env name))) + (async-aser (trampoline (expand-macro (env-get env name) args env)) env ctx) + + ;; Custom element + (and (> (index-of name "-") 0) + (> (len expr) 1) + (= (type-of (nth expr 1)) "keyword")) + (async-aser-call name args env ctx) + + ;; SVG context + (svg-context?) + (async-aser-call name args env ctx) + + ;; Fallback — function/lambda call + :else + (async-aser-eval-call head args env ctx))))))) + + +;; -------------------------------------------------------------------------- +;; async-aser-eval-call — evaluate a function call fully in aser mode +;; -------------------------------------------------------------------------- + +(define-async async-aser-eval-call + (fn (head args env ctx) + (let ((f (async-eval head env ctx)) + (evaled-args (async-eval-args args env ctx))) + (cond + (and (callable? f) (not (lambda? f)) (not (component? f))) + ;; apply directly — async-invoke takes &rest so passing a list + ;; would wrap it in another list + (let ((r (apply f evaled-args))) + (if (async-coroutine? r) (async-await! r) r)) + (lambda? f) + (let ((local (env-merge (lambda-closure f) env))) + (for-each-indexed + (fn (i p) (env-set! local p (nth evaled-args i))) + (lambda-params f)) + (async-aser (lambda-body f) local ctx)) + (component? f) + (async-aser-call (str "~" (component-name f)) args env ctx) + (island? f) + (async-aser-call (str "~" (component-name f)) args env ctx) + :else + (error (str "Not callable: " (inspect f))))))) + + +;; -------------------------------------------------------------------------- +;; async-eval-args — evaluate a list of args asynchronously +;; -------------------------------------------------------------------------- + +(define-async async-eval-args + (fn (args env ctx) + (let ((results (list))) + (for-each + (fn (a) (append! results (async-eval a env ctx))) + args) + results))) + + +;; -------------------------------------------------------------------------- +;; async-aser-map-list — aser each element of a list +;; -------------------------------------------------------------------------- + +(define-async async-aser-map-list + (fn (exprs env ctx) + (let ((results (list))) + (for-each + (fn (x) (append! results (async-aser x env ctx))) + exprs) + results))) + + +;; -------------------------------------------------------------------------- +;; async-aser-fragment — serialize (<> child1 child2 ...) in aser mode +;; -------------------------------------------------------------------------- + +(define-async async-aser-fragment + (fn (children env ctx) + (let ((parts (list))) + (for-each + (fn (c) + (let ((result (async-aser c env ctx))) + (if (= (type-of result) "list") + (for-each + (fn (item) + (when (not (nil? item)) + (append! parts (serialize item)))) + result) + (when (not (nil? result)) + (append! parts (serialize result)))))) + children) + (if (empty? parts) + (make-sx-expr "") + (make-sx-expr (str "(<> " (join " " parts) ")")))))) + + +;; -------------------------------------------------------------------------- +;; async-aser-component — expand component server-side in aser mode +;; -------------------------------------------------------------------------- + +(define-async async-aser-component + (fn (comp args env ctx) + (let ((kwargs (dict)) + (children (list))) + (async-parse-aser-kw-args args kwargs children env ctx) + (let ((local (env-merge (component-closure comp) env))) + (for-each + (fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil))) + (component-params comp)) + (when (component-has-children? comp) + (let ((child-parts (list))) + (for-each + (fn (c) + (let ((result (async-aser c env ctx))) + (if (list? result) + (for-each + (fn (item) + (when (not (nil? item)) + (append! child-parts (serialize item)))) + result) + (when (not (nil? result)) + (append! child-parts (serialize result)))))) + children) + (env-set! local "children" + (make-sx-expr (str "(<> " (join " " child-parts) ")"))))) + (async-aser (component-body comp) local ctx))))) + + +;; -------------------------------------------------------------------------- +;; async-parse-aser-kw-args — parse keyword args for aser mode +;; -------------------------------------------------------------------------- + +(define-async async-parse-aser-kw-args + (fn (args kwargs children env ctx) + (let ((skip false) + (i 0)) + (for-each + (fn (arg) + (if skip + (do (set! skip false) + (set! i (inc i))) + (if (and (= (type-of arg) "keyword") + (< (inc i) (len args))) + (let ((val (async-aser (nth args (inc i)) env ctx))) + (dict-set! kwargs (keyword-name arg) val) + (set! skip true) + (set! i (inc i))) + (do + (append! children arg) + (set! i (inc i)))))) + args)))) + + +;; -------------------------------------------------------------------------- +;; async-aser-call — serialize an SX call (tag or component) in aser mode +;; -------------------------------------------------------------------------- + +(define-async async-aser-call + (fn (name args env ctx) + (let ((token (if (or (= name "svg") (= name "math")) + (svg-context-set! true) + nil)) + (parts (list name)) + (skip false) + (i 0)) + (for-each + (fn (arg) + (if skip + (do (set! skip false) + (set! i (inc i))) + (if (and (= (type-of arg) "keyword") + (< (inc i) (len args))) + (let ((val (async-aser (nth args (inc i)) env ctx))) + (when (not (nil? val)) + (append! parts (str ":" (keyword-name arg))) + (if (= (type-of val) "list") + (let ((live (filter (fn (v) (not (nil? v))) val))) + (if (empty? live) + (append! parts "nil") + (let ((items (map serialize live))) + (if (some (fn (v) (sx-expr? v)) live) + (append! parts (str "(<> " (join " " items) ")")) + (append! parts (str "(list " (join " " items) ")")))))) + (append! parts (serialize val)))) + (set! skip true) + (set! i (inc i))) + (let ((result (async-aser arg env ctx))) + (when (not (nil? result)) + (if (= (type-of result) "list") + (for-each + (fn (item) + (when (not (nil? item)) + (append! parts (serialize item)))) + result) + (append! parts (serialize result)))) + (set! i (inc i)))))) + args) + (when token (svg-context-reset! token)) + (make-sx-expr (str "(" (join " " parts) ")"))))) + + +;; -------------------------------------------------------------------------- +;; Aser form classification +;; -------------------------------------------------------------------------- + +(define ASYNC_ASER_FORM_NAMES + (list "if" "when" "cond" "case" "and" "or" + "let" "let*" "lambda" "fn" + "define" "defcomp" "defmacro" "defstyle" + "defhandler" "defpage" "defquery" "defaction" + "begin" "do" "quote" "->" "set!" "defisland")) + +(define ASYNC_ASER_HO_NAMES + (list "map" "map-indexed" "filter" "for-each")) + +(define async-aser-form? + (fn (name) + (or (contains? ASYNC_ASER_FORM_NAMES name) + (contains? ASYNC_ASER_HO_NAMES name)))) + + +;; -------------------------------------------------------------------------- +;; dispatch-async-aser-form — evaluate special/HO forms in aser mode +;; -------------------------------------------------------------------------- +;; +;; Uses cond-scheme? from eval.sx (the FIXED version with every? check). + +(define-async dispatch-async-aser-form + (fn (name expr env ctx) + (let ((args (rest expr))) + (cond + ;; if + (= name "if") + (let ((cond-val (async-eval (first args) env ctx))) + (if cond-val + (async-aser (nth args 1) env ctx) + (if (> (len args) 2) + (async-aser (nth args 2) env ctx) + nil))) + + ;; when + (= name "when") + (if (not (async-eval (first args) env ctx)) + nil + (let ((result nil)) + (for-each + (fn (body) (set! result (async-aser body env ctx))) + (rest args)) + result)) + + ;; cond — uses cond-scheme? (every? check) + (= name "cond") + (if (cond-scheme? args) + (async-aser-cond-scheme args env ctx) + (async-aser-cond-clojure args env ctx)) + + ;; case + (= name "case") + (let ((match-val (async-eval (first args) env ctx))) + (async-aser-case-loop match-val (rest args) env ctx)) + + ;; let / let* + (or (= name "let") (= name "let*")) + (let ((local (async-process-bindings (first args) env ctx)) + (result nil)) + (for-each + (fn (body) (set! result (async-aser body local ctx))) + (rest args)) + result) + + ;; begin / do + (or (= name "begin") (= name "do")) + (let ((result nil)) + (for-each + (fn (body) (set! result (async-aser body env ctx))) + args) + result) + + ;; and — short-circuit via flag to avoid 'some' with async lambda + (= name "and") + (let ((result true) + (stop false)) + (for-each (fn (arg) + (when (not stop) + (set! result (async-eval arg env ctx)) + (when (not result) + (set! stop true)))) + args) + result) + + ;; or — short-circuit via flag to avoid 'some' with async lambda + (= name "or") + (let ((result false) + (stop false)) + (for-each (fn (arg) + (when (not stop) + (set! result (async-eval arg env ctx)) + (when result + (set! stop true)))) + args) + result) + + ;; lambda / fn + (or (= name "lambda") (= name "fn")) + (sf-lambda args env) + + ;; quote + (= name "quote") + (if (empty? args) nil (first args)) + + ;; -> thread-first + (= name "->") + (async-aser-thread-first args env ctx) + + ;; set! + (= name "set!") + (let ((value (async-eval (nth args 1) env ctx))) + (env-set! env (symbol-name (first args)) value) + value) + + ;; map + (= name "map") + (async-aser-ho-map args env ctx) + + ;; map-indexed + (= name "map-indexed") + (async-aser-ho-map-indexed args env ctx) + + ;; filter + (= name "filter") + (async-eval expr env ctx) + + ;; for-each + (= name "for-each") + (async-aser-ho-for-each args env ctx) + + ;; defisland — evaluate AND serialize + (= name "defisland") + (do (async-eval expr env ctx) + (serialize expr)) + + ;; Definition forms — evaluate for side effects + (or (= name "define") (= name "defcomp") (= name "defmacro") + (= name "defstyle") (= name "defhandler") (= name "defpage") + (= name "defquery") (= name "defaction")) + (do (async-eval expr env ctx) nil) + + ;; Fallback + :else + (async-eval expr env ctx))))) + + +;; -------------------------------------------------------------------------- +;; async-aser-cond-scheme — scheme-style cond for aser mode +;; -------------------------------------------------------------------------- + +(define-async async-aser-cond-scheme + (fn (clauses env ctx) + (if (empty? clauses) + nil + (let ((clause (first clauses)) + (test (first clause)) + (body (nth clause 1))) + (if (or (and (= (type-of test) "symbol") + (or (= (symbol-name test) "else") + (= (symbol-name test) ":else"))) + (and (= (type-of test) "keyword") + (= (keyword-name test) "else"))) + (async-aser body env ctx) + (if (async-eval test env ctx) + (async-aser body env ctx) + (async-aser-cond-scheme (rest clauses) env ctx))))))) + + +;; -------------------------------------------------------------------------- +;; async-aser-cond-clojure — clojure-style cond for aser mode +;; -------------------------------------------------------------------------- + +(define-async async-aser-cond-clojure + (fn (clauses env ctx) + (if (< (len clauses) 2) + nil + (let ((test (first clauses)) + (body (nth clauses 1))) + (if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else")) + (and (= (type-of test) "symbol") + (or (= (symbol-name test) "else") + (= (symbol-name test) ":else")))) + (async-aser body env ctx) + (if (async-eval test env ctx) + (async-aser body env ctx) + (async-aser-cond-clojure (slice clauses 2) env ctx))))))) + + +;; -------------------------------------------------------------------------- +;; async-aser-case-loop — case dispatch for aser mode +;; -------------------------------------------------------------------------- + +(define-async async-aser-case-loop + (fn (match-val clauses env ctx) + (if (< (len clauses) 2) + nil + (let ((test (first clauses)) + (body (nth clauses 1))) + (if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else")) + (and (= (type-of test) "symbol") + (or (= (symbol-name test) ":else") + (= (symbol-name test) "else")))) + (async-aser body env ctx) + (if (= match-val (async-eval test env ctx)) + (async-aser body env ctx) + (async-aser-case-loop match-val (slice clauses 2) env ctx))))))) + + +;; -------------------------------------------------------------------------- +;; async-aser-thread-first — -> form in aser mode +;; -------------------------------------------------------------------------- + +(define-async async-aser-thread-first + (fn (args env ctx) + (let ((result (async-eval (first args) env ctx))) + (for-each + (fn (form) + (if (= (type-of form) "list") + (let ((f (async-eval (first form) env ctx)) + (fn-args (cons result + (async-eval-args (rest form) env ctx)))) + (set! result (async-invoke-or-lambda f fn-args env ctx))) + (let ((f (async-eval form env ctx))) + (set! result (async-invoke-or-lambda f (list result) env ctx))))) + (rest args)) + result))) + + +;; -------------------------------------------------------------------------- +;; async-invoke-or-lambda — invoke a callable or lambda with args +;; -------------------------------------------------------------------------- + +(define-async async-invoke-or-lambda + (fn (f args env ctx) + (cond + (and (callable? f) (not (lambda? f)) (not (component? f))) + (let ((r (apply f args))) + (if (async-coroutine? r) + (async-await! r) + r)) + (lambda? f) + (let ((local (env-merge (lambda-closure f) env))) + (for-each-indexed + (fn (i p) (env-set! local p (nth args i))) + (lambda-params f)) + (async-eval (lambda-body f) local ctx)) + :else + (error (str "-> form not callable: " (inspect f)))))) + + +;; -------------------------------------------------------------------------- +;; Async aser HO forms (map, map-indexed, for-each) +;; -------------------------------------------------------------------------- + +(define-async async-aser-ho-map + (fn (args env ctx) + (let ((f (async-eval (first args) env ctx)) + (coll (async-eval (nth args 1) env ctx)) + (results (list))) + (for-each + (fn (item) + (if (lambda? f) + (let ((local (env-merge (lambda-closure f) env))) + (env-set! local (first (lambda-params f)) item) + (append! results (async-aser (lambda-body f) local ctx))) + (append! results (async-invoke f item)))) + coll) + results))) + + +(define-async async-aser-ho-map-indexed + (fn (args env ctx) + (let ((f (async-eval (first args) env ctx)) + (coll (async-eval (nth args 1) env ctx)) + (results (list)) + (i 0)) + (for-each + (fn (item) + (if (lambda? f) + (let ((local (env-merge (lambda-closure f) env))) + (env-set! local (first (lambda-params f)) i) + (env-set! local (nth (lambda-params f) 1) item) + (append! results (async-aser (lambda-body f) local ctx))) + (append! results (async-invoke f i item))) + (set! i (inc i))) + coll) + results))) + + +(define-async async-aser-ho-for-each + (fn (args env ctx) + (let ((f (async-eval (first args) env ctx)) + (coll (async-eval (nth args 1) env ctx)) + (results (list))) + (for-each + (fn (item) + (if (lambda? f) + (let ((local (env-merge (lambda-closure f) env))) + (env-set! local (first (lambda-params f)) item) + (append! results (async-aser (lambda-body f) local ctx))) + (append! results (async-invoke f item)))) + coll) + results))) + + +;; -------------------------------------------------------------------------- +;; async-eval-slot-inner — server-side slot expansion for aser mode +;; -------------------------------------------------------------------------- +;; +;; Coordinates component expansion for server-rendered pages: +;; 1. If expression is a direct component call (~name ...), expand it +;; 2. Otherwise aser the expression, then check if result is a (~...) +;; call that should be re-expanded +;; +;; Platform primitives required: +;; (sx-parse src) — parse SX source string +;; (make-sx-expr s) — wrap as SxExpr +;; (sx-expr? x) — check if SxExpr +;; (set-expand-components!) — enable component expansion context var + +(define-async async-eval-slot-inner + (fn (expr env ctx) + ;; NOTE: Uses statement-form let + set! to avoid expression-context + ;; let (IIFE lambdas) which can't contain await in Python. + (let ((result nil)) + (if (and (list? expr) (not (empty? expr))) + (let ((head (first expr))) + (if (and (= (type-of head) "symbol") + (starts-with? (symbol-name head) "~")) + (let ((name (symbol-name head)) + (val (if (env-has? env name) (env-get env name) nil))) + (if (component? val) + (set! result (async-aser-component val (rest expr) env ctx)) + (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx)))) + (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx)))) + (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx))) + ;; Normalize result to SxExpr + (if (sx-expr? result) + result + (if (nil? result) + (make-sx-expr "") + (if (string? result) + (make-sx-expr result) + (make-sx-expr (serialize result)))))))) + + +(define-async async-maybe-expand-result + (fn (result env ctx) + ;; If the aser result is a component call string like "(~foo ...)", + ;; re-parse and expand it. This handles indirect component references + ;; (e.g. a let binding that evaluates to a component call). + (let ((raw (if (sx-expr? result) + (trim (str result)) + (if (string? result) + (trim result) + nil)))) + (if (and raw (starts-with? raw "(~")) + (let ((parsed (sx-parse raw))) + (if (and parsed (not (empty? parsed))) + (async-eval-slot-inner (first parsed) env ctx) + result)) + result)))) + + +;; -------------------------------------------------------------------------- +;; Platform interface — async adapter +;; -------------------------------------------------------------------------- +;; +;; Async evaluation (provided by platform): +;; (async-eval expr env ctx) — evaluate with I/O interception +;; (execute-io name args kw ctx) — execute I/O primitive +;; (io-primitive? name) — check if name is I/O primitive +;; +;; From eval.sx: +;; cond-scheme?, eval-cond-scheme, eval-cond-clojure +;; eval-expr, trampoline, expand-macro, sf-lambda +;; env-has?, env-get, env-set!, env-merge +;; lambda?, component?, island?, macro?, callable? +;; lambda-closure, lambda-params, lambda-body +;; component-params, component-body, component-closure, +;; component-has-children?, component-name +;; inspect +;; +;; From render.sx: +;; HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS +;; render-attrs, definition-form?, cond-scheme? +;; escape-html, escape-attr, raw-html-content +;; +;; From adapter-html.sx: +;; serialize-island-state +;; +;; Context management (platform): +;; (expand-components?) — check if component expansion is enabled +;; (svg-context?) — check if in SVG context +;; (svg-context-set! val) — set SVG context (returns reset token) +;; (svg-context-reset! token) — reset SVG context +;; (css-class-collect! val) — collect CSS classes +;; +;; Raw HTML: +;; (is-raw-html? x) — check if raw HTML marker +;; (make-raw-html s) — wrap string as raw HTML +;; (raw-html-content x) — unwrap raw HTML +;; +;; SxExpr: +;; (make-sx-expr s) — wrap as SxExpr (wire format string) +;; (sx-expr? x) — check if SxExpr +;; +;; Async primitives: +;; (async-coroutine? x) — check if value is a coroutine +;; (async-await! x) — await a coroutine +;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/adapter-html.sx b/shared/sx/ref/adapter-html.sx index 039090e7..f4719e22 100644 --- a/shared/sx/ref/adapter-html.sx +++ b/shared/sx/ref/adapter-html.sx @@ -433,11 +433,11 @@ ;; Render the island body as HTML (let ((body-html (render-to-html (component-body island) local)) - (state-json (serialize-island-state kwargs))) + (state-sx (serialize-island-state kwargs))) ;; Wrap in container with hydration attributes (str "" body-html @@ -445,17 +445,17 @@ ;; -------------------------------------------------------------------------- -;; serialize-island-state — serialize kwargs to JSON for hydration +;; serialize-island-state — serialize kwargs to SX for hydration ;; -------------------------------------------------------------------------- ;; -;; Only serializes simple values (numbers, strings, booleans, nil, lists, dicts). -;; Functions, components, and other non-serializable values are skipped. +;; Uses the SX serializer (not JSON) so the client can parse with sx-parse. +;; Handles all SX types natively: numbers, strings, booleans, nil, lists, dicts. (define serialize-island-state (fn (kwargs) (if (empty-dict? kwargs) nil - (json-serialize kwargs)))) + (sx-serialize kwargs)))) ;; -------------------------------------------------------------------------- @@ -476,8 +476,8 @@ ;; Raw HTML construction: ;; (make-raw-html s) → wrap string as raw HTML (not double-escaped) ;; -;; JSON serialization (for island state): -;; (json-serialize dict) → JSON string +;; Island state serialization: +;; (sx-serialize val) → SX source string (from parser.sx) ;; (empty-dict? d) → boolean ;; (escape-attr s) → HTML attribute escape ;; diff --git a/shared/sx/ref/adapter-sx.sx b/shared/sx/ref/adapter-sx.sx index b045faa3..5bc388f0 100644 --- a/shared/sx/ref/adapter-sx.sx +++ b/shared/sx/ref/adapter-sx.sx @@ -106,35 +106,56 @@ (define aser-fragment (fn (children env) ;; Serialize (<> child1 child2 ...) to sx source string - (let ((parts (filter - (fn (x) (not (nil? x))) - (map (fn (c) (aser c env)) children)))) + ;; Must flatten list results (e.g. from map/filter) to avoid nested parens + (let ((parts (list))) + (for-each + (fn (c) + (let ((result (aser c env))) + (if (= (type-of result) "list") + (for-each + (fn (item) + (when (not (nil? item)) + (append! parts (serialize item)))) + result) + (when (not (nil? result)) + (append! parts (serialize result)))))) + children) (if (empty? parts) "" - (str "(<> " (join " " (map serialize parts)) ")"))))) + (str "(<> " (join " " parts) ")"))))) (define aser-call (fn (name args env) ;; Serialize (name :key val child ...) — evaluate args but keep as sx - (let ((parts (list name))) - (reduce - (fn (state arg) - (let ((skip (get state "skip"))) - (if skip - (assoc state "skip" false "i" (inc (get state "i"))) - (if (and (= (type-of arg) "keyword") - (< (inc (get state "i")) (len args))) - (let ((val (aser (nth args (inc (get state "i"))) env))) - (when (not (nil? val)) - (append! parts (str ":" (keyword-name arg))) - (append! parts (serialize val))) - (assoc state "skip" true "i" (inc (get state "i")))) - (let ((val (aser arg env))) - (when (not (nil? val)) - (append! parts (serialize val))) - (assoc state "i" (inc (get state "i")))))))) - (dict "i" 0 "skip" false) + ;; Uses for-each + mutable state (not reduce) so bootstrapper emits for-loops + ;; that can contain nested for-each for list flattening. + (let ((parts (list name)) + (skip false) + (i 0)) + (for-each + (fn (arg) + (if skip + (do (set! skip false) + (set! i (inc i))) + (if (and (= (type-of arg) "keyword") + (< (inc i) (len args))) + (let ((val (aser (nth args (inc i)) env))) + (when (not (nil? val)) + (append! parts (str ":" (keyword-name arg))) + (append! parts (serialize val))) + (set! skip true) + (set! i (inc i))) + (let ((val (aser arg env))) + (when (not (nil? val)) + (if (= (type-of val) "list") + (for-each + (fn (item) + (when (not (nil? item)) + (append! parts (serialize item)))) + val) + (append! parts (serialize val)))) + (set! i (inc i)))))) args) (str "(" (join " " parts) ")")))) diff --git a/shared/sx/ref/async_eval_ref.py b/shared/sx/ref/async_eval_ref.py index af9c9037..96a79d56 100644 --- a/shared/sx/ref/async_eval_ref.py +++ b/shared/sx/ref/async_eval_ref.py @@ -1,993 +1,22 @@ -"""Async evaluation wrapper for the transpiled reference evaluator. +"""Async evaluation — thin re-export from bootstrapped sx_ref.py. -Wraps the sync sx_ref.py evaluator with async I/O support, mirroring -the hand-written async_eval.py. Provides the same public API: +The async adapter (adapter-async.sx) is now bootstrapped directly into +sx_ref.py alongside the sync evaluator. This file re-exports the public +API so existing imports keep working. - async_eval() — evaluate with I/O primitives - async_render() — render to HTML with I/O - async_eval_to_sx() — evaluate to SX wire format with I/O - async_eval_slot_to_sx() — expand components server-side, then serialize +All async rendering, serialization, and evaluation logic lives in the spec: + - shared/sx/ref/adapter-async.sx (canonical SX source) + - shared/sx/ref/sx_ref.py (bootstrapped Python) -The sync transpiled evaluator handles all control flow, special forms, -and lambda/component dispatch. This wrapper adds: - - - RequestContext threading - - I/O primitive interception (query, service, request-arg, etc.) - - Async trampoline for thunks - - SxExpr wrapping for wire format output - -DO NOT EDIT by hand — this is a thin wrapper; the actual eval logic -lives in sx_ref.py (generated) and the I/O primitives in primitives_io.py. +Platform async primitives (I/O dispatch, context vars, RequestContext) +are in shared/sx/ref/platform_py.py → PLATFORM_ASYNC_PY. """ -from __future__ import annotations - -import contextvars -import inspect -from typing import Any - -from ..types import Component, Keyword, Lambda, Macro, NIL, Symbol -from ..parser import SxExpr, serialize -from ..primitives_io import IO_PRIMITIVES, RequestContext, execute_io -from ..html import ( - HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, - escape_text, escape_attr, _RawHTML, css_class_collector, _svg_context, -) - from . import sx_ref -# Re-export EvalError from sx_ref +# Re-export the public API used by handlers.py, helpers.py, pages.py, etc. EvalError = sx_ref.EvalError - -# When True, _aser expands known components server-side -_expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar( - "_expand_components_ref", default=False -) - - -# --------------------------------------------------------------------------- -# Async TCO -# --------------------------------------------------------------------------- - -class _AsyncThunk: - __slots__ = ("expr", "env", "ctx") - def __init__(self, expr, env, ctx): - self.expr = expr - self.env = env - self.ctx = ctx - - -async def _async_trampoline(val): - while isinstance(val, _AsyncThunk): - val = await _async_eval(val.expr, val.env, val.ctx) - return val - - -# --------------------------------------------------------------------------- -# Async evaluate — wraps transpiled sync eval with I/O support -# --------------------------------------------------------------------------- - -async def async_eval(expr, env, ctx=None): - """Public entry point: evaluate with I/O primitives.""" - if ctx is None: - ctx = RequestContext() - result = await _async_eval(expr, env, ctx) - while isinstance(result, _AsyncThunk): - result = await _async_eval(result.expr, result.env, result.ctx) - return result - - -async def _async_eval(expr, env, ctx): - """Internal async evaluator. Intercepts I/O primitives, - delegates everything else to the sync transpiled evaluator.""" - # Intercept I/O primitive calls - if isinstance(expr, list) and expr: - head = expr[0] - if isinstance(head, Symbol) and head.name in IO_PRIMITIVES: - args, kwargs = await _parse_io_args(expr[1:], env, ctx) - return await execute_io(head.name, args, kwargs, ctx) - - # Check if this is a render expression (HTML tag, component, fragment) - # so we can wrap the result in _RawHTML to prevent double-escaping. - # The sync evaluator returns plain strings from render_list_to_html; - # the async renderer would HTML-escape those without this wrapper. - is_render = isinstance(expr, list) and sx_ref.is_render_expr(expr) - - # For everything else, use the sync transpiled evaluator - result = sx_ref.eval_expr(expr, env) - result = sx_ref.trampoline(result) - - if is_render and isinstance(result, str): - return _RawHTML(result) - return result - - -async def _parse_io_args(exprs, env, ctx): - """Parse and evaluate I/O node args (keyword + positional).""" - args = [] - kwargs = {} - i = 0 - while i < len(exprs): - item = exprs[i] - if isinstance(item, Keyword) and i + 1 < len(exprs): - kwargs[item.name] = await async_eval(exprs[i + 1], env, ctx) - i += 2 - else: - args.append(await async_eval(item, env, ctx)) - i += 1 - return args, kwargs - - -# --------------------------------------------------------------------------- -# Async HTML renderer -# --------------------------------------------------------------------------- - -async def async_render(expr, env, ctx=None): - """Render to HTML, awaiting I/O primitives inline.""" - if ctx is None: - ctx = RequestContext() - return await _arender(expr, env, ctx) - - -async def _arender(expr, env, ctx): - if expr is None or expr is NIL or expr is False or expr is True: - return "" - if isinstance(expr, _RawHTML): - return expr.html - # Also handle sx_ref._RawHTML from the sync evaluator - if isinstance(expr, sx_ref._RawHTML): - return expr.html - if isinstance(expr, str): - return escape_text(expr) - if isinstance(expr, (int, float)): - return escape_text(str(expr)) - if isinstance(expr, Symbol): - val = await async_eval(expr, env, ctx) - return await _arender(val, env, ctx) - if isinstance(expr, Keyword): - return escape_text(expr.name) - if isinstance(expr, list): - if not expr: - return "" - return await _arender_list(expr, env, ctx) - if isinstance(expr, dict): - return "" - return escape_text(str(expr)) - - -async def _arender_list(expr, env, ctx): - head = expr[0] - if isinstance(head, Symbol): - name = head.name - - # I/O primitive - if name in IO_PRIMITIVES: - result = await async_eval(expr, env, ctx) - return await _arender(result, env, ctx) - - # raw! - if name == "raw!": - parts = [] - for arg in expr[1:]: - val = await async_eval(arg, env, ctx) - if isinstance(val, _RawHTML): - parts.append(val.html) - elif isinstance(val, str): - parts.append(val) - elif val is not None and val is not NIL: - parts.append(str(val)) - return "".join(parts) - - # Fragment - if name == "<>": - parts = [await _arender(c, env, ctx) for c in expr[1:]] - return "".join(parts) - - # html: prefix - if name.startswith("html:"): - return await _arender_element(name[5:], expr[1:], env, ctx) - - # Render-aware special forms - arsf = _ASYNC_RENDER_FORMS.get(name) - if arsf is not None: - if name in HTML_TAGS and ( - (len(expr) > 1 and isinstance(expr[1], Keyword)) - or _svg_context.get(False) - ): - return await _arender_element(name, expr[1:], env, ctx) - return await arsf(expr, env, ctx) - - # Macro expansion - if name in env: - val = env[name] - if isinstance(val, Macro): - expanded = sx_ref.trampoline( - sx_ref.expand_macro(val, expr[1:], env) - ) - return await _arender(expanded, env, ctx) - - # HTML tag - if name in HTML_TAGS: - return await _arender_element(name, expr[1:], env, ctx) - - # Component - if name.startswith("~"): - val = env.get(name) - if isinstance(val, Component): - return await _arender_component(val, expr[1:], env, ctx) - - # Custom element - if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword): - return await _arender_element(name, expr[1:], env, ctx) - - # SVG context - if _svg_context.get(False): - return await _arender_element(name, expr[1:], env, ctx) - - # Fallback — evaluate then render - result = await async_eval(expr, env, ctx) - return await _arender(result, env, ctx) - - if isinstance(head, (Lambda, list)): - result = await async_eval(expr, env, ctx) - return await _arender(result, env, ctx) - - # Data list - parts = [await _arender(item, env, ctx) for item in expr] - return "".join(parts) - - -async def _arender_element(tag, args, env, ctx): - attrs = {} - children = [] - i = 0 - while i < len(args): - arg = args[i] - if isinstance(arg, Keyword) and i + 1 < len(args): - attrs[arg.name] = await async_eval(args[i + 1], env, ctx) - i += 2 - else: - children.append(arg) - i += 1 - - class_val = attrs.get("class") - if class_val is not None and class_val is not NIL and class_val is not False: - collector = css_class_collector.get(None) - if collector is not None: - collector.update(str(class_val).split()) - - parts = [f"<{tag}"] - for attr_name, attr_val in attrs.items(): - if attr_val is None or attr_val is NIL or attr_val is False: - continue - if attr_name in BOOLEAN_ATTRS: - if attr_val: - parts.append(f" {attr_name}") - elif attr_val is True: - parts.append(f" {attr_name}") - else: - parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"') - parts.append(">") - opening = "".join(parts) - - if tag in VOID_ELEMENTS: - return opening - - token = None - if tag in ("svg", "math"): - token = _svg_context.set(True) - try: - child_parts = [await _arender(c, env, ctx) for c in children] - finally: - if token is not None: - _svg_context.reset(token) - - return f"{opening}{''.join(child_parts)}" - - -async def _arender_component(comp, args, env, ctx): - kwargs = {} - children = [] - i = 0 - while i < len(args): - arg = args[i] - if isinstance(arg, Keyword) and i + 1 < len(args): - kwargs[arg.name] = await async_eval(args[i + 1], env, ctx) - i += 2 - else: - children.append(arg) - i += 1 - local = dict(comp.closure) - local.update(env) - for p in comp.params: - local[p] = kwargs.get(p, NIL) - if comp.has_children: - child_html = [await _arender(c, env, ctx) for c in children] - local["children"] = _RawHTML("".join(child_html)) - return await _arender(comp.body, local, ctx) - - -async def _arender_lambda(fn, args, env, ctx): - local = dict(fn.closure) - local.update(env) - for p, v in zip(fn.params, args): - local[p] = v - return await _arender(fn.body, local, ctx) - - -# --------------------------------------------------------------------------- -# Render-aware special forms -# --------------------------------------------------------------------------- - -async def _arsf_if(expr, env, ctx): - cond = await async_eval(expr[1], env, ctx) - if cond and cond is not NIL: - return await _arender(expr[2], env, ctx) - return await _arender(expr[3], env, ctx) if len(expr) > 3 else "" - - -async def _arsf_when(expr, env, ctx): - cond = await async_eval(expr[1], env, ctx) - if cond and cond is not NIL: - return "".join([await _arender(b, env, ctx) for b in expr[2:]]) - return "" - - -async def _arsf_cond(expr, env, ctx): - clauses = expr[1:] - if not clauses: - return "" - if isinstance(clauses[0], list) and len(clauses[0]) == 2: - for clause in clauses: - test = clause[0] - if isinstance(test, Symbol) and test.name in ("else", ":else"): - return await _arender(clause[1], env, ctx) - if isinstance(test, Keyword) and test.name == "else": - return await _arender(clause[1], env, ctx) - if await async_eval(test, env, ctx): - return await _arender(clause[1], env, ctx) - else: - i = 0 - while i < len(clauses) - 1: - test, result = clauses[i], clauses[i + 1] - if isinstance(test, Keyword) and test.name == "else": - return await _arender(result, env, ctx) - if isinstance(test, Symbol) and test.name in (":else", "else"): - return await _arender(result, env, ctx) - if await async_eval(test, env, ctx): - return await _arender(result, env, ctx) - i += 2 - return "" - - -async def _arsf_let(expr, env, ctx): - bindings = expr[1] - local = dict(env) - if isinstance(bindings, list): - if bindings and isinstance(bindings[0], list): - for b in bindings: - var = b[0] - vname = var.name if isinstance(var, Symbol) else var - local[vname] = await async_eval(b[1], local, ctx) - elif len(bindings) % 2 == 0: - for i in range(0, len(bindings), 2): - var = bindings[i] - vname = var.name if isinstance(var, Symbol) else var - local[vname] = await async_eval(bindings[i + 1], local, ctx) - return "".join([await _arender(b, local, ctx) for b in expr[2:]]) - - -async def _arsf_begin(expr, env, ctx): - return "".join([await _arender(sub, env, ctx) for sub in expr[1:]]) - - -async def _arsf_define(expr, env, ctx): - await async_eval(expr, env, ctx) - return "" - - -async def _arsf_map(expr, env, ctx): - fn = await async_eval(expr[1], env, ctx) - coll = await async_eval(expr[2], env, ctx) - parts = [] - for item in coll: - if isinstance(fn, Lambda): - parts.append(await _arender_lambda(fn, (item,), env, ctx)) - elif callable(fn): - r = fn(item) - if inspect.iscoroutine(r): - r = await r - parts.append(await _arender(r, env, ctx)) - else: - parts.append(await _arender(item, env, ctx)) - return "".join(parts) - - -async def _arsf_map_indexed(expr, env, ctx): - fn = await async_eval(expr[1], env, ctx) - coll = await async_eval(expr[2], env, ctx) - parts = [] - for i, item in enumerate(coll): - if isinstance(fn, Lambda): - parts.append(await _arender_lambda(fn, (i, item), env, ctx)) - elif callable(fn): - r = fn(i, item) - if inspect.iscoroutine(r): - r = await r - parts.append(await _arender(r, env, ctx)) - else: - parts.append(await _arender(item, env, ctx)) - return "".join(parts) - - -async def _arsf_filter(expr, env, ctx): - result = await async_eval(expr, env, ctx) - return await _arender(result, env, ctx) - - -async def _arsf_for_each(expr, env, ctx): - fn = await async_eval(expr[1], env, ctx) - coll = await async_eval(expr[2], env, ctx) - parts = [] - for item in coll: - if isinstance(fn, Lambda): - parts.append(await _arender_lambda(fn, (item,), env, ctx)) - elif callable(fn): - r = fn(item) - if inspect.iscoroutine(r): - r = await r - parts.append(await _arender(r, env, ctx)) - else: - parts.append(await _arender(item, env, ctx)) - return "".join(parts) - - -_ASYNC_RENDER_FORMS = { - "if": _arsf_if, - "when": _arsf_when, - "cond": _arsf_cond, - "let": _arsf_let, - "let*": _arsf_let, - "begin": _arsf_begin, - "do": _arsf_begin, - "define": _arsf_define, - "defstyle": _arsf_define, - "defcomp": _arsf_define, - "defmacro": _arsf_define, - "defhandler": _arsf_define, - "map": _arsf_map, - "map-indexed": _arsf_map_indexed, - "filter": _arsf_filter, - "for-each": _arsf_for_each, -} - - -# --------------------------------------------------------------------------- -# Async SX wire format (aser) -# --------------------------------------------------------------------------- - -async def async_eval_to_sx(expr, env, ctx=None): - """Evaluate and produce SX source string (wire format).""" - if ctx is None: - ctx = RequestContext() - result = await _aser(expr, env, ctx) - if isinstance(result, SxExpr): - return result - if result is None or result is NIL: - return SxExpr("") - if isinstance(result, str): - return SxExpr(result) - return SxExpr(serialize(result)) - - -async def async_eval_slot_to_sx(expr, env, ctx=None): - """Like async_eval_to_sx but expands component calls server-side.""" - if ctx is None: - ctx = RequestContext() - token = _expand_components.set(True) - try: - return await _eval_slot_inner(expr, env, ctx) - finally: - _expand_components.reset(token) - - -async def _eval_slot_inner(expr, env, ctx): - if isinstance(expr, list) and expr: - head = expr[0] - if isinstance(head, Symbol) and head.name.startswith("~"): - comp = env.get(head.name) - if isinstance(comp, Component): - result = await _aser_component(comp, expr[1:], env, ctx) - if isinstance(result, SxExpr): - return result - if result is None or result is NIL: - return SxExpr("") - if isinstance(result, str): - return SxExpr(result) - return SxExpr(serialize(result)) - result = await _aser(expr, env, ctx) - result = await _maybe_expand_component_result(result, env, ctx) - if isinstance(result, SxExpr): - return result - if result is None or result is NIL: - return SxExpr("") - if isinstance(result, str): - return SxExpr(result) - return SxExpr(serialize(result)) - - -async def _maybe_expand_component_result(result, env, ctx): - raw = None - if isinstance(result, SxExpr): - raw = str(result).strip() - elif isinstance(result, str): - raw = result.strip() - if raw and raw.startswith("(~"): - from ..parser import parse_all - parsed = parse_all(raw) - if parsed: - return await async_eval_slot_to_sx(parsed[0], env, ctx) - return result - - -async def _aser(expr, env, ctx): - """Evaluate for SX wire format — serialize rendering forms, evaluate control flow.""" - if isinstance(expr, (int, float, bool)): - return expr - if isinstance(expr, SxExpr): - return expr - if isinstance(expr, str): - return expr - if expr is None or expr is NIL: - return NIL - - if isinstance(expr, Symbol): - name = expr.name - if name in env: - return env[name] - if sx_ref.is_primitive(name): - return sx_ref.get_primitive(name) - if name == "true": - return True - if name == "false": - return False - if name == "nil": - return NIL - raise EvalError(f"Undefined symbol: {name}") - - if isinstance(expr, Keyword): - return expr.name - - if isinstance(expr, dict): - return {k: await _aser(v, env, ctx) for k, v in expr.items()} - - if not isinstance(expr, list): - return expr - if not expr: - return [] - - head = expr[0] - if not isinstance(head, (Symbol, Lambda, list)): - return [await _aser(x, env, ctx) for x in expr] - - if isinstance(head, Symbol): - name = head.name - - # I/O primitives - if name in IO_PRIMITIVES: - args, kwargs = await _parse_io_args(expr[1:], env, ctx) - return await execute_io(name, args, kwargs, ctx) - - # Fragment - if name == "<>": - return await _aser_fragment(expr[1:], env, ctx) - - # raw! - if name == "raw!": - return await _aser_call("raw!", expr[1:], env, ctx) - - # html: prefix - if name.startswith("html:"): - return await _aser_call(name[5:], expr[1:], env, ctx) - - # Component call - if name.startswith("~"): - val = env.get(name) - if isinstance(val, Macro): - expanded = sx_ref.trampoline( - sx_ref.expand_macro(val, expr[1:], env) - ) - return await _aser(expanded, env, ctx) - if isinstance(val, Component) and _expand_components.get(): - return await _aser_component(val, expr[1:], env, ctx) - return await _aser_call(name, expr[1:], env, ctx) - - # Serialize-mode special/HO forms - sf = _ASER_FORMS.get(name) - if sf is not None: - if name in HTML_TAGS and ( - (len(expr) > 1 and isinstance(expr[1], Keyword)) - or _svg_context.get(False) - ): - return await _aser_call(name, expr[1:], env, ctx) - return await sf(expr, env, ctx) - - # HTML tag - if name in HTML_TAGS: - return await _aser_call(name, expr[1:], env, ctx) - - # Macro - if name in env: - val = env[name] - if isinstance(val, Macro): - expanded = sx_ref.trampoline( - sx_ref.expand_macro(val, expr[1:], env) - ) - return await _aser(expanded, env, ctx) - - # Custom element - if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword): - return await _aser_call(name, expr[1:], env, ctx) - - # SVG context - if _svg_context.get(False): - return await _aser_call(name, expr[1:], env, ctx) - - # Function/lambda call - fn = await async_eval(head, env, ctx) - args = [await async_eval(a, env, ctx) for a in expr[1:]] - - if callable(fn) and not isinstance(fn, (Lambda, Component)): - result = fn(*args) - if inspect.iscoroutine(result): - return await result - return result - if isinstance(fn, Lambda): - local = dict(fn.closure) - local.update(env) - for p, v in zip(fn.params, args): - local[p] = v - return await _aser(fn.body, local, ctx) - if isinstance(fn, Component): - return await _aser_call(f"~{fn.name}", expr[1:], env, ctx) - raise EvalError(f"Not callable: {fn!r}") - - -async def _aser_fragment(children, env, ctx): - parts = [] - for child in children: - result = await _aser(child, env, ctx) - if isinstance(result, list): - for item in result: - if item is not NIL and item is not None: - parts.append(serialize(item)) - elif result is not NIL and result is not None: - parts.append(serialize(result)) - if not parts: - return SxExpr("") - return SxExpr("(<> " + " ".join(parts) + ")") - - -async def _aser_component(comp, args, env, ctx): - kwargs = {} - children = [] - i = 0 - while i < len(args): - arg = args[i] - if isinstance(arg, Keyword) and i + 1 < len(args): - kwargs[arg.name] = await _aser(args[i + 1], env, ctx) - i += 2 - else: - children.append(arg) - i += 1 - local = dict(comp.closure) - local.update(env) - for p in comp.params: - local[p] = kwargs.get(p, NIL) - if comp.has_children: - child_parts = [serialize(await _aser(c, env, ctx)) for c in children] - local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")") - return await _aser(comp.body, local, ctx) - - -async def _aser_call(name, args, env, ctx): - token = None - if name in ("svg", "math"): - token = _svg_context.set(True) - try: - parts = [name] - extra_class = None - i = 0 - while i < len(args): - arg = args[i] - if isinstance(arg, Keyword) and i + 1 < len(args): - val = await _aser(args[i + 1], env, ctx) - if val is not NIL and val is not None: - parts.append(f":{arg.name}") - if isinstance(val, list): - live = [v for v in val if v is not NIL and v is not None] - items = [serialize(v) for v in live] - if not items: - parts.append("nil") - elif any(isinstance(v, SxExpr) for v in live): - parts.append("(<> " + " ".join(items) + ")") - else: - parts.append("(list " + " ".join(items) + ")") - else: - parts.append(serialize(val)) - i += 2 - else: - result = await _aser(arg, env, ctx) - if result is not NIL and result is not None: - if isinstance(result, list): - for item in result: - if item is not NIL and item is not None: - parts.append(serialize(item)) - else: - parts.append(serialize(result)) - i += 1 - if extra_class: - _merge_class_into_parts(parts, extra_class) - return SxExpr("(" + " ".join(parts) + ")") - finally: - if token is not None: - _svg_context.reset(token) - - -def _merge_class_into_parts(parts, class_name): - for i, p in enumerate(parts): - if p == ":class" and i + 1 < len(parts): - existing = parts[i + 1] - if existing.startswith('"') and existing.endswith('"'): - parts[i + 1] = existing[:-1] + " " + class_name + '"' - else: - parts[i + 1] = f'(str {existing} " {class_name}")' - return - parts.insert(1, f'"{class_name}"') - parts.insert(1, ":class") - - -# --------------------------------------------------------------------------- -# Aser-mode special forms -# --------------------------------------------------------------------------- - -async def _assf_if(expr, env, ctx): - cond = await async_eval(expr[1], env, ctx) - if cond and cond is not NIL: - return await _aser(expr[2], env, ctx) - return await _aser(expr[3], env, ctx) if len(expr) > 3 else NIL - - -async def _assf_when(expr, env, ctx): - cond = await async_eval(expr[1], env, ctx) - if cond and cond is not NIL: - result = NIL - for body_expr in expr[2:]: - result = await _aser(body_expr, env, ctx) - return result - return NIL - - -async def _assf_let(expr, env, ctx): - bindings = expr[1] - local = dict(env) - if isinstance(bindings, list): - if bindings and isinstance(bindings[0], list): - for b in bindings: - var = b[0] - vname = var.name if isinstance(var, Symbol) else var - local[vname] = await _aser(b[1], local, ctx) - elif len(bindings) % 2 == 0: - for i in range(0, len(bindings), 2): - var = bindings[i] - vname = var.name if isinstance(var, Symbol) else var - local[vname] = await _aser(bindings[i + 1], local, ctx) - result = NIL - for body_expr in expr[2:]: - result = await _aser(body_expr, local, ctx) - return result - - -async def _assf_cond(expr, env, ctx): - clauses = expr[1:] - if not clauses: - return NIL - if isinstance(clauses[0], list) and len(clauses[0]) == 2: - for clause in clauses: - test = clause[0] - if isinstance(test, Symbol) and test.name in ("else", ":else"): - return await _aser(clause[1], env, ctx) - if isinstance(test, Keyword) and test.name == "else": - return await _aser(clause[1], env, ctx) - if await async_eval(test, env, ctx): - return await _aser(clause[1], env, ctx) - else: - i = 0 - while i < len(clauses) - 1: - test, result = clauses[i], clauses[i + 1] - if isinstance(test, Keyword) and test.name == "else": - return await _aser(result, env, ctx) - if isinstance(test, Symbol) and test.name in (":else", "else"): - return await _aser(result, env, ctx) - if await async_eval(test, env, ctx): - return await _aser(result, env, ctx) - i += 2 - return NIL - - -async def _assf_case(expr, env, ctx): - match_val = await async_eval(expr[1], env, ctx) - clauses = expr[2:] - i = 0 - while i < len(clauses) - 1: - test, result = clauses[i], clauses[i + 1] - if isinstance(test, Keyword) and test.name == "else": - return await _aser(result, env, ctx) - if isinstance(test, Symbol) and test.name in (":else", "else"): - return await _aser(result, env, ctx) - if match_val == await async_eval(test, env, ctx): - return await _aser(result, env, ctx) - i += 2 - return NIL - - -async def _assf_begin(expr, env, ctx): - result = NIL - for sub in expr[1:]: - result = await _aser(sub, env, ctx) - return result - - -async def _assf_define(expr, env, ctx): - await async_eval(expr, env, ctx) - return NIL - - -async def _assf_and(expr, env, ctx): - result = True - for arg in expr[1:]: - result = await async_eval(arg, env, ctx) - if not result: - return result - return result - - -async def _assf_or(expr, env, ctx): - result = False - for arg in expr[1:]: - result = await async_eval(arg, env, ctx) - if result: - return result - return result - - -async def _assf_lambda(expr, env, ctx): - params_expr = expr[1] - param_names = [] - for p in params_expr: - if isinstance(p, Symbol): - param_names.append(p.name) - elif isinstance(p, str): - param_names.append(p) - return Lambda(param_names, expr[2], dict(env)) - - -async def _assf_quote(expr, env, ctx): - return expr[1] if len(expr) > 1 else NIL - - -async def _assf_thread_first(expr, env, ctx): - result = await async_eval(expr[1], env, ctx) - for form in expr[2:]: - if isinstance(form, list): - fn = await async_eval(form[0], env, ctx) - fn_args = [result] + [await async_eval(a, env, ctx) for a in form[1:]] - else: - fn = await async_eval(form, env, ctx) - fn_args = [result] - if callable(fn) and not isinstance(fn, (Lambda, Component)): - result = fn(*fn_args) - if inspect.iscoroutine(result): - result = await result - elif isinstance(fn, Lambda): - local = dict(fn.closure) - local.update(env) - for p, v in zip(fn.params, fn_args): - local[p] = v - result = await async_eval(fn.body, local, ctx) - else: - raise EvalError(f"-> form not callable: {fn!r}") - return result - - -async def _assf_set_bang(expr, env, ctx): - value = await async_eval(expr[2], env, ctx) - env[expr[1].name] = value - return value - - -# Aser-mode HO forms - -async def _asho_map(expr, env, ctx): - fn = await async_eval(expr[1], env, ctx) - coll = await async_eval(expr[2], env, ctx) - results = [] - for item in coll: - if isinstance(fn, Lambda): - local = dict(fn.closure) - local.update(env) - local[fn.params[0]] = item - results.append(await _aser(fn.body, local, ctx)) - elif callable(fn): - r = fn(item) - results.append(await r if inspect.iscoroutine(r) else r) - else: - raise EvalError(f"map requires callable, got {type(fn).__name__}") - return results - - -async def _asho_map_indexed(expr, env, ctx): - fn = await async_eval(expr[1], env, ctx) - coll = await async_eval(expr[2], env, ctx) - results = [] - for i, item in enumerate(coll): - if isinstance(fn, Lambda): - local = dict(fn.closure) - local.update(env) - local[fn.params[0]] = i - local[fn.params[1]] = item - results.append(await _aser(fn.body, local, ctx)) - elif callable(fn): - r = fn(i, item) - results.append(await r if inspect.iscoroutine(r) else r) - else: - raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}") - return results - - -async def _asho_filter(expr, env, ctx): - return await async_eval(expr, env, ctx) - - -async def _asho_for_each(expr, env, ctx): - fn = await async_eval(expr[1], env, ctx) - coll = await async_eval(expr[2], env, ctx) - results = [] - for item in coll: - if isinstance(fn, Lambda): - local = dict(fn.closure) - local.update(env) - local[fn.params[0]] = item - results.append(await _aser(fn.body, local, ctx)) - elif callable(fn): - r = fn(item) - results.append(await r if inspect.iscoroutine(r) else r) - return results - - -_ASER_FORMS = { - "if": _assf_if, - "when": _assf_when, - "cond": _assf_cond, - "case": _assf_case, - "and": _assf_and, - "or": _assf_or, - "let": _assf_let, - "let*": _assf_let, - "lambda": _assf_lambda, - "fn": _assf_lambda, - "define": _assf_define, - "defstyle": _assf_define, - "defcomp": _assf_define, - "defmacro": _assf_define, - "defhandler": _assf_define, - "begin": _assf_begin, - "do": _assf_begin, - "quote": _assf_quote, - "->": _assf_thread_first, - "set!": _assf_set_bang, - "map": _asho_map, - "map-indexed": _asho_map_indexed, - "filter": _asho_filter, - "for-each": _asho_for_each, -} +async_eval = sx_ref.async_eval +async_render = sx_ref.async_render +async_eval_to_sx = sx_ref.async_eval_to_sx +async_eval_slot_to_sx = sx_ref.async_eval_slot_to_sx diff --git a/shared/sx/ref/boot.sx b/shared/sx/ref/boot.sx index afb9d55d..30a2e634 100644 --- a/shared/sx/ref/boot.sx +++ b/shared/sx/ref/boot.sx @@ -344,15 +344,15 @@ (define hydrate-island (fn (el) (let ((name (dom-get-attr el "data-sx-island")) - (state-json (or (dom-get-attr el "data-sx-state") "{}"))) + (state-sx (or (dom-get-attr el "data-sx-state") "{}"))) (let ((comp-name (str "~" name)) (env (get-render-env nil))) (let ((comp (env-get env comp-name))) (if (not (or (component? comp) (island? comp))) (log-warn (str "hydrate-island: unknown island " comp-name)) - ;; Parse state and build keyword args - (let ((kwargs (json-parse state-json)) + ;; Parse state and build keyword args — SX format, not JSON + (let ((kwargs (or (first (sx-parse state-sx)) {})) (disposers (list)) (local (env-merge (component-closure comp) env))) @@ -494,8 +494,8 @@ ;; (log-info msg) → void (console.log with prefix) ;; (log-parse-error label text err) → void (diagnostic parse error) ;; -;; === JSON === -;; (json-parse str) → dict/list/value (JSON.parse) +;; === Parsing (island state) === +;; (sx-parse str) → list of AST expressions (from parser.sx) ;; ;; === Processing markers === ;; (mark-processed! el key) → void diff --git a/shared/sx/ref/bootstrap_py.py b/shared/sx/ref/bootstrap_py.py index 20c2383b..4cd506b8 100644 --- a/shared/sx/ref/bootstrap_py.py +++ b/shared/sx/ref/bootstrap_py.py @@ -49,6 +49,8 @@ class PyEmitter: def __init__(self): self.indent = 0 + self._async_names: set[str] = set() # SX names of define-async functions + self._in_async: bool = False # Currently emitting async def body? def emit(self, expr) -> str: """Emit a Python expression from an SX AST node.""" @@ -80,6 +82,8 @@ class PyEmitter: name = head.name if name == "define": return self._emit_define(expr, indent) + if name == "define-async": + return self._emit_define_async(expr, indent) if name == "set!": return f"{pad}{self._mangle(expr[1].name)} = {self.emit(expr[2])}" if name == "when": @@ -275,6 +279,19 @@ class PyEmitter: "sf-defisland": "sf_defisland", # adapter-sx.sx "render-to-sx": "render_to_sx", + # adapter-async.sx platform primitives + "svg-context-set!": "svg_context_set", + "svg-context-reset!": "svg_context_reset", + "css-class-collect!": "css_class_collect", + "is-raw-html?": "is_raw_html", + "async-coroutine?": "is_async_coroutine", + "async-await!": "async_await", + "is-sx-expr?": "is_sx_expr", + "sx-expr?": "is_sx_expr", + "io-primitive?": "io_primitive_p", + "expand-components?": "expand_components_p", + "svg-context?": "svg_context_p", + "make-sx-expr": "make_sx_expr", "aser": "aser", "eval-case-aser": "eval_case_aser", "sx-serialize": "sx_serialize", @@ -417,6 +434,8 @@ class PyEmitter: # Regular function call fn_name = self._mangle(name) args = ", ".join(self.emit(x) for x in expr[1:]) + if self._in_async and name in self._async_names: + return f"(await {fn_name}({args}))" return f"{fn_name}({args})" # --- Special form emitters --- @@ -513,13 +532,16 @@ class PyEmitter: body_parts = expr[2:] lines = [f"{pad}if sx_truthy({cond}):"] for b in body_parts: - lines.append(self.emit_statement(b, indent + 1)) + self._emit_stmt_recursive(b, lines, indent + 1) return "\n".join(lines) def _emit_cond(self, expr) -> str: clauses = expr[1:] if not clauses: return "NIL" + # Check ALL clauses are 2-element lists (scheme-style). + # Checking only the first is ambiguous — (nil? x) is a 2-element + # function call, not a scheme clause ((test body)). is_scheme = ( all(isinstance(c, list) and len(c) == 2 for c in clauses) and not any(isinstance(c, Keyword) for c in clauses) @@ -642,6 +664,16 @@ class PyEmitter: val = self.emit(val_expr) return f"{pad}{self._mangle(name)} = {val}" + def _emit_define_async(self, expr, indent: int = 0) -> str: + """Emit a define-async form as an async def statement.""" + name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) + val_expr = expr[2] + if (isinstance(val_expr, list) and val_expr and + isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")): + return self._emit_define_as_def(name, val_expr, indent, is_async=True) + # Shouldn't happen — define-async should always wrap fn/lambda + return self._emit_define(expr, indent) + def _body_uses_set(self, fn_expr) -> bool: """Check if a fn expression's body (recursively) uses set!.""" def _has_set(node): @@ -654,12 +686,16 @@ class PyEmitter: body = fn_expr[2:] return any(_has_set(b) for b in body) - def _emit_define_as_def(self, name: str, fn_expr, indent: int = 0) -> str: + def _emit_define_as_def(self, name: str, fn_expr, indent: int = 0, + is_async: bool = False) -> str: """Emit a define with fn value as a proper def statement. This is used for functions that contain set! — Python closures can't rebind outer lambda params, so we need proper def + local variables. Variables mutated by set! from nested lambdas use a _cells dict. + + When is_async=True, emits 'async def' and sets _in_async so that + calls to other async functions receive 'await'. """ pad = " " * indent params = fn_expr[1] @@ -686,14 +722,19 @@ class PyEmitter: py_name = self._mangle(name) # Find set! target variables that are used from nested lambda scopes nested_set_vars = self._find_nested_set_vars(body) - lines = [f"{pad}def {py_name}({params_str}):"] + def_kw = "async def" if is_async else "def" + lines = [f"{pad}{def_kw} {py_name}({params_str}):"] if nested_set_vars: lines.append(f"{pad} _cells = {{}}") - # Emit body with cell var tracking + # Emit body with cell var tracking (and async context if needed) old_cells = getattr(self, '_current_cell_vars', set()) + old_async = self._in_async self._current_cell_vars = nested_set_vars + if is_async: + self._in_async = True self._emit_body_stmts(body, lines, indent + 1) self._current_cell_vars = old_cells + self._in_async = old_async return "\n".join(lines) def _find_nested_set_vars(self, body) -> set[str]: @@ -750,7 +791,7 @@ class PyEmitter: if is_last: self._emit_return_expr(expr, lines, indent) else: - lines.append(self.emit_statement(expr, indent)) + self._emit_stmt_recursive(expr, lines, indent) def _emit_return_expr(self, expr, lines: list, indent: int) -> None: """Emit an expression in return position, flattening control flow.""" @@ -775,6 +816,11 @@ class PyEmitter: if name in ("do", "begin"): self._emit_body_stmts(expr[1:], lines, indent) return + if name == "for-each": + # for-each in return position: emit as statement, return NIL + lines.append(self._emit_for_each_stmt(expr, indent)) + lines.append(f"{pad}return NIL") + return lines.append(f"{pad}return {self.emit(expr)}") def _emit_if_return(self, expr, lines: list, indent: int) -> None: @@ -1034,12 +1080,15 @@ class PyEmitter: # --------------------------------------------------------------------------- def extract_defines(source: str) -> list[tuple[str, list]]: - """Parse .sx source, return list of (name, define-expr) for top-level defines.""" + """Parse .sx source, return list of (name, define-expr) for top-level defines. + + Extracts both (define ...) and (define-async ...) forms. + """ exprs = parse_all(source) defines = [] for expr in exprs: if isinstance(expr, list) and expr and isinstance(expr[0], Symbol): - if expr[0].name == "define": + if expr[0].name in ("define", "define-async"): name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) defines.append((name, expr)) return defines @@ -1196,6 +1245,8 @@ def compile_ref_to_py( spec_mod_set.add("deps") if "signals" in SPEC_MODULES: spec_mod_set.add("signals") + if "page-helpers" in SPEC_MODULES: + spec_mod_set.add("page-helpers") has_deps = "deps" in spec_mod_set # Core files always included, then selected adapters, then spec modules @@ -1210,6 +1261,28 @@ def compile_ref_to_py( for name in sorted(spec_mod_set): sx_files.append(SPEC_MODULES[name]) + # Pre-scan define-async names (needed before transpilation so emitter + # knows which calls require 'await') + has_async = "async" in adapter_set + if has_async: + async_filename = ADAPTER_FILES["async"][0] + async_filepath = os.path.join(ref_dir, async_filename) + if os.path.exists(async_filepath): + with open(async_filepath) as f: + async_src = f.read() + for aexpr in parse_all(async_src): + if (isinstance(aexpr, list) and aexpr + and isinstance(aexpr[0], Symbol) + and aexpr[0].name == "define-async"): + aname = aexpr[1].name if isinstance(aexpr[1], Symbol) else str(aexpr[1]) + emitter._async_names.add(aname) + # Platform async primitives (provided by host, also need await) + emitter._async_names.update({ + "async-eval", "execute-io", "async-await!", + }) + # Async adapter is transpiled last (after sync adapters) + sx_files.append(ADAPTER_FILES["async"]) + all_sections = [] for filename, label in sx_files: filepath = os.path.join(ref_dir, filename) @@ -1246,6 +1319,9 @@ def compile_ref_to_py( if has_deps: parts.append(PLATFORM_DEPS_PY) + if has_async: + parts.append(PLATFORM_ASYNC_PY) + for label, defines in all_sections: parts.append(f"\n# === Transpiled from {label} ===\n") for name, expr in defines: @@ -1256,7 +1332,7 @@ def compile_ref_to_py( parts.append(FIXUPS_PY) if has_continuations: parts.append(CONTINUATIONS_PY) - parts.append(public_api_py(has_html, has_sx, has_deps)) + parts.append(public_api_py(has_html, has_sx, has_deps, has_async)) return "\n".join(parts) diff --git a/shared/sx/ref/bootstrap_test.py b/shared/sx/ref/bootstrap_test.py index 08eb64bc..0f1ef43c 100644 --- a/shared/sx/ref/bootstrap_test.py +++ b/shared/sx/ref/bootstrap_test.py @@ -143,7 +143,7 @@ def _emit_py(suites: list[dict], preamble: list) -> str: lines.append('') lines.append('import pytest') lines.append('from shared.sx.parser import parse_all') - lines.append('from shared.sx.evaluator import _eval, _trampoline') + lines.append('from shared.sx.ref.sx_ref import eval_expr as _eval, trampoline as _trampoline') lines.append('') lines.append('') lines.append(f"_PREAMBLE = '''{preamble_escaped}'''") diff --git a/shared/sx/ref/engine.sx b/shared/sx/ref/engine.sx index e626d56d..cdbfb140 100644 --- a/shared/sx/ref/engine.sx +++ b/shared/sx/ref/engine.sx @@ -34,11 +34,12 @@ (define parse-time (fn (s) ;; Parse time string: "2s" → 2000, "500ms" → 500 - (cond - (nil? s) 0 - (ends-with? s "ms") (parse-int s 0) - (ends-with? s "s") (* (parse-int (replace s "s" "") 0) 1000) - :else (parse-int s 0)))) + ;; Uses nested if (not cond) because cond misclassifies 2-element + ;; function calls like (nil? s) as scheme-style ((test body)) clauses. + (if (nil? s) 0 + (if (ends-with? s "ms") (parse-int s 0) + (if (ends-with? s "s") (* (parse-int (replace s "s" "") 0) 1000) + (parse-int s 0)))))) (define parse-trigger-spec @@ -219,20 +220,19 @@ ;; Filter form parameters by sx-params spec. ;; all-params is a list of (key value) pairs. ;; Returns filtered list of (key value) pairs. - (cond - (nil? params-spec) all-params - (= params-spec "none") (list) - (= params-spec "*") all-params - (starts-with? params-spec "not ") - (let ((excluded (map trim (split (slice params-spec 4) ",")))) - (filter - (fn (p) (not (contains? excluded (first p)))) - all-params)) - :else - (let ((allowed (map trim (split params-spec ",")))) - (filter - (fn (p) (contains? allowed (first p))) - all-params))))) + ;; Uses nested if (not cond) — see parse-time comment. + (if (nil? params-spec) all-params + (if (= params-spec "none") (list) + (if (= params-spec "*") all-params + (if (starts-with? params-spec "not ") + (let ((excluded (map trim (split (slice params-spec 4) ",")))) + (filter + (fn (p) (not (contains? excluded (first p)))) + all-params)) + (let ((allowed (map trim (split params-spec ",")))) + (filter + (fn (p) (contains? allowed (first p))) + all-params)))))))) ;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/eval.sx b/shared/sx/ref/eval.sx index 1aa55dbd..b8ea21fe 100644 --- a/shared/sx/ref/eval.sx +++ b/shared/sx/ref/eval.sx @@ -309,14 +309,18 @@ nil)))) +;; cond-scheme? — check if ALL clauses are 2-element lists (scheme-style). +;; Checking only the first arg is ambiguous — (nil? x) is a 2-element +;; function call, not a scheme clause ((test body)). +(define cond-scheme? + (fn (clauses) + (every? (fn (c) (and (= (type-of c) "list") (= (len c) 2))) + clauses))) + (define sf-cond (fn (args env) - ;; Detect scheme-style: first arg is a 2-element list - (if (and (= (type-of (first args)) "list") - (= (len (first args)) 2)) - ;; Scheme-style: ((test body) ...) + (if (cond-scheme? args) (sf-cond-scheme args env) - ;; Clojure-style: test body test body ... (sf-cond-clojure args env)))) (define sf-cond-scheme diff --git a/shared/sx/ref/js.sx b/shared/sx/ref/js.sx index cea6379b..3c35b73b 100644 --- a/shared/sx/ref/js.sx +++ b/shared/sx/ref/js.sx @@ -1290,8 +1290,9 @@ (= name "append!") (str (js-expr (nth expr 1)) ".push(" (js-expr (nth expr 2)) ");") (= name "env-set!") - (str (js-expr (nth expr 1)) "[" (js-expr (nth expr 2)) - "] = " (js-expr (nth expr 3)) ";") + (str "envSet(" (js-expr (nth expr 1)) + ", " (js-expr (nth expr 2)) + ", " (js-expr (nth expr 3)) ");") (= name "set-lambda-name!") (str (js-expr (nth expr 1)) ".name = " (js-expr (nth expr 2)) ";") :else diff --git a/shared/sx/ref/page-helpers.sx b/shared/sx/ref/page-helpers.sx new file mode 100644 index 00000000..69e2ba1c --- /dev/null +++ b/shared/sx/ref/page-helpers.sx @@ -0,0 +1,368 @@ +;; ========================================================================== +;; page-helpers.sx — Pure data-transformation page helpers +;; +;; These functions take raw data (from Python I/O edge) and return +;; structured dicts for page rendering. No I/O — pure transformations +;; only. Bootstrapped to every host. +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; categorize-special-forms +;; +;; Parses define-special-form declarations from special-forms.sx AST, +;; categorizes each form by name lookup, returns dict of category → forms. +;; -------------------------------------------------------------------------- + +(define special-form-category-map + {"if" "Control Flow" "when" "Control Flow" "cond" "Control Flow" + "case" "Control Flow" "and" "Control Flow" "or" "Control Flow" + "let" "Binding" "let*" "Binding" "letrec" "Binding" + "define" "Binding" "set!" "Binding" + "lambda" "Functions & Components" "fn" "Functions & Components" + "defcomp" "Functions & Components" "defmacro" "Functions & Components" + "begin" "Sequencing & Threading" "do" "Sequencing & Threading" + "->" "Sequencing & Threading" + "quote" "Quoting" "quasiquote" "Quoting" + "reset" "Continuations" "shift" "Continuations" + "dynamic-wind" "Guards" + "map" "Higher-Order Forms" "map-indexed" "Higher-Order Forms" + "filter" "Higher-Order Forms" "reduce" "Higher-Order Forms" + "some" "Higher-Order Forms" "every?" "Higher-Order Forms" + "for-each" "Higher-Order Forms" + "defstyle" "Domain Definitions" + "defhandler" "Domain Definitions" "defpage" "Domain Definitions" + "defquery" "Domain Definitions" "defaction" "Domain Definitions"}) + + +(define extract-define-kwargs + (fn (expr) + ;; Extract keyword args from a define-special-form expression. + ;; Returns dict of keyword-name → string value. + ;; Walks items pairwise: when item[i] is a keyword, item[i+1] is its value. + (let ((result {}) + (items (slice expr 2)) + (n (len items))) + (for-each + (fn (idx) + (when (and (< (+ idx 1) n) + (= (type-of (nth items idx)) "keyword")) + (let ((key (keyword-name (nth items idx))) + (val (nth items (+ idx 1)))) + (dict-set! result key + (if (= (type-of val) "list") + (str "(" (join " " (map serialize val)) ")") + (str val)))))) + (range 0 n)) + result))) + + +(define categorize-special-forms + (fn (parsed-exprs) + ;; parsed-exprs: result of parse-all on special-forms.sx + ;; Returns dict of category-name → list of form dicts. + (let ((categories {})) + (for-each + (fn (expr) + (when (and (= (type-of expr) "list") + (>= (len expr) 2) + (= (type-of (first expr)) "symbol") + (= (symbol-name (first expr)) "define-special-form")) + (let ((name (nth expr 1)) + (kwargs (extract-define-kwargs expr)) + (category (or (get special-form-category-map name) "Other"))) + (when (not (has-key? categories category)) + (dict-set! categories category (list))) + (append! (get categories category) + {"name" name + "syntax" (or (get kwargs "syntax") "") + "doc" (or (get kwargs "doc") "") + "tail-position" (or (get kwargs "tail-position") "") + "example" (or (get kwargs "example") "")})))) + parsed-exprs) + categories))) + + +;; -------------------------------------------------------------------------- +;; build-reference-data +;; +;; Takes a slug and raw reference data, returns structured dict for rendering. +;; -------------------------------------------------------------------------- + +(define build-ref-items-with-href + (fn (items base-path detail-keys n-fields) + ;; items: list of lists (tuples), each with n-fields elements + ;; base-path: e.g. "/hypermedia/reference/attributes/" + ;; detail-keys: list of strings (keys that have detail pages) + ;; n-fields: 2 or 3 (number of fields per tuple) + (map + (fn (item) + (if (= n-fields 3) + ;; [name, desc/value, exists/desc] + (let ((name (nth item 0)) + (field2 (nth item 1)) + (field3 (nth item 2))) + {"name" name + "desc" field2 + "exists" field3 + "href" (if (and field3 (some (fn (k) (= k name)) detail-keys)) + (str base-path name) + nil)}) + ;; [name, desc] + (let ((name (nth item 0)) + (desc (nth item 1))) + {"name" name + "desc" desc + "href" (if (some (fn (k) (= k name)) detail-keys) + (str base-path name) + nil)}))) + items))) + + +(define build-reference-data + (fn (slug raw-data detail-keys) + ;; slug: "attributes", "headers", "events", "js-api" + ;; raw-data: dict with the raw data lists for this slug + ;; detail-keys: list of names that have detail pages + (case slug + "attributes" + {"req-attrs" (build-ref-items-with-href + (get raw-data "req-attrs") + "/hypermedia/reference/attributes/" detail-keys 3) + "beh-attrs" (build-ref-items-with-href + (get raw-data "beh-attrs") + "/hypermedia/reference/attributes/" detail-keys 3) + "uniq-attrs" (build-ref-items-with-href + (get raw-data "uniq-attrs") + "/hypermedia/reference/attributes/" detail-keys 3)} + + "headers" + {"req-headers" (build-ref-items-with-href + (get raw-data "req-headers") + "/hypermedia/reference/headers/" detail-keys 3) + "resp-headers" (build-ref-items-with-href + (get raw-data "resp-headers") + "/hypermedia/reference/headers/" detail-keys 3)} + + "events" + {"events-list" (build-ref-items-with-href + (get raw-data "events-list") + "/hypermedia/reference/events/" detail-keys 2)} + + "js-api" + {"js-api-list" (map (fn (item) {"name" (nth item 0) "desc" (nth item 1)}) + (get raw-data "js-api-list"))} + + ;; default: attributes + :else + {"req-attrs" (build-ref-items-with-href + (get raw-data "req-attrs") + "/hypermedia/reference/attributes/" detail-keys 3) + "beh-attrs" (build-ref-items-with-href + (get raw-data "beh-attrs") + "/hypermedia/reference/attributes/" detail-keys 3) + "uniq-attrs" (build-ref-items-with-href + (get raw-data "uniq-attrs") + "/hypermedia/reference/attributes/" detail-keys 3)}))) + + +;; -------------------------------------------------------------------------- +;; build-attr-detail / build-header-detail / build-event-detail +;; +;; Lookup a slug in a detail dict, reshape for page rendering. +;; -------------------------------------------------------------------------- + +(define build-attr-detail + (fn (slug detail) + ;; detail: dict with "description", "example", "handler", "demo" keys or nil + (if (nil? detail) + {"attr-not-found" true} + {"attr-not-found" nil + "attr-title" slug + "attr-description" (get detail "description") + "attr-example" (get detail "example") + "attr-handler" (get detail "handler") + "attr-demo" (get detail "demo") + "attr-wire-id" (if (has-key? detail "handler") + (str "ref-wire-" + (replace (replace slug ":" "-") "*" "star")) + nil)}))) + + +(define build-header-detail + (fn (slug detail) + (if (nil? detail) + {"header-not-found" true} + {"header-not-found" nil + "header-title" slug + "header-direction" (get detail "direction") + "header-description" (get detail "description") + "header-example" (get detail "example") + "header-demo" (get detail "demo")}))) + + +(define build-event-detail + (fn (slug detail) + (if (nil? detail) + {"event-not-found" true} + {"event-not-found" nil + "event-title" slug + "event-description" (get detail "description") + "event-example" (get detail "example") + "event-demo" (get detail "demo")}))) + + +;; -------------------------------------------------------------------------- +;; build-component-source +;; +;; Reconstruct defcomp/defisland source from component metadata. +;; -------------------------------------------------------------------------- + +(define build-component-source + (fn (comp-data) + ;; comp-data: dict with "type", "name", "params", "has-children", "body-sx", "affinity" + (let ((comp-type (get comp-data "type")) + (name (get comp-data "name")) + (params (get comp-data "params")) + (has-children (get comp-data "has-children")) + (body-sx (get comp-data "body-sx")) + (affinity (get comp-data "affinity"))) + (if (= comp-type "not-found") + (str ";; component " name " not found") + (let ((param-strs (if (empty? params) + (if has-children + (list "&rest" "children") + (list)) + (if has-children + (append (cons "&key" params) (list "&rest" "children")) + (cons "&key" params)))) + (params-sx (str "(" (join " " param-strs) ")")) + (form-name (if (= comp-type "island") "defisland" "defcomp")) + (affinity-str (if (and (= comp-type "component") + (not (nil? affinity)) + (not (= affinity "auto"))) + (str " :affinity " affinity) + ""))) + (str "(" form-name " " name " " params-sx affinity-str "\n " body-sx ")")))))) + + +;; -------------------------------------------------------------------------- +;; build-bundle-analysis +;; +;; Compute per-page bundle stats from pre-extracted component data. +;; -------------------------------------------------------------------------- + +(define build-bundle-analysis + (fn (pages-raw components-raw total-components total-macros pure-count io-count) + ;; pages-raw: list of {:name :path :direct :needed-names} + ;; components-raw: dict of name → {:is-pure :affinity :render-target :io-refs :deps :source} + (let ((pages-data (list))) + (for-each + (fn (page) + (let ((needed-names (get page "needed-names")) + (n (len needed-names)) + (pct (if (> total-components 0) + (round (* (/ n total-components) 100)) + 0)) + (savings (- 100 pct)) + (pure-in-page 0) + (io-in-page 0) + (page-io-refs (list)) + (comp-details (list))) + ;; Walk needed components + (for-each + (fn (comp-name) + (let ((info (get components-raw comp-name))) + (when (not (nil? info)) + (if (get info "is-pure") + (set! pure-in-page (+ pure-in-page 1)) + (do + (set! io-in-page (+ io-in-page 1)) + (for-each + (fn (ref) (when (not (some (fn (r) (= r ref)) page-io-refs)) + (append! page-io-refs ref))) + (or (get info "io-refs") (list))))) + (append! comp-details + {"name" comp-name + "is-pure" (get info "is-pure") + "affinity" (get info "affinity") + "render-target" (get info "render-target") + "io-refs" (or (get info "io-refs") (list)) + "deps" (or (get info "deps") (list)) + "source" (get info "source")})))) + needed-names) + (append! pages-data + {"name" (get page "name") + "path" (get page "path") + "direct" (get page "direct") + "needed" n + "pct" pct + "savings" savings + "io-refs" (len page-io-refs) + "pure-in-page" pure-in-page + "io-in-page" io-in-page + "components" comp-details}))) + pages-raw) + {"pages" pages-data + "total-components" total-components + "total-macros" total-macros + "pure-count" pure-count + "io-count" io-count}))) + + +;; -------------------------------------------------------------------------- +;; build-routing-analysis +;; +;; Classify pages by routing mode (client vs server). +;; -------------------------------------------------------------------------- + +(define build-routing-analysis + (fn (pages-raw) + ;; pages-raw: list of {:name :path :has-data :content-src} + (let ((pages-data (list)) + (client-count 0) + (server-count 0)) + (for-each + (fn (page) + (let ((has-data (get page "has-data")) + (content-src (or (get page "content-src") "")) + (mode nil) + (reason "")) + (cond + has-data + (do (set! mode "server") + (set! reason "Has :data expression — needs server IO") + (set! server-count (+ server-count 1))) + (empty? content-src) + (do (set! mode "server") + (set! reason "No content expression") + (set! server-count (+ server-count 1))) + :else + (do (set! mode "client") + (set! client-count (+ client-count 1)))) + (append! pages-data + {"name" (get page "name") + "path" (get page "path") + "mode" mode + "has-data" has-data + "content-expr" (if (> (len content-src) 80) + (str (slice content-src 0 80) "...") + content-src) + "reason" reason}))) + pages-raw) + {"pages" pages-data + "total-pages" (+ client-count server-count) + "client-count" client-count + "server-count" server-count}))) + + +;; -------------------------------------------------------------------------- +;; build-affinity-analysis +;; +;; Package component affinity info + page render plans for display. +;; -------------------------------------------------------------------------- + +(define build-affinity-analysis + (fn (demo-components page-plans) + {"components" demo-components + "page-plans" page-plans})) diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py new file mode 100644 index 00000000..ad5e759e --- /dev/null +++ b/shared/sx/ref/platform_js.py @@ -0,0 +1,3189 @@ +""" +JS platform constants and functions for the SX bootstrap compiler. + +This module contains all platform-specific JS code (string constants, helper +functions, and configuration dicts) shared by bootstrap_js.py and run_js_sx.py. +The JSEmitter class, compile_ref_to_js function, and main entry point remain +in bootstrap_js.py. +""" +from __future__ import annotations + +from shared.sx.parser import parse_all +from shared.sx.types import Symbol + + +def extract_defines(source: str) -> list[tuple[str, list]]: + """Parse .sx source, return list of (name, define-expr) for top-level defines.""" + exprs = parse_all(source) + defines = [] + for expr in exprs: + if isinstance(expr, list) and expr and isinstance(expr[0], Symbol): + if expr[0].name == "define": + name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) + defines.append((name, expr)) + return defines + +ADAPTER_FILES = { + "parser": ("parser.sx", "parser"), + "html": ("adapter-html.sx", "adapter-html"), + "sx": ("adapter-sx.sx", "adapter-sx"), + "dom": ("adapter-dom.sx", "adapter-dom"), + "engine": ("engine.sx", "engine"), + "orchestration": ("orchestration.sx","orchestration"), + "boot": ("boot.sx", "boot"), +} + +# Dependencies +ADAPTER_DEPS = { + "engine": ["dom"], + "orchestration": ["engine", "dom"], + "boot": ["dom", "engine", "orchestration", "parser"], + "parser": [], +} + +SPEC_MODULES = { + "deps": ("deps.sx", "deps (component dependency analysis)"), + "router": ("router.sx", "router (client-side route matching)"), + "signals": ("signals.sx", "signals (reactive signal runtime)"), + "page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"), +} + + +EXTENSION_NAMES = {"continuations"} +CONTINUATIONS_JS = ''' + // ========================================================================= + // Extension: Delimited continuations (shift/reset) + // ========================================================================= + + function Continuation(fn) { this.fn = fn; } + Continuation.prototype._continuation = true; + Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); }; + + function ShiftSignal(kName, body, env) { + this.kName = kName; + this.body = body; + this.env = env; + } + + PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; }; + + var _resetResume = []; + + function sfReset(args, env) { + var body = args[0]; + try { + return trampoline(evalExpr(body, env)); + } catch (e) { + if (e instanceof ShiftSignal) { + var sig = e; + var cont = new Continuation(function(value) { + if (value === undefined) value = NIL; + _resetResume.push(value); + try { + return trampoline(evalExpr(body, env)); + } finally { + _resetResume.pop(); + } + }); + var sigEnv = merge(sig.env); + sigEnv[sig.kName] = cont; + return trampoline(evalExpr(sig.body, sigEnv)); + } + throw e; + } + } + + function sfShift(args, env) { + if (_resetResume.length > 0) { + return _resetResume[_resetResume.length - 1]; + } + var kName = symbolName(args[0]); + var body = args[1]; + throw new ShiftSignal(kName, body, env); + } + + // Wrap evalList to intercept reset/shift + var _baseEvalList = evalList; + evalList = function(expr, env) { + var head = expr[0]; + if (isSym(head)) { + var name = head.name; + if (name === "reset") return sfReset(expr.slice(1), env); + if (name === "shift") return sfShift(expr.slice(1), env); + } + return _baseEvalList(expr, env); + }; + + // Wrap aserSpecial to handle reset/shift in SX wire mode + if (typeof aserSpecial === "function") { + var _baseAserSpecial = aserSpecial; + aserSpecial = function(name, expr, env) { + if (name === "reset") return sfReset(expr.slice(1), env); + if (name === "shift") return sfShift(expr.slice(1), env); + return _baseAserSpecial(name, expr, env); + }; + } + + // Wrap typeOf to recognize continuations + var _baseTypeOf = typeOf; + typeOf = function(x) { + if (x != null && x._continuation) return "continuation"; + return _baseTypeOf(x); + }; +''' + +ASYNC_IO_JS = ''' + // ========================================================================= + // Async IO: Promise-aware rendering for client-side IO primitives + // ========================================================================= + // + // IO primitives (query, current-user, etc.) return Promises on the client. + // asyncRenderToDom walks the component tree; when it encounters an IO + // primitive, it awaits the Promise and continues rendering. + // + // The sync evaluator/renderer is untouched. This is a separate async path + // used only when a page's component tree contains IO references. + + var IO_PRIMITIVES = {}; + + function registerIoPrimitive(name, fn) { + IO_PRIMITIVES[name] = fn; + } + + function isPromise(x) { + return x != null && typeof x === "object" && typeof x.then === "function"; + } + + // Async trampoline: resolves thunks, awaits Promises + function asyncTrampoline(val) { + if (isPromise(val)) return val.then(asyncTrampoline); + if (isThunk(val)) return asyncTrampoline(evalExpr(thunkExpr(val), thunkEnv(val))); + return val; + } + + // Async eval: like trampoline(evalExpr(...)) but handles IO primitives + function asyncEval(expr, env) { + // Intercept IO primitive calls at the AST level + if (Array.isArray(expr) && expr.length > 0) { + var head = expr[0]; + if (head && head._sym) { + var name = head.name; + if (IO_PRIMITIVES[name]) { + // Evaluate args, then call the IO primitive + return asyncEvalIoCall(name, expr.slice(1), env); + } + } + } + // Non-IO: use sync eval, but result might be a thunk + var result = evalExpr(expr, env); + return asyncTrampoline(result); + } + + function asyncEvalIoCall(name, rawArgs, env) { + // Parse keyword args and positional args, evaluating each (may be async) + var kwargs = {}; + var args = []; + var promises = []; + var i = 0; + while (i < rawArgs.length) { + var arg = rawArgs[i]; + if (arg && arg._kw && (i + 1) < rawArgs.length) { + var kName = arg.name; + var kVal = asyncEval(rawArgs[i + 1], env); + if (isPromise(kVal)) { + (function(k) { promises.push(kVal.then(function(v) { kwargs[k] = v; })); })(kName); + } else { + kwargs[kName] = kVal; + } + i += 2; + } else { + var aVal = asyncEval(arg, env); + if (isPromise(aVal)) { + (function(idx) { promises.push(aVal.then(function(v) { args[idx] = v; })); })(args.length); + args.push(null); // placeholder + } else { + args.push(aVal); + } + i++; + } + } + var ioFn = IO_PRIMITIVES[name]; + if (promises.length > 0) { + return Promise.all(promises).then(function() { return ioFn(args, kwargs); }); + } + return ioFn(args, kwargs); + } + + // Async render-to-dom: returns Promise or Node + function asyncRenderToDom(expr, env, ns) { + // Literals + if (expr === NIL || expr === null || expr === undefined) return null; + if (expr === true || expr === false) return null; + if (typeof expr === "string") return document.createTextNode(expr); + if (typeof expr === "number") return document.createTextNode(String(expr)); + + // Symbol -> async eval then render + if (expr && expr._sym) { + var val = asyncEval(expr, env); + if (isPromise(val)) return val.then(function(v) { return asyncRenderToDom(v, env, ns); }); + return asyncRenderToDom(val, env, ns); + } + + // Keyword + if (expr && expr._kw) return document.createTextNode(expr.name); + + // DocumentFragment / DOM nodes pass through + if (expr instanceof DocumentFragment || (expr && expr.nodeType)) return expr; + + // Dict -> skip + if (expr && typeof expr === "object" && !Array.isArray(expr)) return null; + + // List + if (!Array.isArray(expr) || expr.length === 0) return null; + + var head = expr[0]; + if (!head) return null; + + // Symbol head + if (head._sym) { + var hname = head.name; + + // IO primitive + if (IO_PRIMITIVES[hname]) { + var ioResult = asyncEval(expr, env); + if (isPromise(ioResult)) return ioResult.then(function(v) { return asyncRenderToDom(v, env, ns); }); + return asyncRenderToDom(ioResult, env, ns); + } + + // Fragment + if (hname === "<>") return asyncRenderChildren(expr.slice(1), env, ns); + + // raw! + if (hname === "raw!") { + return asyncEvalRaw(expr.slice(1), env); + } + + // Special forms that need async handling + if (hname === "if") return asyncRenderIf(expr, env, ns); + if (hname === "when") return asyncRenderWhen(expr, env, ns); + if (hname === "cond") return asyncRenderCond(expr, env, ns); + if (hname === "case") return asyncRenderCase(expr, env, ns); + if (hname === "let" || hname === "let*") return asyncRenderLet(expr, env, ns); + if (hname === "begin" || hname === "do") return asyncRenderChildren(expr.slice(1), env, ns); + if (hname === "map") return asyncRenderMap(expr, env, ns); + if (hname === "map-indexed") return asyncRenderMapIndexed(expr, env, ns); + if (hname === "for-each") return asyncRenderMap(expr, env, ns); + + // define/defcomp/defmacro — eval for side effects + if (hname === "define" || hname === "defcomp" || hname === "defmacro" || + hname === "defstyle" || hname === "defhandler") { + trampoline(evalExpr(expr, env)); + return null; + } + + // quote + if (hname === "quote") return null; + + // lambda/fn + if (hname === "lambda" || hname === "fn") { + trampoline(evalExpr(expr, env)); + return null; + } + + // and/or — eval and render result + if (hname === "and" || hname === "or" || hname === "->") { + var aoResult = asyncEval(expr, env); + if (isPromise(aoResult)) return aoResult.then(function(v) { return asyncRenderToDom(v, env, ns); }); + return asyncRenderToDom(aoResult, env, ns); + } + + // set! + if (hname === "set!") { + asyncEval(expr, env); + return null; + } + + // Component or Island + if (hname.charAt(0) === "~") { + var comp = env[hname]; + if (comp && comp._island) return renderDomIsland(comp, expr.slice(1), env, ns); + if (comp && comp._component) return asyncRenderComponent(comp, expr.slice(1), env, ns); + if (comp && comp._macro) { + var expanded = trampoline(expandMacro(comp, expr.slice(1), env)); + return asyncRenderToDom(expanded, env, ns); + } + } + + // Macro + if (env[hname] && env[hname]._macro) { + var mac = env[hname]; + var expanded = trampoline(expandMacro(mac, expr.slice(1), env)); + return asyncRenderToDom(expanded, env, ns); + } + + // HTML tag + if (typeof renderDomElement === "function" && contains(HTML_TAGS, hname)) { + return asyncRenderElement(hname, expr.slice(1), env, ns); + } + + // html: prefix + if (hname.indexOf("html:") === 0) { + return asyncRenderElement(hname.slice(5), expr.slice(1), env, ns); + } + + // Custom element + if (hname.indexOf("-") >= 0 && expr.length > 1 && expr[1] && expr[1]._kw) { + return asyncRenderElement(hname, expr.slice(1), env, ns); + } + + // SVG context + if (ns) return asyncRenderElement(hname, expr.slice(1), env, ns); + + // Fallback: eval and render + var fResult = asyncEval(expr, env); + if (isPromise(fResult)) return fResult.then(function(v) { return asyncRenderToDom(v, env, ns); }); + return asyncRenderToDom(fResult, env, ns); + } + + // Non-symbol head: eval call + var cResult = asyncEval(expr, env); + if (isPromise(cResult)) return cResult.then(function(v) { return asyncRenderToDom(v, env, ns); }); + return asyncRenderToDom(cResult, env, ns); + } + + function asyncRenderChildren(exprs, env, ns) { + var frag = document.createDocumentFragment(); + var pending = []; + for (var i = 0; i < exprs.length; i++) { + var result = asyncRenderToDom(exprs[i], env, ns); + if (isPromise(result)) { + // Insert placeholder, replace when resolved + var placeholder = document.createComment("async"); + frag.appendChild(placeholder); + (function(ph) { + pending.push(result.then(function(node) { + if (node) ph.parentNode.replaceChild(node, ph); + else ph.parentNode.removeChild(ph); + })); + })(placeholder); + } else if (result) { + frag.appendChild(result); + } + } + if (pending.length > 0) { + return Promise.all(pending).then(function() { return frag; }); + } + return frag; + } + + function asyncRenderElement(tag, args, env, ns) { + var newNs = tag === "svg" ? SVG_NS : tag === "math" ? MATH_NS : ns; + var el = domCreateElement(tag, newNs); + var pending = []; + var isVoid = contains(VOID_ELEMENTS, tag); + for (var i = 0; i < args.length; i++) { + var arg = args[i]; + if (arg && arg._kw && (i + 1) < args.length) { + var attrName = arg.name; + var attrVal = asyncEval(args[i + 1], env); + i++; + if (isPromise(attrVal)) { + (function(an, av) { + pending.push(av.then(function(v) { + if (!isNil(v) && v !== false) { + if (contains(BOOLEAN_ATTRS, an)) { if (isSxTruthy(v)) el.setAttribute(an, ""); } + else if (v === true) el.setAttribute(an, ""); + else el.setAttribute(an, String(v)); + } + })); + })(attrName, attrVal); + } else { + if (!isNil(attrVal) && attrVal !== false) { + if (contains(BOOLEAN_ATTRS, attrName)) { + if (isSxTruthy(attrVal)) el.setAttribute(attrName, ""); + } else if (attrVal === true) { + el.setAttribute(attrName, ""); + } else { + el.setAttribute(attrName, String(attrVal)); + } + } + } + } else if (!isVoid) { + var child = asyncRenderToDom(arg, env, newNs); + if (isPromise(child)) { + var placeholder = document.createComment("async"); + el.appendChild(placeholder); + (function(ph) { + pending.push(child.then(function(node) { + if (node) ph.parentNode.replaceChild(node, ph); + else ph.parentNode.removeChild(ph); + })); + })(placeholder); + } else if (child) { + el.appendChild(child); + } + } + } + if (pending.length > 0) return Promise.all(pending).then(function() { return el; }); + return el; + } + + function asyncRenderComponent(comp, args, env, ns) { + var kwargs = {}; + var children = []; + var pending = []; + for (var i = 0; i < args.length; i++) { + var arg = args[i]; + if (arg && arg._kw && (i + 1) < args.length) { + var kName = arg.name; + var kVal = asyncEval(args[i + 1], env); + if (isPromise(kVal)) { + (function(k) { pending.push(kVal.then(function(v) { kwargs[k] = v; })); })(kName); + } else { + kwargs[kName] = kVal; + } + i++; + } else { + children.push(arg); + } + } + + function doRender() { + var local = Object.create(componentClosure(comp)); + for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k]; + var params = componentParams(comp); + for (var j = 0; j < params.length; j++) { + local[params[j]] = params[j] in kwargs ? kwargs[params[j]] : NIL; + } + if (componentHasChildren(comp)) { + var childResult = asyncRenderChildren(children, env, ns); + if (isPromise(childResult)) { + return childResult.then(function(childFrag) { + local["children"] = childFrag; + return asyncRenderToDom(componentBody(comp), local, ns); + }); + } + local["children"] = childResult; + } + return asyncRenderToDom(componentBody(comp), local, ns); + } + + if (pending.length > 0) return Promise.all(pending).then(doRender); + return doRender(); + } + + function asyncRenderIf(expr, env, ns) { + var cond = asyncEval(expr[1], env); + if (isPromise(cond)) { + return cond.then(function(v) { + return isSxTruthy(v) + ? asyncRenderToDom(expr[2], env, ns) + : (expr.length > 3 ? asyncRenderToDom(expr[3], env, ns) : null); + }); + } + return isSxTruthy(cond) + ? asyncRenderToDom(expr[2], env, ns) + : (expr.length > 3 ? asyncRenderToDom(expr[3], env, ns) : null); + } + + function asyncRenderWhen(expr, env, ns) { + var cond = asyncEval(expr[1], env); + if (isPromise(cond)) { + return cond.then(function(v) { + return isSxTruthy(v) ? asyncRenderChildren(expr.slice(2), env, ns) : null; + }); + } + return isSxTruthy(cond) ? asyncRenderChildren(expr.slice(2), env, ns) : null; + } + + function asyncRenderCond(expr, env, ns) { + var clauses = expr.slice(1); + function step(idx) { + if (idx >= clauses.length) return null; + var clause = clauses[idx]; + if (!Array.isArray(clause) || clause.length < 2) return step(idx + 1); + var test = clause[0]; + if ((test && test._sym && (test.name === "else" || test.name === ":else")) || + (test && test._kw && test.name === "else")) { + return asyncRenderToDom(clause[1], env, ns); + } + var v = asyncEval(test, env); + if (isPromise(v)) return v.then(function(r) { return isSxTruthy(r) ? asyncRenderToDom(clause[1], env, ns) : step(idx + 1); }); + return isSxTruthy(v) ? asyncRenderToDom(clause[1], env, ns) : step(idx + 1); + } + return step(0); + } + + function asyncRenderCase(expr, env, ns) { + var matchVal = asyncEval(expr[1], env); + function doCase(mv) { + var clauses = expr.slice(2); + for (var i = 0; i < clauses.length - 1; i += 2) { + var test = clauses[i]; + if ((test && test._kw && test.name === "else") || + (test && test._sym && (test.name === "else" || test.name === ":else"))) { + return asyncRenderToDom(clauses[i + 1], env, ns); + } + var tv = trampoline(evalExpr(test, env)); + if (mv === tv || (typeof mv === "string" && typeof tv === "string" && mv === tv)) { + return asyncRenderToDom(clauses[i + 1], env, ns); + } + } + return null; + } + if (isPromise(matchVal)) return matchVal.then(doCase); + return doCase(matchVal); + } + + function asyncRenderLet(expr, env, ns) { + var bindings = expr[1]; + var local = Object.create(env); + for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k]; + function bindStep(idx) { + if (!Array.isArray(bindings)) return asyncRenderChildren(expr.slice(2), local, ns); + // Nested pairs: ((a 1) (b 2)) + if (bindings.length > 0 && Array.isArray(bindings[0])) { + if (idx >= bindings.length) return asyncRenderChildren(expr.slice(2), local, ns); + var b = bindings[idx]; + var vname = b[0]._sym ? b[0].name : String(b[0]); + var val = asyncEval(b[1], local); + if (isPromise(val)) return val.then(function(v) { local[vname] = v; return bindStep(idx + 1); }); + local[vname] = val; + return bindStep(idx + 1); + } + // Flat pairs: (a 1 b 2) + if (idx >= bindings.length) return asyncRenderChildren(expr.slice(2), local, ns); + var vn = bindings[idx]._sym ? bindings[idx].name : String(bindings[idx]); + var vv = asyncEval(bindings[idx + 1], local); + if (isPromise(vv)) return vv.then(function(v) { local[vn] = v; return bindStep(idx + 2); }); + local[vn] = vv; + return bindStep(idx + 2); + } + return bindStep(0); + } + + function asyncRenderMap(expr, env, ns) { + var fn = asyncEval(expr[1], env); + var coll = asyncEval(expr[2], env); + function doMap(f, c) { + if (!Array.isArray(c)) return null; + var frag = document.createDocumentFragment(); + var pending = []; + for (var i = 0; i < c.length; i++) { + var item = c[i]; + var result; + if (f && f._lambda) { + var lenv = Object.create(f.closure || env); + for (var k in env) if (env.hasOwnProperty(k)) lenv[k] = env[k]; + lenv[f.params[0]] = item; + result = asyncRenderToDom(f.body, lenv, null); + } else if (typeof f === "function") { + var r = f(item); + result = isPromise(r) ? r.then(function(v) { return asyncRenderToDom(v, env, null); }) : asyncRenderToDom(r, env, null); + } else { + result = asyncRenderToDom(item, env, null); + } + if (isPromise(result)) { + var ph = document.createComment("async"); + frag.appendChild(ph); + (function(p) { pending.push(result.then(function(n) { if (n) p.parentNode.replaceChild(n, p); else p.parentNode.removeChild(p); })); })(ph); + } else if (result) { + frag.appendChild(result); + } + } + if (pending.length) return Promise.all(pending).then(function() { return frag; }); + return frag; + } + if (isPromise(fn) || isPromise(coll)) { + return Promise.all([isPromise(fn) ? fn : Promise.resolve(fn), isPromise(coll) ? coll : Promise.resolve(coll)]) + .then(function(r) { return doMap(r[0], r[1]); }); + } + return doMap(fn, coll); + } + + function asyncRenderMapIndexed(expr, env, ns) { + var fn = asyncEval(expr[1], env); + var coll = asyncEval(expr[2], env); + function doMap(f, c) { + if (!Array.isArray(c)) return null; + var frag = document.createDocumentFragment(); + var pending = []; + for (var i = 0; i < c.length; i++) { + var item = c[i]; + var result; + if (f && f._lambda) { + var lenv = Object.create(f.closure || env); + for (var k in env) if (env.hasOwnProperty(k)) lenv[k] = env[k]; + lenv[f.params[0]] = i; + lenv[f.params[1]] = item; + result = asyncRenderToDom(f.body, lenv, null); + } else if (typeof f === "function") { + var r = f(i, item); + result = isPromise(r) ? r.then(function(v) { return asyncRenderToDom(v, env, null); }) : asyncRenderToDom(r, env, null); + } else { + result = asyncRenderToDom(item, env, null); + } + if (isPromise(result)) { + var ph = document.createComment("async"); + frag.appendChild(ph); + (function(p) { pending.push(result.then(function(n) { if (n) p.parentNode.replaceChild(n, p); else p.parentNode.removeChild(p); })); })(ph); + } else if (result) { + frag.appendChild(result); + } + } + if (pending.length) return Promise.all(pending).then(function() { return frag; }); + return frag; + } + if (isPromise(fn) || isPromise(coll)) { + return Promise.all([isPromise(fn) ? fn : Promise.resolve(fn), isPromise(coll) ? coll : Promise.resolve(coll)]) + .then(function(r) { return doMap(r[0], r[1]); }); + } + return doMap(fn, coll); + } + + function asyncEvalRaw(args, env) { + var parts = []; + var pending = []; + for (var i = 0; i < args.length; i++) { + var val = asyncEval(args[i], env); + if (isPromise(val)) { + (function(idx) { + pending.push(val.then(function(v) { parts[idx] = v; })); + })(parts.length); + parts.push(null); + } else { + parts.push(val); + } + } + function assemble() { + var html = ""; + for (var j = 0; j < parts.length; j++) { + var p = parts[j]; + if (p && p._rawHtml) html += p.html; + else if (typeof p === "string") html += p; + else if (p != null && !isNil(p)) html += String(p); + } + var el = document.createElement("span"); + el.innerHTML = html; + var frag = document.createDocumentFragment(); + while (el.firstChild) frag.appendChild(el.firstChild); + return frag; + } + if (pending.length) return Promise.all(pending).then(assemble); + return assemble(); + } + + // Async version of sxRenderWithEnv — returns Promise + function asyncSxRenderWithEnv(source, extraEnv) { + var env = extraEnv ? merge(componentEnv, extraEnv) : componentEnv; + var exprs = parse(source); + if (!_hasDom) return Promise.resolve(null); + return asyncRenderChildren(exprs, env, null); + } + + // IO proxy cache: key → { value, expires } + var _ioCache = {}; + var IO_CACHE_TTL = 300000; // 5 minutes + + // Register a server-proxied IO primitive: fetches from /sx/io/ + // Uses GET for short args, POST for long payloads (URL length safety). + // Results are cached client-side by (name + args) with a TTL. + function registerProxiedIo(name) { + registerIoPrimitive(name, function(args, kwargs) { + // Cache key: name + serialized args + var cacheKey = name; + for (var ci = 0; ci < args.length; ci++) cacheKey += "\0" + String(args[ci]); + for (var ck in kwargs) { + if (kwargs.hasOwnProperty(ck)) cacheKey += "\0" + ck + "=" + String(kwargs[ck]); + } + var cached = _ioCache[cacheKey]; + if (cached && cached.expires > Date.now()) return cached.value; + + var url = "/sx/io/" + encodeURIComponent(name); + var qs = []; + for (var i = 0; i < args.length; i++) { + qs.push("_arg" + i + "=" + encodeURIComponent(String(args[i]))); + } + for (var k in kwargs) { + if (kwargs.hasOwnProperty(k)) { + qs.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(kwargs[k]))); + } + } + var queryStr = qs.join("&"); + var fetchOpts; + if (queryStr.length > 1500) { + // POST with JSON body for long payloads + var sArgs = []; + for (var j = 0; j < args.length; j++) sArgs.push(String(args[j])); + var sKwargs = {}; + for (var kk in kwargs) { + if (kwargs.hasOwnProperty(kk)) sKwargs[kk] = String(kwargs[kk]); + } + var postHeaders = { "SX-Request": "true", "Content-Type": "application/json" }; + var csrf = csrfToken(); + if (csrf && csrf !== NIL) postHeaders["X-CSRFToken"] = csrf; + fetchOpts = { + method: "POST", + headers: postHeaders, + body: JSON.stringify({ args: sArgs, kwargs: sKwargs }) + }; + } else { + if (queryStr) url += "?" + queryStr; + fetchOpts = { headers: { "SX-Request": "true" } }; + } + var result = fetch(url, fetchOpts) + .then(function(resp) { + if (!resp.ok) { + logWarn("sx:io " + name + " failed " + resp.status); + return NIL; + } + return resp.text(); + }) + .then(function(text) { + if (!text || text === "nil") return NIL; + try { + var exprs = parse(text); + var val = exprs.length === 1 ? exprs[0] : exprs; + _ioCache[cacheKey] = { value: val, expires: Date.now() + IO_CACHE_TTL }; + return val; + } catch (e) { + logWarn("sx:io " + name + " parse error: " + (e && e.message ? e.message : e)); + return NIL; + } + }) + .catch(function(e) { + logWarn("sx:io " + name + " network error: " + (e && e.message ? e.message : e)); + return NIL; + }); + // Cache the in-flight promise too (dedup concurrent calls for same args) + _ioCache[cacheKey] = { value: result, expires: Date.now() + IO_CACHE_TTL }; + return result; + }); + } + + // Register IO deps as proxied primitives (idempotent, called per-page) + function registerIoDeps(names) { + if (!names || !names.length) return; + var registered = 0; + for (var i = 0; i < names.length; i++) { + var name = names[i]; + if (!IO_PRIMITIVES[name]) { + registerProxiedIo(name); + registered++; + } + } + if (registered > 0) { + logInfo("sx:io registered " + registered + " proxied primitives: " + names.join(", ")); + } + } +''' + +PREAMBLE = '''\ +/** + * sx-ref.js — Generated from reference SX evaluator specification. + * + * Bootstrap-compiled from shared/sx/ref/{eval,render,primitives}.sx + * Compare against hand-written sx.js for correctness verification. + * + * DO NOT EDIT — regenerate with: python bootstrap_js.py + */ +;(function(global) { + "use strict"; + + // ========================================================================= + // Types + // ========================================================================= + + var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); + var SX_VERSION = "BUILD_TIMESTAMP"; + + function isNil(x) { return x === NIL || x === null || x === undefined; } + function isSxTruthy(x) { return x !== false && !isNil(x); } + + function Symbol(name) { this.name = name; } + Symbol.prototype.toString = function() { return this.name; }; + Symbol.prototype._sym = true; + + function Keyword(name) { this.name = name; } + Keyword.prototype.toString = function() { return ":" + this.name; }; + Keyword.prototype._kw = true; + + function Lambda(params, body, closure, name) { + this.params = params; + this.body = body; + this.closure = closure || {}; + this.name = name || null; + } + Lambda.prototype._lambda = true; + + function Component(name, params, hasChildren, body, closure, affinity) { + this.name = name; + this.params = params; + this.hasChildren = hasChildren; + this.body = body; + this.closure = closure || {}; + this.affinity = affinity || "auto"; + } + Component.prototype._component = true; + + function Island(name, params, hasChildren, body, closure) { + this.name = name; + this.params = params; + this.hasChildren = hasChildren; + this.body = body; + this.closure = closure || {}; + } + Island.prototype._island = true; + + function SxSignal(value) { + this.value = value; + this.subscribers = []; + this.deps = []; + } + SxSignal.prototype._signal = true; + + function TrackingCtx(notifyFn) { + this.notifyFn = notifyFn; + this.deps = []; + } + + var _trackingContext = null; + + function Macro(params, restParam, body, closure, name) { + this.params = params; + this.restParam = restParam; + this.body = body; + this.closure = closure || {}; + this.name = name || null; + } + Macro.prototype._macro = true; + + function Thunk(expr, env) { this.expr = expr; this.env = env; } + Thunk.prototype._thunk = true; + + function RawHTML(html) { this.html = html; } + RawHTML.prototype._raw = true; + + function isSym(x) { return x != null && x._sym === true; } + function isKw(x) { return x != null && x._kw === true; } + + function merge() { + var out = {}; + for (var i = 0; i < arguments.length; i++) { + var d = arguments[i]; + if (d) for (var k in d) out[k] = d[k]; + } + return out; + } + + function sxOr() { + for (var i = 0; i < arguments.length; i++) { + if (isSxTruthy(arguments[i])) return arguments[i]; + } + return arguments.length ? arguments[arguments.length - 1] : false; + }''' +PRIMITIVES_JS_MODULES: dict[str, str] = { + "core.arithmetic": ''' + // core.arithmetic + PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; }; + PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; }; + PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; }; + PRIMITIVES["/"] = function(a, b) { return a / b; }; + PRIMITIVES["mod"] = function(a, b) { return a % b; }; + PRIMITIVES["inc"] = function(n) { return n + 1; }; + PRIMITIVES["dec"] = function(n) { return n - 1; }; + PRIMITIVES["abs"] = Math.abs; + PRIMITIVES["floor"] = Math.floor; + PRIMITIVES["ceil"] = Math.ceil; + PRIMITIVES["round"] = function(x, n) { + if (n === undefined || n === 0) return Math.round(x); + var f = Math.pow(10, n); return Math.round(x * f) / f; + }; + PRIMITIVES["min"] = Math.min; + PRIMITIVES["max"] = Math.max; + PRIMITIVES["sqrt"] = Math.sqrt; + PRIMITIVES["pow"] = Math.pow; + PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }; +''', + + "core.comparison": ''' + // core.comparison + PRIMITIVES["="] = function(a, b) { return a === b; }; + PRIMITIVES["!="] = function(a, b) { return a !== b; }; + PRIMITIVES["<"] = function(a, b) { return a < b; }; + PRIMITIVES[">"] = function(a, b) { return a > b; }; + PRIMITIVES["<="] = function(a, b) { return a <= b; }; + PRIMITIVES[">="] = function(a, b) { return a >= b; }; +''', + + "core.logic": ''' + // core.logic + PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); }; +''', + + "core.predicates": ''' + // core.predicates + PRIMITIVES["nil?"] = isNil; + PRIMITIVES["number?"] = function(x) { return typeof x === "number"; }; + PRIMITIVES["string?"] = function(x) { return typeof x === "string"; }; + PRIMITIVES["list?"] = Array.isArray; + PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; }; + PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); }; + PRIMITIVES["contains?"] = function(c, k) { + if (typeof c === "string") return c.indexOf(String(k)) !== -1; + if (Array.isArray(c)) return c.indexOf(k) !== -1; + return k in c; + }; + PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; }; + PRIMITIVES["even?"] = function(n) { return n % 2 === 0; }; + PRIMITIVES["zero?"] = function(n) { return n === 0; }; + PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; }; + PRIMITIVES["component-affinity"] = componentAffinity; +''', + + "core.strings": ''' + // core.strings + PRIMITIVES["str"] = function() { + var p = []; + for (var i = 0; i < arguments.length; i++) { + var v = arguments[i]; if (isNil(v)) continue; p.push(String(v)); + } + return p.join(""); + }; + PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); }; + PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); }; + PRIMITIVES["trim"] = function(s) { return String(s).trim(); }; + PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); }; + PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); }; + PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); }; + PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); }; + PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; }; + PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; }; + PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); }; + PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); }; + PRIMITIVES["string-length"] = function(s) { return String(s).length; }; + PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; }; + PRIMITIVES["concat"] = function() { + var out = []; + for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]); + return out; + }; +''', + + "core.collections": ''' + // core.collections + PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); }; + PRIMITIVES["dict"] = function() { + var d = {}; + for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1]; + return d; + }; + PRIMITIVES["range"] = function(a, b, step) { + var r = []; step = step || 1; + for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i); + return r; + }; + PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); }; + PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; }; + PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; }; + PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; }; + PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; }; + PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; }; + PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); }; + PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); }; + PRIMITIVES["append!"] = function(arr, x) { arr.push(x); return arr; }; + PRIMITIVES["chunk-every"] = function(c, n) { + var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r; + }; + PRIMITIVES["zip-pairs"] = function(c) { + var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r; + }; + PRIMITIVES["reverse"] = function(c) { return Array.isArray(c) ? c.slice().reverse() : String(c).split("").reverse().join(""); }; + PRIMITIVES["flatten"] = function(c) { + var out = []; + function walk(a) { for (var i = 0; i < a.length; i++) Array.isArray(a[i]) ? walk(a[i]) : out.push(a[i]); } + walk(c || []); return out; + }; +''', + + "core.dict": ''' + // core.dict + PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); }; + PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; }; + PRIMITIVES["merge"] = function() { + var out = {}; + for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; } + return out; + }; + PRIMITIVES["assoc"] = function(d) { + var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; + for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1]; + return out; + }; + PRIMITIVES["dissoc"] = function(d) { + var out = {}; for (var k in d) out[k] = d[k]; + for (var i = 1; i < arguments.length; i++) delete out[arguments[i]]; + return out; + }; + PRIMITIVES["dict-set!"] = function(d, k, v) { d[k] = v; return v; }; + PRIMITIVES["has-key?"] = function(d, k) { return d !== null && d !== undefined && k in d; }; + PRIMITIVES["into"] = function(target, coll) { + if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll); + var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; } + return r; + }; +''', + + "stdlib.format": ''' + // stdlib.format + PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); }; + PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; }; + PRIMITIVES["format-date"] = function(s, fmt) { + if (!s) return ""; + try { + var d = new Date(s); + if (isNaN(d.getTime())) return String(s); + var months = ["January","February","March","April","May","June","July","August","September","October","November","December"]; + var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; + return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2)) + .replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()]) + .replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2)) + .replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2)); + } catch (e) { return String(s); } + }; + PRIMITIVES["parse-datetime"] = function(s) { return s ? String(s) : NIL; }; +''', + + "stdlib.text": ''' + // stdlib.text + PRIMITIVES["pluralize"] = function(n, s, p) { + if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s"); + return n == 1 ? "" : "s"; + }; + PRIMITIVES["escape"] = function(s) { + return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"); + }; + PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); }; +''', + + "stdlib.debug": ''' + // stdlib.debug + PRIMITIVES["assert"] = function(cond, msg) { + if (!isSxTruthy(cond)) throw new Error("Assertion error: " + (msg || "Assertion failed")); + return true; + }; +''', +} +# Modules to include by default (all) +_ALL_JS_MODULES = list(PRIMITIVES_JS_MODULES.keys()) + +def _assemble_primitives_js(modules: list[str] | None = None) -> str: + """Assemble JS primitive code from selected modules. + + If modules is None, all modules are included. + Core modules are always included regardless of the list. + """ + if modules is None: + modules = _ALL_JS_MODULES + parts = [] + for mod in modules: + if mod in PRIMITIVES_JS_MODULES: + parts.append(PRIMITIVES_JS_MODULES[mod]) + return "\n".join(parts) + +PLATFORM_JS_PRE = ''' + // ========================================================================= + // Platform interface — JS implementation + // ========================================================================= + + function typeOf(x) { + if (isNil(x)) return "nil"; + if (typeof x === "number") return "number"; + if (typeof x === "string") return "string"; + if (typeof x === "boolean") return "boolean"; + if (x._sym) return "symbol"; + if (x._kw) return "keyword"; + if (x._thunk) return "thunk"; + if (x._lambda) return "lambda"; + if (x._component) return "component"; + if (x._island) return "island"; + if (x._signal) return "signal"; + if (x._macro) return "macro"; + if (x._raw) return "raw-html"; + if (typeof Node !== "undefined" && x instanceof Node) return "dom-node"; + if (Array.isArray(x)) return "list"; + if (typeof x === "object") return "dict"; + return "unknown"; + } + + function symbolName(s) { return s.name; } + function keywordName(k) { return k.name; } + function makeSymbol(n) { return new Symbol(n); } + function makeKeyword(n) { return new Keyword(n); } + + function makeLambda(params, body, env) { return new Lambda(params, body, merge(env)); } + function makeComponent(name, params, hasChildren, body, env, affinity) { + return new Component(name, params, hasChildren, body, merge(env), affinity); + } + function makeMacro(params, restParam, body, env, name) { + return new Macro(params, restParam, body, merge(env), name); + } + function makeThunk(expr, env) { return new Thunk(expr, env); } + + function lambdaParams(f) { return f.params; } + function lambdaBody(f) { return f.body; } + function lambdaClosure(f) { return f.closure; } + function lambdaName(f) { return f.name; } + function setLambdaName(f, n) { f.name = n; } + + function componentParams(c) { return c.params; } + function componentBody(c) { return c.body; } + function componentClosure(c) { return c.closure; } + function componentHasChildren(c) { return c.hasChildren; } + function componentName(c) { return c.name; } + function componentAffinity(c) { return c.affinity || "auto"; } + + function macroParams(m) { return m.params; } + function macroRestParam(m) { return m.restParam; } + function macroBody(m) { return m.body; } + function macroClosure(m) { return m.closure; } + + function isThunk(x) { return x != null && x._thunk === true; } + function thunkExpr(t) { return t.expr; } + function thunkEnv(t) { return t.env; } + + function isCallable(x) { return typeof x === "function" || (x != null && x._lambda === true); } + function isLambda(x) { return x != null && x._lambda === true; } + function isComponent(x) { return x != null && x._component === true; } + function isIsland(x) { return x != null && x._island === true; } + function isMacro(x) { return x != null && x._macro === true; } + function isIdentical(a, b) { return a === b; } + + // Island platform + function makeIsland(name, params, hasChildren, body, env) { + return new Island(name, params, hasChildren, body, merge(env)); + } + + // Signal platform + function makeSignal(value) { return new SxSignal(value); } + function isSignal(x) { return x != null && x._signal === true; } + function signalValue(s) { return s.value; } + function signalSetValue(s, v) { s.value = v; } + function signalSubscribers(s) { return s.subscribers.slice(); } + function signalAddSub(s, fn) { if (s.subscribers.indexOf(fn) < 0) s.subscribers.push(fn); } + function signalRemoveSub(s, fn) { var i = s.subscribers.indexOf(fn); if (i >= 0) s.subscribers.splice(i, 1); } + function signalDeps(s) { return s.deps.slice(); } + function signalSetDeps(s, deps) { s.deps = Array.isArray(deps) ? deps.slice() : []; } + function setTrackingContext(ctx) { _trackingContext = ctx; } + function getTrackingContext() { return _trackingContext || NIL; } + function makeTrackingContext(notifyFn) { return new TrackingCtx(notifyFn); } + function trackingContextDeps(ctx) { return ctx ? ctx.deps : []; } + function trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); } + function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; } + + // invoke — call any callable (native fn or SX lambda) with args. + // Transpiled code emits direct calls f(args) which fail on SX lambdas + // from runtime-evaluated island bodies. invoke dispatches correctly. + function invoke() { + var f = arguments[0]; + var args = Array.prototype.slice.call(arguments, 1); + if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f))); + if (typeof f === 'function') return f.apply(null, args); + return NIL; + } + + // JSON / dict helpers for island state serialization + function jsonSerialize(obj) { + return JSON.stringify(obj); + } + function isEmptyDict(d) { + if (!d || typeof d !== "object") return true; + for (var k in d) if (d.hasOwnProperty(k)) return false; + return true; + } + + function envHas(env, name) { return name in env; } + function envGet(env, name) { return env[name]; } + function envSet(env, name, val) { + // Walk prototype chain to find where the variable is defined (for set!) + var obj = env; + while (obj !== null && obj !== Object.prototype) { + if (obj.hasOwnProperty(name)) { obj[name] = val; return; } + obj = Object.getPrototypeOf(obj); + } + // Not found in any parent scope — set on the immediate env + env[name] = val; + } + function envExtend(env) { return Object.create(env); } + function envMerge(base, overlay) { + // Same env or overlay is descendant of base — just extend, no copy. + // This prevents set! inside lambdas from modifying shadow copies. + if (base === overlay) return Object.create(base); + var p = overlay; + for (var d = 0; p && p !== Object.prototype && d < 100; d++) { + if (p === base) return Object.create(base); + p = Object.getPrototypeOf(p); + } + // General case: extend base, copy ONLY overlay properties that don't + // exist in the base chain (avoids shadowing closure bindings). + var child = Object.create(base); + if (overlay) { + for (var k in overlay) { + if (overlay.hasOwnProperty(k) && !(k in base)) child[k] = overlay[k]; + } + } + return child; + } + + function dictSet(d, k, v) { d[k] = v; return v; } + function dictGet(d, k) { var v = d[k]; return v !== undefined ? v : NIL; } + + // Render-expression detection — lets the evaluator delegate to the active adapter. + // Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements. + // Placeholder — overridden by transpiled version from render.sx + function isRenderExpr(expr) { return false; } + + // Render dispatch — call the active adapter's render function. + // Set by each adapter when loaded; defaults to identity (no rendering). + var _renderExprFn = null; + + // Render mode flag — set by render-to-html/aser, checked by eval-list. + // When false, render expressions fall through to evalCall. + var _renderMode = false; + function renderActiveP() { return _renderMode; } + function setRenderActiveB(val) { _renderMode = !!val; } + + function renderExpr(expr, env) { + if (_renderExprFn) return _renderExprFn(expr, env); + // No adapter loaded — fall through to evalCall + return evalCall(first(expr), rest(expr), env); + } + + function stripPrefix(s, prefix) { + return s.indexOf(prefix) === 0 ? s.slice(prefix.length) : s; + } + + function error(msg) { throw new Error(msg); } + function inspect(x) { return JSON.stringify(x); } + +''' + + +PLATFORM_JS_POST = ''' + function isPrimitive(name) { return name in PRIMITIVES; } + function getPrimitive(name) { return PRIMITIVES[name]; } + + // Higher-order helpers used by the transpiled code + function map(fn, coll) { return coll.map(fn); } + function mapIndexed(fn, coll) { return coll.map(function(item, i) { return fn(i, item); }); } + function filter(fn, coll) { return coll.filter(function(x) { return isSxTruthy(fn(x)); }); } + function reduce(fn, init, coll) { + var acc = init; + for (var i = 0; i < coll.length; i++) acc = fn(acc, coll[i]); + return acc; + } + function some(fn, coll) { + for (var i = 0; i < coll.length; i++) { var r = fn(coll[i]); if (isSxTruthy(r)) return r; } + return NIL; + } + function forEach(fn, coll) { for (var i = 0; i < coll.length; i++) fn(coll[i]); return NIL; } + function isEvery(fn, coll) { + for (var i = 0; i < coll.length; i++) { if (!isSxTruthy(fn(coll[i]))) return false; } + return true; + } + function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; } + + // List primitives used directly by transpiled code + var len = PRIMITIVES["len"]; + var first = PRIMITIVES["first"]; + var last = PRIMITIVES["last"]; + var rest = PRIMITIVES["rest"]; + var nth = PRIMITIVES["nth"]; + var cons = PRIMITIVES["cons"]; + var append = PRIMITIVES["append"]; + var isEmpty = PRIMITIVES["empty?"]; + var contains = PRIMITIVES["contains?"]; + var startsWith = PRIMITIVES["starts-with?"]; + var slice = PRIMITIVES["slice"]; + var concat = PRIMITIVES["concat"]; + var str = PRIMITIVES["str"]; + var join = PRIMITIVES["join"]; + var keys = PRIMITIVES["keys"]; + var get = PRIMITIVES["get"]; + var assoc = PRIMITIVES["assoc"]; + var range = PRIMITIVES["range"]; + function zip(a, b) { var r = []; for (var i = 0; i < Math.min(a.length, b.length); i++) r.push([a[i], b[i]]); return r; } + function append_b(arr, x) { arr.push(x); return arr; } + var apply = function(f, args) { + if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f))); + return f.apply(null, args); + }; + + // Additional primitive aliases used by adapter/engine transpiled code + var split = PRIMITIVES["split"]; + var trim = PRIMITIVES["trim"]; + var upper = PRIMITIVES["upper"]; + var lower = PRIMITIVES["lower"]; + var replace_ = function(s, old, nw) { return s.split(old).join(nw); }; + var endsWith = PRIMITIVES["ends-with?"]; + var parseInt_ = PRIMITIVES["parse-int"]; + var dict_fn = PRIMITIVES["dict"]; + + // HTML rendering helpers + function escapeHtml(s) { + return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,"""); + } + function escapeAttr(s) { return escapeHtml(s); } + function rawHtmlContent(r) { return r.html; } + function makeRawHtml(s) { return { _raw: true, html: s }; } + function sxExprSource(x) { return x && x.source ? x.source : String(x); } + + // Placeholders — overridden by transpiled spec from parser.sx / adapter-sx.sx + function serialize(val) { return String(val); } + function isSpecialForm(n) { return false; } + function isHoForm(n) { return false; } + + // processBindings and evalCond — now specced in render.sx, bootstrapped above + + function isDefinitionForm(name) { + return name === "define" || name === "defcomp" || name === "defmacro" || + name === "defstyle" || name === "defhandler"; + } + + function indexOf_(s, ch) { + return typeof s === "string" ? s.indexOf(ch) : -1; + } + + function dictHas(d, k) { return d != null && k in d; } + function dictDelete(d, k) { delete d[k]; } + + function forEachIndexed(fn, coll) { + for (var i = 0; i < coll.length; i++) fn(i, coll[i]); + return NIL; + } + + // ========================================================================= + // Performance overrides — evaluator hot path + // ========================================================================= + + // Override parseKeywordArgs: imperative loop instead of reduce+assoc + parseKeywordArgs = function(rawArgs, env) { + var kwargs = {}; + var children = []; + for (var i = 0; i < rawArgs.length; i++) { + var arg = rawArgs[i]; + if (arg && arg._kw && (i + 1) < rawArgs.length) { + kwargs[arg.name] = trampoline(evalExpr(rawArgs[i + 1], env)); + i++; + } else { + children.push(trampoline(evalExpr(arg, env))); + } + } + return [kwargs, children]; + }; + + // Override callComponent: use prototype chain env, imperative kwarg binding + callComponent = function(comp, rawArgs, env) { + var kwargs = {}; + var children = []; + for (var i = 0; i < rawArgs.length; i++) { + var arg = rawArgs[i]; + if (arg && arg._kw && (i + 1) < rawArgs.length) { + kwargs[arg.name] = trampoline(evalExpr(rawArgs[i + 1], env)); + i++; + } else { + children.push(trampoline(evalExpr(arg, env))); + } + } + var local = Object.create(componentClosure(comp)); + for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k]; + var params = componentParams(comp); + for (var j = 0; j < params.length; j++) { + var p = params[j]; + local[p] = p in kwargs ? kwargs[p] : NIL; + } + if (componentHasChildren(comp)) { + local["children"] = children; + } + return makeThunk(componentBody(comp), local); + };''' + + +PLATFORM_DEPS_JS = ''' + // ========================================================================= + // Platform: deps module — component dependency analysis + // ========================================================================= + + function componentDeps(c) { + return c.deps ? c.deps.slice() : []; + } + + function componentSetDeps(c, deps) { + c.deps = deps; + } + + function componentCssClasses(c) { + return c.cssClasses ? c.cssClasses.slice() : []; + } + + function envComponents(env) { + var names = []; + for (var k in env) { + var v = env[k]; + if (v && (v._component || v._macro)) names.push(k); + } + return names; + } + + function regexFindAll(pattern, source) { + var re = new RegExp(pattern, "g"); + var results = []; + var m; + while ((m = re.exec(source)) !== null) { + if (m[1] !== undefined) results.push(m[1]); + else results.push(m[0]); + } + return results; + } + + function scanCssClasses(source) { + var classes = {}; + var result = []; + var m; + var re1 = /:class\\s+"([^"]*)"/g; + while ((m = re1.exec(source)) !== null) { + var parts = m[1].split(/\\s+/); + for (var i = 0; i < parts.length; i++) { + if (parts[i] && !classes[parts[i]]) { + classes[parts[i]] = true; + result.push(parts[i]); + } + } + } + var re2 = /:class\\s+\\(str\\s+((?:"[^"]*"\\s*)+)\\)/g; + while ((m = re2.exec(source)) !== null) { + var re3 = /"([^"]*)"/g; + var m2; + while ((m2 = re3.exec(m[1])) !== null) { + var parts2 = m2[1].split(/\\s+/); + for (var j = 0; j < parts2.length; j++) { + if (parts2[j] && !classes[parts2[j]]) { + classes[parts2[j]] = true; + result.push(parts2[j]); + } + } + } + } + var re4 = /;;\\s*@css\\s+(.+)/g; + while ((m = re4.exec(source)) !== null) { + var parts3 = m[1].split(/\\s+/); + for (var k = 0; k < parts3.length; k++) { + if (parts3[k] && !classes[parts3[k]]) { + classes[parts3[k]] = true; + result.push(parts3[k]); + } + } + } + return result; + } + + function componentIoRefs(c) { + return c.ioRefs ? c.ioRefs.slice() : []; + } + + function componentSetIoRefs(c, refs) { + c.ioRefs = refs; + } +''' + + +PLATFORM_PARSER_JS = r""" + // ========================================================================= + // Platform interface — Parser + // ========================================================================= + // Character classification derived from the grammar: + // ident-start → [a-zA-Z_~*+\-><=/!?&] + // ident-char → ident-start + [0-9.:\/\[\]#,] + + var _identStartRe = /[a-zA-Z_~*+\-><=/!?&]/; + var _identCharRe = /[a-zA-Z0-9_~*+\-><=/!?.:&/\[\]#,]/; + + function isIdentStart(ch) { return _identStartRe.test(ch); } + function isIdentChar(ch) { return _identCharRe.test(ch); } + function parseNumber(s) { return Number(s); } + function escapeString(s) { + return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t"); + } + function sxExprSource(e) { return typeof e === "string" ? e : String(e); } +""" + + +PLATFORM_DOM_JS = """ + // ========================================================================= + // Platform interface — DOM adapter (browser-only) + // ========================================================================= + + var _hasDom = typeof document !== "undefined"; + + // Register DOM adapter as the render dispatch target for the evaluator. + _renderExprFn = function(expr, env) { return renderToDom(expr, env, null); }; + _renderMode = true; // Browser always evaluates in render context. + + var SVG_NS = "http://www.w3.org/2000/svg"; + var MATH_NS = "http://www.w3.org/1998/Math/MathML"; + + function domCreateElement(tag, ns) { + if (!_hasDom) return null; + if (ns && ns !== NIL) return document.createElementNS(ns, tag); + return document.createElement(tag); + } + + function createTextNode(s) { + return _hasDom ? document.createTextNode(s) : null; + } + + function createComment(s) { + return _hasDom ? document.createComment(s || "") : null; + } + + function createFragment() { + return _hasDom ? document.createDocumentFragment() : null; + } + + function domAppend(parent, child) { + if (parent && child) parent.appendChild(child); + } + + function domPrepend(parent, child) { + if (parent && child) parent.insertBefore(child, parent.firstChild); + } + + function domSetAttr(el, name, val) { + if (el && el.setAttribute) el.setAttribute(name, val); + } + + function domGetAttr(el, name) { + if (!el || !el.getAttribute) return NIL; + var v = el.getAttribute(name); + return v === null ? NIL : v; + } + + function domRemoveAttr(el, name) { + if (el && el.removeAttribute) el.removeAttribute(name); + } + + function domHasAttr(el, name) { + return !!(el && el.hasAttribute && el.hasAttribute(name)); + } + + function domParseHtml(html) { + if (!_hasDom) return null; + var tpl = document.createElement("template"); + tpl.innerHTML = html; + return tpl.content; + } + + function domClone(node) { + return node && node.cloneNode ? node.cloneNode(true) : node; + } + + function domParent(el) { return el ? el.parentNode : null; } + function domId(el) { return el && el.id ? el.id : NIL; } + function domNodeType(el) { return el ? el.nodeType : 0; } + function domNodeName(el) { return el ? el.nodeName : ""; } + function domTextContent(el) { return el ? el.textContent || el.nodeValue || "" : ""; } + function domSetTextContent(el, s) { if (el) { if (el.nodeType === 3 || el.nodeType === 8) el.nodeValue = s; else el.textContent = s; } } + function domIsFragment(el) { return el ? el.nodeType === 11 : false; } + function domIsChildOf(child, parent) { return !!(parent && child && child.parentNode === parent); } + function domIsActiveElement(el) { return _hasDom && el === document.activeElement; } + function domIsInputElement(el) { + if (!el || !el.tagName) return false; + var t = el.tagName; + return t === "INPUT" || t === "TEXTAREA" || t === "SELECT"; + } + function domFirstChild(el) { return el ? el.firstChild : null; } + function domNextSibling(el) { return el ? el.nextSibling : null; } + + function domChildList(el) { + if (!el || !el.childNodes) return []; + return Array.prototype.slice.call(el.childNodes); + } + + function domAttrList(el) { + if (!el || !el.attributes) return []; + var r = []; + for (var i = 0; i < el.attributes.length; i++) { + r.push([el.attributes[i].name, el.attributes[i].value]); + } + return r; + } + + function domInsertBefore(parent, node, ref) { + if (parent && node) parent.insertBefore(node, ref || null); + } + + function domInsertAfter(ref, node) { + if (ref && ref.parentNode && node) { + ref.parentNode.insertBefore(node, ref.nextSibling); + } + } + + function domRemoveChild(parent, child) { + if (parent && child && child.parentNode === parent) parent.removeChild(child); + } + + function domReplaceChild(parent, newChild, oldChild) { + if (parent && newChild && oldChild) parent.replaceChild(newChild, oldChild); + } + + function domSetInnerHtml(el, html) { + if (el) el.innerHTML = html; + } + + function domInsertAdjacentHtml(el, pos, html) { + if (el && el.insertAdjacentHTML) el.insertAdjacentHTML(pos, html); + } + + function domGetStyle(el, prop) { + return el && el.style ? el.style[prop] || "" : ""; + } + + function domSetStyle(el, prop, val) { + if (el && el.style) el.style[prop] = val; + } + + function domGetProp(el, name) { return el ? el[name] : NIL; } + function domSetProp(el, name, val) { if (el) el[name] = val; } + + function domAddClass(el, cls) { + if (el && el.classList) el.classList.add(cls); + } + + function domRemoveClass(el, cls) { + if (el && el.classList) el.classList.remove(cls); + } + + function domDispatch(el, name, detail) { + if (!_hasDom || !el) return false; + var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} }); + return el.dispatchEvent(evt); + } + + function domListen(el, name, handler) { + if (!_hasDom || !el) return function() {}; + // Wrap SX lambdas from runtime-evaluated island code into native fns + // If lambda takes 0 params, call without event arg (convenience for on-click handlers) + var wrapped = isLambda(handler) + ? (lambdaParams(handler).length === 0 + ? function(e) { try { invoke(handler); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } } + : function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }) + : handler; + if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler)); + el.addEventListener(name, wrapped); + return function() { el.removeEventListener(name, wrapped); }; + } + + function eventDetail(e) { + return (e && e.detail != null) ? e.detail : nil; + } + + function domQuery(sel) { + return _hasDom ? document.querySelector(sel) : null; + } + + function domEnsureElement(sel) { + if (!_hasDom) return null; + var el = document.querySelector(sel); + if (el) return el; + // Parse #id selector → create div with that id, append to body + if (sel.charAt(0) === '#') { + el = document.createElement('div'); + el.id = sel.slice(1); + document.body.appendChild(el); + return el; + } + return null; + } + + function domQueryAll(root, sel) { + if (!root || !root.querySelectorAll) return []; + return Array.prototype.slice.call(root.querySelectorAll(sel)); + } + + function domTagName(el) { return el && el.tagName ? el.tagName : ""; } + + // Island DOM helpers + function domRemove(node) { + if (node && node.parentNode) node.parentNode.removeChild(node); + } + function domChildNodes(el) { + if (!el || !el.childNodes) return []; + return Array.prototype.slice.call(el.childNodes); + } + function domRemoveChildrenAfter(marker) { + if (!marker || !marker.parentNode) return; + var parent = marker.parentNode; + while (marker.nextSibling) parent.removeChild(marker.nextSibling); + } + function domSetData(el, key, val) { + if (el) { if (!el._sxData) el._sxData = {}; el._sxData[key] = val; } + } + function domGetData(el, key) { + return (el && el._sxData) ? (el._sxData[key] != null ? el._sxData[key] : nil) : nil; + } + function domInnerHtml(el) { + return (el && el.innerHTML != null) ? el.innerHTML : ""; + } + function jsonParse(s) { + try { return JSON.parse(s); } catch(e) { return {}; } + } + + // renderDomComponent and renderDomElement are transpiled from + // adapter-dom.sx — no imperative overrides needed. +""" + + +PLATFORM_ENGINE_PURE_JS = """ + // ========================================================================= + // Platform interface — Engine pure logic (browser + node compatible) + // ========================================================================= + + function browserLocationHref() { + return typeof location !== "undefined" ? location.href : ""; + } + + function browserSameOrigin(url) { + try { return new URL(url, location.href).origin === location.origin; } + catch (e) { return true; } + } + + function browserPushState(url) { + if (typeof history !== "undefined") { + try { history.pushState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); } + catch (e) {} + } + } + + function browserReplaceState(url) { + if (typeof history !== "undefined") { + try { history.replaceState({ sxUrl: url, scrollY: typeof window !== "undefined" ? window.scrollY : 0 }, "", url); } + catch (e) {} + } + } + + function nowMs() { return (typeof performance !== "undefined") ? performance.now() : Date.now(); } + + function parseHeaderValue(s) { + if (!s) return null; + try { + if (s.charAt(0) === "{" && s.charAt(1) === ":") return parse(s); + return JSON.parse(s); + } catch (e) { return null; } + } +""" + + +PLATFORM_ORCHESTRATION_JS = """ + // ========================================================================= + // Platform interface — Orchestration (browser-only) + // ========================================================================= + + // --- Browser/Network --- + + function browserNavigate(url) { + if (typeof location !== "undefined") location.assign(url); + } + + function browserReload() { + if (typeof location !== "undefined") location.reload(); + } + + function browserScrollTo(x, y) { + if (typeof window !== "undefined") window.scrollTo(x, y); + } + + function browserMediaMatches(query) { + if (typeof window === "undefined") return false; + return window.matchMedia(query).matches; + } + + function browserConfirm(msg) { + if (typeof window === "undefined") return false; + return window.confirm(msg); + } + + function browserPrompt(msg) { + if (typeof window === "undefined") return NIL; + var r = window.prompt(msg); + return r === null ? NIL : r; + } + + function csrfToken() { + if (!_hasDom) return NIL; + var m = document.querySelector('meta[name="csrf-token"]'); + return m ? m.getAttribute("content") : NIL; + } + + function isCrossOrigin(url) { + try { + var h = new URL(url, location.href).hostname; + return h !== location.hostname && + (h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0); + } catch (e) { return false; } + } + + // --- Promises --- + + function promiseResolve(val) { return Promise.resolve(val); } + + function promiseThen(p, onResolve, onReject) { + if (!p || !p.then) return p; + return onReject ? p.then(onResolve, onReject) : p.then(onResolve); + } + + function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; } + + function promiseDelayed(ms, value) { + return new Promise(function(resolve) { + setTimeout(function() { resolve(value); }, ms); + }); + } + + // --- Abort controllers --- + + var _controllers = typeof WeakMap !== "undefined" ? new WeakMap() : null; + + function abortPrevious(el) { + if (_controllers) { + var prev = _controllers.get(el); + if (prev) prev.abort(); + } + } + + function trackController(el, ctrl) { + if (_controllers) _controllers.set(el, ctrl); + } + + var _targetControllers = typeof WeakMap !== "undefined" ? new WeakMap() : null; + + function abortPreviousTarget(el) { + if (_targetControllers) { + var prev = _targetControllers.get(el); + if (prev) prev.abort(); + } + } + + function trackControllerTarget(el, ctrl) { + if (_targetControllers) _targetControllers.set(el, ctrl); + } + + function newAbortController() { + return typeof AbortController !== "undefined" ? new AbortController() : { signal: null, abort: function() {} }; + } + + function controllerSignal(ctrl) { return ctrl ? ctrl.signal : null; } + + function isAbortError(err) { return err && err.name === "AbortError"; } + + // --- Timers --- + + function _wrapSxFn(fn) { + if (fn && fn._lambda) { + return function() { return trampoline(callLambda(fn, [], lambdaClosure(fn))); }; + } + return fn; + } + function setTimeout_(fn, ms) { return setTimeout(_wrapSxFn(fn), ms || 0); } + function setInterval_(fn, ms) { return setInterval(_wrapSxFn(fn), ms || 1000); } + function clearTimeout_(id) { clearTimeout(id); } + function clearInterval_(id) { clearInterval(id); } + function requestAnimationFrame_(fn) { + var cb = _wrapSxFn(fn); + if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(cb); + else setTimeout(cb, 16); + } + + // --- Fetch --- + + function fetchRequest(config, successFn, errorFn) { + var opts = { method: config.method, headers: config.headers }; + if (config.signal) opts.signal = config.signal; + if (config.body && config.method !== "GET") opts.body = config.body; + if (config["cross-origin"]) opts.credentials = "include"; + + var p = (config.preloaded && config.preloaded !== NIL) + ? Promise.resolve({ + ok: true, status: 200, + headers: new Headers({ "Content-Type": config.preloaded["content-type"] || "" }), + text: function() { return Promise.resolve(config.preloaded.text); } + }) + : fetch(config.url, opts); + + return p.then(function(resp) { + return resp.text().then(function(text) { + var getHeader = function(name) { + var v = resp.headers.get(name); + return v === null ? NIL : v; + }; + return successFn(resp.ok, resp.status, getHeader, text); + }); + }).catch(function(err) { + return errorFn(err); + }); + } + + function fetchLocation(headerVal) { + if (!_hasDom) return; + var locUrl = headerVal; + try { var obj = JSON.parse(headerVal); locUrl = obj.path || obj; } catch (e) {} + fetch(locUrl, { headers: { "SX-Request": "true" } }).then(function(r) { + return r.text().then(function(t) { + var main = document.getElementById("main-panel"); + if (main) { + main.innerHTML = t; + postSwap(main); + try { history.pushState({ sxUrl: locUrl }, "", locUrl); } catch (e) {} + } + }); + }); + } + + function fetchAndRestore(main, url, headers, scrollY) { + var opts = { headers: headers }; + try { + var h = new URL(url, location.href).hostname; + if (h !== location.hostname && + (h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) { + opts.credentials = "include"; + } + } catch (e) {} + + fetch(url, opts).then(function(resp) { + return resp.text().then(function(text) { + text = stripComponentScripts(text); + text = extractResponseCss(text); + text = text.trim(); + if (text.charAt(0) === "(") { + try { + var dom = sxRender(text); + var container = document.createElement("div"); + container.appendChild(dom); + processOobSwaps(container, function(t, oob, s) { + swapDomNodes(t, oob, s); + sxHydrate(t); + processElements(t); + }); + var newMain = container.querySelector("#main-panel"); + morphChildren(main, newMain || container); + postSwap(main); + if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0); + } catch (err) { + console.error("sx-ref popstate error:", err); + location.reload(); + } + } else { + var parser = new DOMParser(); + var doc = parser.parseFromString(text, "text/html"); + var newMain = doc.getElementById("main-panel"); + if (newMain) { + morphChildren(main, newMain); + postSwap(main); + if (typeof window !== "undefined") window.scrollTo(0, scrollY || 0); + } else { + location.reload(); + } + } + }); + }).catch(function() { location.reload(); }); + } + + function fetchStreaming(target, url, headers) { + // Streaming fetch for multi-stream pages. + // First chunk = OOB SX swap (shell with skeletons). + // Subsequent chunks = __sxResolve script tags filling suspense slots. + var opts = { headers: headers }; + try { + var h = new URL(url, location.href).hostname; + if (h !== location.hostname && + (h.indexOf(".rose-ash.com") >= 0 || h.indexOf(".localhost") >= 0)) { + opts.credentials = "include"; + } + } catch (e) {} + + fetch(url, opts).then(function(resp) { + if (!resp.ok || !resp.body) { + // Fallback: non-streaming + return resp.text().then(function(text) { + text = stripComponentScripts(text); + text = extractResponseCss(text); + text = text.trim(); + if (text.charAt(0) === "(") { + var dom = sxRender(text); + var container = document.createElement("div"); + container.appendChild(dom); + processOobSwaps(container, function(t, oob, s) { + swapDomNodes(t, oob, s); + sxHydrate(t); + processElements(t); + }); + var newMain = container.querySelector("#main-panel"); + morphChildren(target, newMain || container); + postSwap(target); + } + }); + } + + var reader = resp.body.getReader(); + var decoder = new TextDecoder(); + var buffer = ""; + var initialSwapDone = false; + // Regex to match __sxResolve script tags + var RESOLVE_START = ""; + + function processResolveScripts() { + // Strip and load any extra component defs before resolve scripts + buffer = stripSxScripts(buffer); + var idx; + while ((idx = buffer.indexOf(RESOLVE_START)) >= 0) { + var endIdx = buffer.indexOf(RESOLVE_END, idx); + if (endIdx < 0) break; // incomplete, wait for more data + var argsStr = buffer.substring(idx + RESOLVE_START.length, endIdx); + buffer = buffer.substring(endIdx + RESOLVE_END.length); + // argsStr is: "stream-id","sx source" + var commaIdx = argsStr.indexOf(","); + if (commaIdx >= 0) { + try { + var id = JSON.parse(argsStr.substring(0, commaIdx)); + var sx = JSON.parse(argsStr.substring(commaIdx + 1)); + if (typeof Sx !== "undefined" && Sx.resolveSuspense) { + Sx.resolveSuspense(id, sx); + } + } catch (e) { + console.error("[sx-ref] resolve parse error:", e); + } + } + } + } + + function pump() { + return reader.read().then(function(result) { + buffer += decoder.decode(result.value || new Uint8Array(), { stream: !result.done }); + + if (!initialSwapDone) { + // Look for the first resolve script — everything before it is OOB content + var scriptIdx = buffer.indexOf(" (without data-components or data-init). + // These contain extra component defs from streaming resolve chunks. + // data-init scripts are preserved for process-sx-scripts to evaluate as side effects. + var SxObj = typeof Sx !== "undefined" ? Sx : null; + return text.replace(/]*type="text\\/sx"[^>]*>[\\s\\S]*?<\\/script>/gi, + function(match) { + if (/data-init/.test(match)) return match; // preserve data-init scripts + var m = match.match(/]*>([\\s\\S]*?)<\\/script>/i); + if (m && SxObj && SxObj.loadComponents) SxObj.loadComponents(m[1]); + return ""; + }); + } + + function extractResponseCss(text) { + if (!_hasDom) return text; + var target = document.getElementById("sx-css"); + if (!target) return text; + return text.replace(/]*data-sx-css[^>]*>([\\s\\S]*?)<\\/style>/gi, + function(_, css) { target.textContent += css; return ""; }); + } + + function selectFromContainer(container, sel) { + var frag = document.createDocumentFragment(); + sel.split(",").forEach(function(s) { + container.querySelectorAll(s.trim()).forEach(function(m) { frag.appendChild(m); }); + }); + return frag; + } + + function childrenToFragment(container) { + var frag = document.createDocumentFragment(); + while (container.firstChild) frag.appendChild(container.firstChild); + return frag; + } + + function selectHtmlFromDoc(doc, sel) { + var parts = sel.split(",").map(function(s) { return s.trim(); }); + var frags = []; + parts.forEach(function(s) { + doc.querySelectorAll(s).forEach(function(m) { frags.push(m.outerHTML); }); + }); + return frags.join(""); + } + + // --- Parsing --- + + function tryParseJson(s) { + if (!s) return NIL; + try { return JSON.parse(s); } catch (e) { return NIL; } + } +""" + + +PLATFORM_BOOT_JS = """ + // ========================================================================= + // Platform interface — Boot (mount, hydrate, scripts, cookies) + // ========================================================================= + + function resolveMountTarget(target) { + if (typeof target === "string") return _hasDom ? document.querySelector(target) : null; + return target; + } + + function sxRenderWithEnv(source, extraEnv) { + var env = extraEnv ? merge(componentEnv, extraEnv) : componentEnv; + var exprs = parse(source); + if (!_hasDom) return null; + var frag = document.createDocumentFragment(); + for (var i = 0; i < exprs.length; i++) { + var node = renderToDom(exprs[i], env, null); + if (node) frag.appendChild(node); + } + return frag; + } + + function getRenderEnv(extraEnv) { + return extraEnv ? merge(componentEnv, extraEnv) : componentEnv; + } + + function mergeEnvs(base, newEnv) { + return newEnv ? merge(componentEnv, base, newEnv) : merge(componentEnv, base); + } + + function sxLoadComponents(text) { + try { + var exprs = parse(text); + for (var i = 0; i < exprs.length; i++) trampoline(evalExpr(exprs[i], componentEnv)); + } catch (err) { + logParseError("loadComponents", text, err); + throw err; + } + } + + function setDocumentTitle(s) { + if (_hasDom) document.title = s || ""; + } + + function removeHeadElement(sel) { + if (!_hasDom) return; + var old = document.head.querySelector(sel); + if (old) old.parentNode.removeChild(old); + } + + function querySxScripts(root) { + if (!_hasDom) return []; + var r = (root && root !== NIL) ? root : document; + return Array.prototype.slice.call( + r.querySelectorAll('script[type="text/sx"]')); + } + + function queryPageScripts() { + if (!_hasDom) return []; + return Array.prototype.slice.call( + document.querySelectorAll('script[type="text/sx-pages"]')); + } + + // --- localStorage --- + + function localStorageGet(key) { + try { var v = localStorage.getItem(key); return v === null ? NIL : v; } + catch (e) { return NIL; } + } + + function localStorageSet(key, val) { + try { localStorage.setItem(key, val); } catch (e) {} + } + + function localStorageRemove(key) { + try { localStorage.removeItem(key); } catch (e) {} + } + + // --- Cookies --- + + function setSxCompCookie(hash) { + if (_hasDom) document.cookie = "sx-comp-hash=" + hash + ";path=/;max-age=31536000;SameSite=Lax"; + } + + function clearSxCompCookie() { + if (_hasDom) document.cookie = "sx-comp-hash=;path=/;max-age=0;SameSite=Lax"; + } + + // --- Env helpers --- + + function parseEnvAttr(el) { + var attr = el && el.getAttribute ? el.getAttribute("data-sx-env") : null; + if (!attr) return {}; + try { return JSON.parse(attr); } catch (e) { return {}; } + } + + function storeEnvAttr(el, base, newEnv) { + var merged = merge(base, newEnv); + if (el && el.setAttribute) el.setAttribute("data-sx-env", JSON.stringify(merged)); + } + + function toKebab(s) { return s.replace(/_/g, "-"); } + + // --- Logging --- + + function logInfo(msg) { + if (typeof console !== "undefined") console.log("[sx-ref] " + msg); + } + + function logWarn(msg) { + if (typeof console !== "undefined") console.warn("[sx-ref] " + msg); + } + + function logParseError(label, text, err) { + if (typeof console === "undefined") return; + var msg = err && err.message ? err.message : String(err); + var colMatch = msg.match(/col (\\d+)/); + var lineMatch = msg.match(/line (\\d+)/); + if (colMatch && text) { + var errLine = lineMatch ? parseInt(lineMatch[1]) : 1; + var errCol = parseInt(colMatch[1]); + var lines = text.split("\\n"); + var pos = 0; + for (var i = 0; i < errLine - 1 && i < lines.length; i++) pos += lines[i].length + 1; + pos += errCol; + var ws = 80; + var start = Math.max(0, pos - ws); + var end = Math.min(text.length, pos + ws); + console.error("[sx-ref] " + label + ":", msg, + "\\n around error (pos ~" + pos + "):", + "\\n \\u00ab" + text.substring(start, pos) + "\\u26d4" + text.substring(pos, end) + "\\u00bb"); + } else { + console.error("[sx-ref] " + label + ":", msg); + } + } + +""" + + +def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_page_helpers=False): + lines = [''' + // ========================================================================= + // Post-transpilation fixups + // ========================================================================= + // The reference spec's call-lambda only handles Lambda objects, but HO forms + // (map, reduce, etc.) may receive native primitives. Wrap to handle both. + var _rawCallLambda = callLambda; + callLambda = function(f, args, callerEnv) { + if (typeof f === "function") return f.apply(null, args); + return _rawCallLambda(f, args, callerEnv); + }; + + // Expose render functions as primitives so SX code can call them'''] + if has_html: + lines.append(' if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml;') + if has_sx: + lines.append(' if (typeof renderToSx === "function") PRIMITIVES["render-to-sx"] = renderToSx;') + lines.append(' if (typeof aser === "function") PRIMITIVES["aser"] = aser;') + if has_dom: + lines.append(' if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;') + if has_signals: + lines.append(''' + // Expose signal functions as primitives so runtime-evaluated SX code + // (e.g. island bodies from .sx files) can call them + PRIMITIVES["signal"] = signal; + PRIMITIVES["signal?"] = isSignal; + PRIMITIVES["deref"] = deref; + PRIMITIVES["reset!"] = reset_b; + PRIMITIVES["swap!"] = swap_b; + PRIMITIVES["computed"] = computed; + PRIMITIVES["effect"] = effect; + PRIMITIVES["batch"] = batch; + // Timer primitives for island code + PRIMITIVES["set-interval"] = setInterval_; + PRIMITIVES["clear-interval"] = clearInterval_; + // Reactive DOM helpers for island code + PRIMITIVES["reactive-text"] = reactiveText; + PRIMITIVES["create-text-node"] = createTextNode; + PRIMITIVES["dom-set-text-content"] = domSetTextContent; + PRIMITIVES["dom-listen"] = domListen; + PRIMITIVES["dom-dispatch"] = domDispatch; + PRIMITIVES["event-detail"] = eventDetail; + PRIMITIVES["resource"] = resource; + PRIMITIVES["promise-delayed"] = promiseDelayed; + PRIMITIVES["promise-then"] = promiseThen; + PRIMITIVES["def-store"] = defStore; + PRIMITIVES["use-store"] = useStore; + PRIMITIVES["emit-event"] = emitEvent; + PRIMITIVES["on-event"] = onEvent; + PRIMITIVES["bridge-event"] = bridgeEvent; + // DOM primitives for island code + PRIMITIVES["dom-focus"] = domFocus; + PRIMITIVES["dom-tag-name"] = domTagName; + PRIMITIVES["dom-get-prop"] = domGetProp; + PRIMITIVES["stop-propagation"] = stopPropagation_; + PRIMITIVES["error-message"] = errorMessage; + PRIMITIVES["schedule-idle"] = scheduleIdle; + PRIMITIVES["invoke"] = invoke; + PRIMITIVES["error"] = function(msg) { throw new Error(msg); }; + PRIMITIVES["filter"] = filter; + // DOM primitives for sx-on:* handlers and data-init scripts + if (typeof domBody === "function") PRIMITIVES["dom-body"] = domBody; + if (typeof domQuery === "function") PRIMITIVES["dom-query"] = domQuery; + if (typeof domQueryAll === "function") PRIMITIVES["dom-query-all"] = domQueryAll; + if (typeof domQueryById === "function") PRIMITIVES["dom-query-by-id"] = domQueryById; + if (typeof domSetAttr === "function") PRIMITIVES["dom-set-attr"] = domSetAttr; + if (typeof domGetAttr === "function") PRIMITIVES["dom-get-attr"] = domGetAttr; + if (typeof domRemoveAttr === "function") PRIMITIVES["dom-remove-attr"] = domRemoveAttr; + if (typeof domHasAttr === "function") PRIMITIVES["dom-has-attr?"] = domHasAttr; + if (typeof domAddClass === "function") PRIMITIVES["dom-add-class"] = domAddClass; + if (typeof domRemoveClass === "function") PRIMITIVES["dom-remove-class"] = domRemoveClass; + if (typeof domHasClass === "function") PRIMITIVES["dom-has-class?"] = domHasClass; + if (typeof domClosest === "function") PRIMITIVES["dom-closest"] = domClosest; + if (typeof domMatches === "function") PRIMITIVES["dom-matches?"] = domMatches; + if (typeof preventDefault_ === "function") PRIMITIVES["prevent-default"] = preventDefault_; + if (typeof elementValue === "function") PRIMITIVES["element-value"] = elementValue; + if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml; + if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml; + if (typeof domTextContent === "function") PRIMITIVES["dom-text-content"] = domTextContent; + if (typeof jsonParse === "function") PRIMITIVES["json-parse"] = jsonParse; + if (typeof nowMs === "function") PRIMITIVES["now-ms"] = nowMs; + PRIMITIVES["sx-parse"] = sxParse;''') + if has_deps: + lines.append(''' + // Expose deps module functions as primitives so runtime-evaluated SX code + // (e.g. test-deps.sx in browser) can call them + // Platform functions (from PLATFORM_DEPS_JS) + PRIMITIVES["component-deps"] = componentDeps; + PRIMITIVES["component-set-deps!"] = componentSetDeps; + PRIMITIVES["component-css-classes"] = componentCssClasses; + PRIMITIVES["env-components"] = envComponents; + PRIMITIVES["regex-find-all"] = regexFindAll; + PRIMITIVES["scan-css-classes"] = scanCssClasses; + // Transpiled functions (from deps.sx) + PRIMITIVES["scan-refs"] = scanRefs; + PRIMITIVES["scan-refs-walk"] = scanRefsWalk; + PRIMITIVES["transitive-deps"] = transitiveDeps; + PRIMITIVES["transitive-deps-walk"] = transitiveDepsWalk; + PRIMITIVES["compute-all-deps"] = computeAllDeps; + PRIMITIVES["scan-components-from-source"] = scanComponentsFromSource; + PRIMITIVES["components-needed"] = componentsNeeded; + PRIMITIVES["page-component-bundle"] = pageComponentBundle; + PRIMITIVES["page-css-classes"] = pageCssClasses; + PRIMITIVES["scan-io-refs-walk"] = scanIoRefsWalk; + PRIMITIVES["scan-io-refs"] = scanIoRefs; + PRIMITIVES["transitive-io-refs-walk"] = transitiveIoRefsWalk; + PRIMITIVES["transitive-io-refs"] = transitiveIoRefs; + PRIMITIVES["compute-all-io-refs"] = computeAllIoRefs; + PRIMITIVES["component-io-refs-cached"] = componentIoRefsCached; + PRIMITIVES["component-pure?"] = componentPure_p; + PRIMITIVES["render-target"] = renderTarget; + PRIMITIVES["page-render-plan"] = pageRenderPlan;''') + if has_page_helpers: + lines.append(''' + // 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;''') + return "\n".join(lines) + + +def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps=False, has_router=False, has_signals=False, has_page_helpers=False): + # Parser: use compiled sxParse from parser.sx, or inline a minimal fallback + if has_parser: + parser = ''' + // Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes) + var parse = sxParse;''' + else: + parser = r''' + // Minimal fallback parser (no parser adapter) + function parse(text) { + throw new Error("Parser adapter not included — cannot parse SX source at runtime"); + }''' + + # Public API — conditional on adapters + api_lines = [parser, ''' + // ========================================================================= + // Public API + // ========================================================================= + + var componentEnv = {}; + + function loadComponents(source) { + var exprs = parse(source); + for (var i = 0; i < exprs.length; i++) { + trampoline(evalExpr(exprs[i], componentEnv)); + } + }'''] + + # render() — auto-dispatches based on available adapters + if has_html and has_dom: + api_lines.append(''' + function render(source) { + if (!_hasDom) { + var exprs = parse(source); + var parts = []; + for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv))); + return parts.join(""); + } + var exprs = parse(source); + var frag = document.createDocumentFragment(); + for (var i = 0; i < exprs.length; i++) frag.appendChild(renderToDom(exprs[i], merge(componentEnv), null)); + return frag; + }''') + elif has_dom: + api_lines.append(''' + function render(source) { + var exprs = parse(source); + var frag = document.createDocumentFragment(); + for (var i = 0; i < exprs.length; i++) frag.appendChild(renderToDom(exprs[i], merge(componentEnv), null)); + return frag; + }''') + elif has_html: + api_lines.append(''' + function render(source) { + var exprs = parse(source); + var parts = []; + for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv))); + return parts.join(""); + }''') + else: + api_lines.append(''' + function render(source) { + var exprs = parse(source); + var results = []; + for (var i = 0; i < exprs.length; i++) results.push(trampoline(evalExpr(exprs[i], merge(componentEnv)))); + return results.length === 1 ? results[0] : results; + }''') + + # renderToString helper + if has_html: + api_lines.append(''' + function renderToString(source) { + var exprs = parse(source); + var parts = []; + for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv))); + return parts.join(""); + }''') + + # Build Sx object + version = f"ref-2.0 ({adapter_label}, bootstrap-compiled)" + api_lines.append(f''' + var Sx = {{ + VERSION: "ref-2.0", + parse: parse, + parseAll: parse, + eval: function(expr, env) {{ return trampoline(evalExpr(expr, env || merge(componentEnv))); }}, + loadComponents: loadComponents, + render: render,{"" if has_html else ""} + {"renderToString: renderToString," if has_html else ""} + serialize: serialize, + NIL: NIL, + Symbol: Symbol, + Keyword: Keyword, + isTruthy: isSxTruthy, + isNil: isNil, + componentEnv: componentEnv,''') + + if has_html: + api_lines.append(' renderToHtml: function(expr, env) { return renderToHtml(expr, env || merge(componentEnv)); },') + if has_sx: + api_lines.append(' renderToSx: function(expr, env) { return renderToSx(expr, env || merge(componentEnv)); },') + if has_dom: + api_lines.append(' renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,') + if has_engine: + api_lines.append(' parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,') + api_lines.append(' parseTime: typeof parseTime === "function" ? parseTime : null,') + api_lines.append(' defaultTrigger: typeof defaultTrigger === "function" ? defaultTrigger : null,') + api_lines.append(' parseSwapSpec: typeof parseSwapSpec === "function" ? parseSwapSpec : null,') + api_lines.append(' parseRetrySpec: typeof parseRetrySpec === "function" ? parseRetrySpec : null,') + api_lines.append(' nextRetryMs: typeof nextRetryMs === "function" ? nextRetryMs : null,') + api_lines.append(' filterParams: typeof filterParams === "function" ? filterParams : null,') + api_lines.append(' morphNode: typeof morphNode === "function" ? morphNode : null,') + api_lines.append(' morphChildren: typeof morphChildren === "function" ? morphChildren : null,') + api_lines.append(' swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,') + if has_orch: + api_lines.append(' process: typeof processElements === "function" ? processElements : null,') + api_lines.append(' executeRequest: typeof executeRequest === "function" ? executeRequest : null,') + api_lines.append(' postSwap: typeof postSwap === "function" ? postSwap : null,') + if has_boot: + api_lines.append(' processScripts: typeof processSxScripts === "function" ? processSxScripts : null,') + api_lines.append(' mount: typeof sxMount === "function" ? sxMount : null,') + api_lines.append(' hydrate: typeof sxHydrateElements === "function" ? sxHydrateElements : null,') + api_lines.append(' update: typeof sxUpdateElement === "function" ? sxUpdateElement : null,') + api_lines.append(' renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null,') + api_lines.append(' getEnv: function() { return componentEnv; },') + api_lines.append(' resolveSuspense: typeof resolveSuspense === "function" ? resolveSuspense : null,') + api_lines.append(' hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null,') + api_lines.append(' disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null,') + api_lines.append(' init: typeof bootInit === "function" ? bootInit : null,') + elif has_orch: + api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,') + if has_deps: + api_lines.append(' scanRefs: scanRefs,') + api_lines.append(' scanComponentsFromSource: scanComponentsFromSource,') + api_lines.append(' transitiveDeps: transitiveDeps,') + api_lines.append(' computeAllDeps: computeAllDeps,') + api_lines.append(' componentsNeeded: componentsNeeded,') + api_lines.append(' pageComponentBundle: pageComponentBundle,') + api_lines.append(' pageCssClasses: pageCssClasses,') + api_lines.append(' scanIoRefs: scanIoRefs,') + api_lines.append(' transitiveIoRefs: transitiveIoRefs,') + api_lines.append(' computeAllIoRefs: computeAllIoRefs,') + api_lines.append(' componentPure_p: componentPure_p,') + if has_page_helpers: + api_lines.append(' categorizeSpecialForms: categorizeSpecialForms,') + api_lines.append(' buildReferenceData: buildReferenceData,') + api_lines.append(' buildAttrDetail: buildAttrDetail,') + api_lines.append(' buildHeaderDetail: buildHeaderDetail,') + api_lines.append(' buildEventDetail: buildEventDetail,') + api_lines.append(' buildComponentSource: buildComponentSource,') + api_lines.append(' buildBundleAnalysis: buildBundleAnalysis,') + api_lines.append(' buildRoutingAnalysis: buildRoutingAnalysis,') + api_lines.append(' buildAffinityAnalysis: buildAffinityAnalysis,') + if has_router: + api_lines.append(' splitPathSegments: splitPathSegments,') + api_lines.append(' parseRoutePattern: parseRoutePattern,') + api_lines.append(' matchRoute: matchRoute,') + api_lines.append(' findMatchingRoute: findMatchingRoute,') + + if has_dom: + api_lines.append(' registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null,') + api_lines.append(' registerIoDeps: typeof registerIoDeps === "function" ? registerIoDeps : null,') + api_lines.append(' asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,') + api_lines.append(' asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null,') + if has_signals: + api_lines.append(' signal: signal,') + api_lines.append(' deref: deref,') + api_lines.append(' reset: reset_b,') + api_lines.append(' swap: swap_b,') + api_lines.append(' computed: computed,') + api_lines.append(' effect: effect,') + api_lines.append(' batch: batch,') + api_lines.append(' isSignal: isSignal,') + api_lines.append(' makeSignal: makeSignal,') + api_lines.append(' defStore: defStore,') + api_lines.append(' useStore: useStore,') + api_lines.append(' clearStores: clearStores,') + api_lines.append(' emitEvent: emitEvent,') + api_lines.append(' onEvent: onEvent,') + api_lines.append(' bridgeEvent: bridgeEvent,') + api_lines.append(f' _version: "{version}"') + api_lines.append(' };') + api_lines.append('') + if has_orch: + api_lines.append(''' + // --- Popstate listener --- + if (typeof window !== "undefined") { + window.addEventListener("popstate", function(e) { + handlePopstate(e && e.state ? e.state.scrollY || 0 : 0); + }); + }''') + if has_boot: + api_lines.append(''' + // --- Auto-init --- + if (typeof document !== "undefined") { + var _sxInit = function() { + bootInit(); + // Process any suspense resolutions that arrived before init + if (global.__sxPending) { + for (var pi = 0; pi < global.__sxPending.length; pi++) { + resolveSuspense(global.__sxPending[pi].id, global.__sxPending[pi].sx); + } + global.__sxPending = null; + } + // Set up direct resolution for future chunks + global.__sxResolve = function(id, sx) { resolveSuspense(id, sx); }; + // Register service worker for offline data caching + if ("serviceWorker" in navigator) { + navigator.serviceWorker.register("/sx-sw.js", { scope: "/" }).then(function(reg) { + logInfo("sx:sw registered (scope: " + reg.scope + ")"); + }).catch(function(err) { + logWarn("sx:sw registration failed: " + (err && err.message ? err.message : err)); + }); + } + }; + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", _sxInit); + } else { + _sxInit(); + } + }''') + elif has_orch: + api_lines.append(''' + // --- Auto-init --- + if (typeof document !== "undefined") { + var _sxInit = function() { engineInit(); }; + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", _sxInit); + } else { + _sxInit(); + } + }''') + + api_lines.append(' if (typeof module !== "undefined" && module.exports) module.exports = Sx;') + api_lines.append(' else global.Sx = Sx;') + + return "\n".join(api_lines) + + +EPILOGUE = ''' +})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);''' diff --git a/shared/sx/ref/platform_py.py b/shared/sx/ref/platform_py.py index b1d1d65a..e2ccec07 100644 --- a/shared/sx/ref/platform_py.py +++ b/shared/sx/ref/platform_py.py @@ -470,10 +470,7 @@ def invoke(f, *args): def json_serialize(obj): import json - try: - return json.dumps(obj) - except (TypeError, ValueError): - return "{}" + return json.dumps(obj) 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 = [ '', '# =========================================================================', @@ -1427,8 +1425,9 @@ 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"), + "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)"), } diff --git a/shared/sx/ref/reader_z3.py b/shared/sx/ref/reader_z3.py index f5c76239..8ab2297a 100644 --- a/shared/sx/ref/reader_z3.py +++ b/shared/sx/ref/reader_z3.py @@ -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) diff --git a/shared/sx/ref/render.sx b/shared/sx/ref/render.sx index 66384c5b..f967eadf 100644 --- a/shared/sx/ref/render.sx +++ b/shared/sx/ref/render.sx @@ -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)) diff --git a/shared/sx/ref/run_js_sx.py b/shared/sx/ref/run_js_sx.py index ebe478b6..a5a92ed4 100644 --- a/shared/sx/ref/run_js_sx.py +++ b/shared/sx/ref/run_js_sx.py @@ -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") diff --git a/shared/sx/ref/run_py_sx.py b/shared/sx/ref/run_py_sx.py index ab170f5f..54927cf6 100644 --- a/shared/sx/ref/run_py_sx.py +++ b/shared/sx/ref/run_py_sx.py @@ -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({}) diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index 5cd4025d..d937b2da 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -266,14 +266,6 @@ def component_affinity(c): return getattr(c, 'affinity', 'auto') -def component_param_types(c): - return getattr(c, 'param_types', None) - - -def component_set_param_types(c, d): - c.param_types = d - - def macro_params(m): return m.params @@ -429,10 +421,7 @@ def invoke(f, *args): def json_serialize(obj): import json - try: - return json.dumps(obj) - except (TypeError, ValueError): - return "{}" + return json.dumps(obj) def is_empty_dict(d): @@ -567,7 +556,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 @@ -976,6 +965,191 @@ def component_set_io_refs(c, refs): c.io_refs = set(refs) if not isinstance(refs, set) else refs +# ========================================================================= +# Platform interface -- Async adapter +# ========================================================================= + +import contextvars +import inspect + +from shared.sx.primitives_io import ( + IO_PRIMITIVES, RequestContext, execute_io, +) + +# 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 +) + + +class _AsyncThunk: + __slots__ = ("expr", "env", "ctx") + def __init__(self, expr, env, ctx): + self.expr = expr + self.env = env + self.ctx = ctx + + +def io_primitive_p(name): + return name in IO_PRIMITIVES + + +def expand_components_p(): + return _expand_components_cv.get() + + +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()) + + +def is_raw_html(x): + return isinstance(x, _RawHTML) + + +def make_sx_expr(s): + return SxExpr(s) + + +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) + + +async def async_await(x): + return await x + + +async def _async_trampoline(val): + while isinstance(val, _AsyncThunk): + val = await async_eval(val.expr, val.env, val.ctx) + return val + + +async def async_eval(expr, env, ctx=None): + """Evaluate with I/O primitives. Entry point for async evaluation.""" + if ctx is None: + ctx = RequestContext() + result = await _async_eval_inner(expr, env, ctx) + while isinstance(result, _AsyncThunk): + result = await _async_eval_inner(result.expr, result.env, result.ctx) + return result + + +async def _async_eval_inner(expr, env, ctx): + """Intercept I/O primitives, delegate everything else to sync eval.""" + if isinstance(expr, list) and expr: + head = expr[0] + if isinstance(head, Symbol) and head.name in IO_PRIMITIVES: + args_list, kwargs = await _parse_io_args(expr[1:], env, ctx) + return await execute_io(head.name, args_list, kwargs, ctx) + is_render = isinstance(expr, list) and is_render_expr(expr) + result = eval_expr(expr, env) + result = trampoline(result) + if is_render and isinstance(result, str): + return _RawHTML(result) + return result + + +async def _parse_io_args(exprs, env, ctx): + """Parse and evaluate I/O node args (keyword + positional).""" + from shared.sx.types import Keyword as _Kw + args_list = [] + kwargs = {} + i = 0 + while i < len(exprs): + item = exprs[i] + if isinstance(item, _Kw) and i + 1 < len(exprs): + kwargs[item.name] = await async_eval(exprs[i + 1], env, ctx) + i += 2 + else: + args_list.append(await async_eval(item, env, ctx)) + i += 1 + return args_list, kwargs + + +async def async_eval_to_sx(expr, env, ctx=None): + """Evaluate and produce SX source string (wire format).""" + if ctx is None: + ctx = RequestContext() + result = await async_aser(expr, env, ctx) + if isinstance(result, SxExpr): + return result + if result is None or result is NIL: + return SxExpr("") + if isinstance(result, str): + return SxExpr(result) + return SxExpr(sx_serialize(result)) + + +async def async_eval_slot_to_sx(expr, env, ctx=None): + """Like async_eval_to_sx but expands component calls server-side.""" + if ctx is None: + ctx = RequestContext() + token = _expand_components_cv.set(True) + try: + 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) + + # === Transpiled from eval === # trampoline @@ -1183,9 +1357,13 @@ def sf_when(args, env): else: return NIL +# cond-scheme? +def cond_scheme_p(clauses): + return every_p(lambda c: ((type_of(c) == 'list') if not sx_truthy((type_of(c) == 'list')) else (len(c) == 2)), clauses) + # sf-cond def sf_cond(args, env): - if sx_truthy(((type_of(first(args)) == 'list') if not sx_truthy((type_of(first(args)) == 'list')) else (len(first(args)) == 2))): + if sx_truthy(cond_scheme_p(args)): return sf_cond_scheme(args, env) else: return sf_cond_clojure(args, env) @@ -1275,7 +1453,13 @@ def sf_let(args, env): bindings = first(args) body = rest(args) local = env_extend(env) - (for_each(lambda binding: (lambda vname: _sx_dict_set(local, vname, trampoline(eval_expr(nth(binding, 1), local))))((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else (lambda i: reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_dict_set(local, vname, trampoline(eval_expr(val_expr, local))))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2))))(0)) + if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))): + for binding in bindings: + vname = (symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding)) + local[vname] = trampoline(eval_expr(nth(binding, 1), local)) + else: + i = 0 + reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_dict_set(local, vname, trampoline(eval_expr(val_expr, local))))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2))) for e in slice(body, 0, (len(body) - 1)): trampoline(eval_expr(e, local)) return make_thunk(last(body), local) @@ -1287,10 +1471,12 @@ def sf_named_let(args, env): body = slice(args, 2) params = [] inits = [] - (for_each(_sx_fn(lambda binding: ( - _sx_append(params, (symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), - _sx_append(inits, nth(binding, 1)) -)[-1]), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else reduce(lambda acc, pair_idx: _sx_begin(_sx_append(params, (symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), _sx_append(inits, nth(bindings, ((pair_idx * 2) + 1)))), NIL, range(0, (len(bindings) / 2)))) + if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))): + for binding in bindings: + params.append((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))) + inits.append(nth(binding, 1)) + else: + reduce(lambda acc, pair_idx: _sx_begin(_sx_append(params, (symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), _sx_append(inits, nth(bindings, ((pair_idx * 2) + 1)))), NIL, range(0, (len(bindings) / 2))) loop_body = (first(body) if sx_truthy((len(body) == 1)) else cons(make_symbol('begin'), body)) loop_fn = make_lambda(params, loop_body, env) loop_fn.name = loop_name @@ -1456,7 +1642,14 @@ def sf_letrec(args, env): local = env_extend(env) names = [] val_exprs = [] - (for_each(lambda binding: (lambda vname: _sx_begin(_sx_append(names, vname), _sx_append(val_exprs, nth(binding, 1)), _sx_dict_set(local, vname, NIL)))((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_begin(_sx_append(names, vname), _sx_append(val_exprs, val_expr), _sx_dict_set(local, vname, NIL)))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2)))) + if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))): + for binding in bindings: + vname = (symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding)) + names.append(vname) + val_exprs.append(nth(binding, 1)) + local[vname] = NIL + else: + reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_begin(_sx_append(names, vname), _sx_append(val_exprs, val_expr), _sx_dict_set(local, vname, NIL)))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2))) values = map(lambda e: trampoline(eval_expr(e, local)), val_exprs) for pair in zip(names, values): local[first(pair)] = nth(pair, 1) @@ -1539,7 +1732,9 @@ def ho_every(args, env): def ho_for_each(args, env): f = trampoline(eval_expr(first(args), env)) coll = trampoline(eval_expr(nth(args, 1), env)) - return for_each(lambda item: call_fn(f, [item], env), coll) + for item in coll: + call_fn(f, [item], env) + return NIL # === Transpiled from forms (server definition forms) === @@ -1668,7 +1863,7 @@ def render_attrs(attrs): # eval-cond def eval_cond(clauses, env): - if sx_truthy(((not sx_truthy(empty_p(clauses))) if not sx_truthy((not sx_truthy(empty_p(clauses)))) else ((type_of(first(clauses)) == 'list') if not sx_truthy((type_of(first(clauses)) == 'list')) else (len(first(clauses)) == 2)))): + if sx_truthy(cond_scheme_p(clauses)): return eval_cond_scheme(clauses, env) else: return eval_cond_clojure(clauses, env) @@ -1706,7 +1901,7 @@ def eval_cond_clojure(clauses, env): # process-bindings def process_bindings(bindings, env): - local = merge(env) + local = env_extend(env) for pair in bindings: if sx_truthy(((type_of(pair) == 'list') if not sx_truthy((type_of(pair) == 'list')) else (len(pair) >= 2))): name = (symbol_name(first(pair)) if sx_truthy((type_of(first(pair)) == 'symbol')) else sx_str(first(pair))) @@ -1928,15 +2123,15 @@ def render_html_island(island, args, env): if sx_truthy(component_has_children(island)): local['children'] = make_raw_html(join('', map(lambda c: render_to_html(c, env), children))) body_html = render_to_html(component_body(island), local) - state_json = serialize_island_state(kwargs) - return sx_str('', body_html, '') + state_sx = serialize_island_state(kwargs) + return sx_str('', body_html, '') # serialize-island-state def serialize_island_state(kwargs): if sx_truthy(is_empty_dict(kwargs)): return NIL else: - return json_serialize(kwargs) + return sx_serialize(kwargs) # === Transpiled from adapter-sx === @@ -2023,16 +2218,49 @@ def aser_list(expr, env): # aser-fragment def aser_fragment(children, env): - parts = filter(lambda x: (not sx_truthy(is_nil(x))), map(lambda c: aser(c, env), children)) + parts = [] + for c in children: + result = aser(c, env) + if sx_truthy((type_of(result) == 'list')): + for item in result: + if sx_truthy((not sx_truthy(is_nil(item)))): + parts.append(serialize(item)) + else: + if sx_truthy((not sx_truthy(is_nil(result)))): + parts.append(serialize(result)) if sx_truthy(empty_p(parts)): return '' else: - return sx_str('(<> ', join(' ', map(serialize, parts)), ')') + return sx_str('(<> ', join(' ', parts), ')') # aser-call def aser_call(name, args, env): + _cells = {} parts = [name] - reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda val: _sx_begin((_sx_begin(_sx_append(parts, sx_str(':', keyword_name(arg))), _sx_append(parts, serialize(val))) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(aser(nth(args, (get(state, 'i') + 1)), env)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else (lambda val: _sx_begin((_sx_append(parts, serialize(val)) if sx_truthy((not sx_truthy(is_nil(val)))) else NIL), assoc(state, 'i', (get(state, 'i') + 1))))(aser(arg, env)))))(get(state, 'skip')), {'i': 0, 'skip': False}, args) + _cells['skip'] = False + _cells['i'] = 0 + for arg in args: + if sx_truthy(_cells['skip']): + _cells['skip'] = False + _cells['i'] = (_cells['i'] + 1) + else: + if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))): + val = aser(nth(args, (_cells['i'] + 1)), env) + if sx_truthy((not sx_truthy(is_nil(val)))): + parts.append(sx_str(':', keyword_name(arg))) + parts.append(serialize(val)) + _cells['skip'] = True + _cells['i'] = (_cells['i'] + 1) + else: + val = aser(arg, env) + if sx_truthy((not sx_truthy(is_nil(val)))): + if sx_truthy((type_of(val) == 'list')): + for item in val: + if sx_truthy((not sx_truthy(is_nil(item)))): + parts.append(serialize(item)) + else: + parts.append(serialize(val)) + _cells['i'] = (_cells['i'] + 1) return sx_str('(', join(' ', parts), ')') # SPECIAL_FORM_NAMES @@ -2170,9 +2398,13 @@ def scan_refs_walk(node, refs): return NIL return NIL elif sx_truthy((type_of(node) == 'list')): - return for_each(lambda item: scan_refs_walk(item, refs), node) + for item in node: + scan_refs_walk(item, refs) + return NIL elif sx_truthy((type_of(node) == 'dict')): - return for_each(lambda key: scan_refs_walk(dict_get(node, key), refs), keys(node)) + for key in keys(node): + scan_refs_walk(dict_get(node, key), refs) + return NIL else: return NIL @@ -2182,9 +2414,13 @@ def transitive_deps_walk(n, seen, env): seen.append(n) val = env_get(env, n) if sx_truthy((type_of(val) == 'component')): - return for_each(lambda ref: transitive_deps_walk(ref, seen, env), scan_refs(component_body(val))) + for ref in scan_refs(component_body(val)): + transitive_deps_walk(ref, seen, env) + return NIL elif sx_truthy((type_of(val) == 'macro')): - return for_each(lambda ref: transitive_deps_walk(ref, seen, env), scan_refs(macro_body(val))) + for ref in scan_refs(macro_body(val)): + transitive_deps_walk(ref, seen, env) + return NIL else: return NIL return NIL @@ -2198,7 +2434,11 @@ def transitive_deps(name, env): # compute-all-deps def compute_all_deps(env): - return for_each(lambda name: (lambda val: (component_set_deps(val, transitive_deps(name, env)) if sx_truthy((type_of(val) == 'component')) else NIL))(env_get(env, name)), env_components(env)) + for name in env_components(env): + val = env_get(env, name) + if sx_truthy((type_of(val) == 'component')): + component_set_deps(val, transitive_deps(name, env)) + return NIL # scan-components-from-source def scan_components_from_source(source): @@ -2248,9 +2488,13 @@ def scan_io_refs_walk(node, io_names, refs): return NIL return NIL elif sx_truthy((type_of(node) == 'list')): - return for_each(lambda item: scan_io_refs_walk(item, io_names, refs), node) + for item in node: + scan_io_refs_walk(item, io_names, refs) + return NIL elif sx_truthy((type_of(node) == 'dict')): - return for_each(lambda key: scan_io_refs_walk(dict_get(node, key), io_names, refs), keys(node)) + for key in keys(node): + scan_io_refs_walk(dict_get(node, key), io_names, refs) + return NIL else: return NIL @@ -2269,12 +2513,16 @@ def transitive_io_refs_walk(n, seen, all_refs, env, io_names): for ref in scan_io_refs(component_body(val), io_names): if sx_truthy((not sx_truthy(contains_p(all_refs, ref)))): all_refs.append(ref) - return for_each(lambda dep: transitive_io_refs_walk(dep, seen, all_refs, env, io_names), scan_refs(component_body(val))) + for dep in scan_refs(component_body(val)): + transitive_io_refs_walk(dep, seen, all_refs, env, io_names) + return NIL elif sx_truthy((type_of(val) == 'macro')): for ref in scan_io_refs(macro_body(val), io_names): if sx_truthy((not sx_truthy(contains_p(all_refs, ref)))): all_refs.append(ref) - return for_each(lambda dep: transitive_io_refs_walk(dep, seen, all_refs, env, io_names), scan_refs(macro_body(val))) + for dep in scan_refs(macro_body(val)): + transitive_io_refs_walk(dep, seen, all_refs, env, io_names) + return NIL else: return NIL return NIL @@ -2289,7 +2537,11 @@ def transitive_io_refs(name, env, io_names): # compute-all-io-refs def compute_all_io_refs(env, io_names): - return for_each(lambda name: (lambda val: (component_set_io_refs(val, transitive_io_refs(name, env, io_names)) if sx_truthy((type_of(val) == 'component')) else NIL))(env_get(env, name)), env_components(env)) + for name in env_components(env): + val = env_get(env, name) + if sx_truthy((type_of(val) == 'component')): + component_set_io_refs(val, transitive_io_refs(name, env, io_names)) + return NIL # component-io-refs-cached def component_io_refs_cached(name, env, io_names): @@ -2350,6 +2602,149 @@ def env_components(env): return filter(lambda k: (lambda v: (is_component(v) if sx_truthy(is_component(v)) else is_macro(v)))(env_get(env, k)), keys(env)) +# === Transpiled from page-helpers (pure data transformation helpers) === + +# special-form-category-map +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'} + +# extract-define-kwargs +def extract_define_kwargs(expr): + result = {} + items = slice(expr, 2) + n = len(items) + for idx in range(0, n): + if sx_truthy((((idx + 1) < n) if not sx_truthy(((idx + 1) < n)) else (type_of(nth(items, idx)) == 'keyword'))): + key = keyword_name(nth(items, idx)) + val = nth(items, (idx + 1)) + result[key] = (sx_str('(', join(' ', map(serialize, val)), ')') if sx_truthy((type_of(val) == 'list')) else sx_str(val)) + return result + +# categorize-special-forms +def categorize_special_forms(parsed_exprs): + categories = {} + for expr in parsed_exprs: + if sx_truthy(((type_of(expr) == 'list') if not sx_truthy((type_of(expr) == 'list')) else ((len(expr) >= 2) if not sx_truthy((len(expr) >= 2)) else ((type_of(first(expr)) == 'symbol') if not sx_truthy((type_of(first(expr)) == 'symbol')) else (symbol_name(first(expr)) == 'define-special-form'))))): + name = nth(expr, 1) + kwargs = extract_define_kwargs(expr) + category = (get(special_form_category_map, name) if sx_truthy(get(special_form_category_map, name)) else 'Other') + if sx_truthy((not sx_truthy(has_key_p(categories, category)))): + categories[category] = [] + get(categories, category).append({'name': name, 'syntax': (get(kwargs, 'syntax') if sx_truthy(get(kwargs, 'syntax')) else ''), 'doc': (get(kwargs, 'doc') if sx_truthy(get(kwargs, 'doc')) else ''), 'tail-position': (get(kwargs, 'tail-position') if sx_truthy(get(kwargs, 'tail-position')) else ''), 'example': (get(kwargs, 'example') if sx_truthy(get(kwargs, 'example')) else '')}) + return categories + +# build-ref-items-with-href +def build_ref_items_with_href(items, base_path, detail_keys, n_fields): + return map(lambda item: ((lambda name: (lambda field2: (lambda field3: {'name': name, 'desc': field2, 'exists': field3, 'href': (sx_str(base_path, name) if sx_truthy((field3 if not sx_truthy(field3) else some(lambda k: (k == name), detail_keys))) else NIL)})(nth(item, 2)))(nth(item, 1)))(nth(item, 0)) if sx_truthy((n_fields == 3)) else (lambda name: (lambda desc: {'name': name, 'desc': desc, 'href': (sx_str(base_path, name) if sx_truthy(some(lambda k: (k == name), detail_keys)) else NIL)})(nth(item, 1)))(nth(item, 0))), items) + +# build-reference-data +def build_reference_data(slug, raw_data, detail_keys): + _match = slug + if _match == 'attributes': + return {'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)} + elif _match == 'headers': + return {'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)} + elif _match == 'events': + return {'events-list': build_ref_items_with_href(get(raw_data, 'events-list'), '/hypermedia/reference/events/', detail_keys, 2)} + elif _match == 'js-api': + return {'js-api-list': map(lambda item: {'name': nth(item, 0), 'desc': nth(item, 1)}, get(raw_data, 'js-api-list'))} + else: + return {'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 +def build_attr_detail(slug, detail): + if sx_truthy(is_nil(detail)): + return {'attr-not-found': True} + else: + return {'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': (sx_str('ref-wire-', replace(replace(slug, ':', '-'), '*', 'star')) if sx_truthy(has_key_p(detail, 'handler')) else NIL)} + +# build-header-detail +def build_header_detail(slug, detail): + if sx_truthy(is_nil(detail)): + return {'header-not-found': True} + else: + return {'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 +def build_event_detail(slug, detail): + if sx_truthy(is_nil(detail)): + return {'event-not-found': True} + else: + return {'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 +def build_component_source(comp_data): + 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 sx_truthy((comp_type == 'not-found')): + return sx_str(';; component ', name, ' not found') + else: + param_strs = ((['&rest', 'children'] if sx_truthy(has_children) else []) if sx_truthy(empty_p(params)) else (append(cons('&key', params), ['&rest', 'children']) if sx_truthy(has_children) else cons('&key', params))) + params_sx = sx_str('(', join(' ', param_strs), ')') + form_name = ('defisland' if sx_truthy((comp_type == 'island')) else 'defcomp') + affinity_str = (sx_str(' :affinity ', affinity) if sx_truthy(((comp_type == 'component') if not sx_truthy((comp_type == 'component')) else ((not sx_truthy(is_nil(affinity))) if not sx_truthy((not sx_truthy(is_nil(affinity)))) else (not sx_truthy((affinity == 'auto')))))) else '') + return sx_str('(', form_name, ' ', name, ' ', params_sx, affinity_str, '\n ', body_sx, ')') + +# build-bundle-analysis +def build_bundle_analysis(pages_raw, components_raw, total_components, total_macros, pure_count, io_count): + _cells = {} + pages_data = [] + for page in pages_raw: + needed_names = get(page, 'needed-names') + n = len(needed_names) + pct = (round(((n / total_components) * 100)) if sx_truthy((total_components > 0)) else 0) + savings = (100 - pct) + _cells['pure_in_page'] = 0 + _cells['io_in_page'] = 0 + page_io_refs = [] + comp_details = [] + for comp_name in needed_names: + info = get(components_raw, comp_name) + if sx_truthy((not sx_truthy(is_nil(info)))): + if sx_truthy(get(info, 'is-pure')): + _cells['pure_in_page'] = (_cells['pure_in_page'] + 1) + else: + _cells['io_in_page'] = (_cells['io_in_page'] + 1) + for ref in (get(info, 'io-refs') if sx_truthy(get(info, 'io-refs')) else []): + if sx_truthy((not sx_truthy(some(lambda r: (r == ref), page_io_refs)))): + page_io_refs.append(ref) + comp_details.append({'name': comp_name, 'is-pure': get(info, 'is-pure'), 'affinity': get(info, 'affinity'), 'render-target': get(info, 'render-target'), 'io-refs': (get(info, 'io-refs') if sx_truthy(get(info, 'io-refs')) else []), 'deps': (get(info, 'deps') if sx_truthy(get(info, 'deps')) else []), 'source': get(info, 'source')}) + pages_data.append({'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': _cells['pure_in_page'], 'io-in-page': _cells['io_in_page'], 'components': comp_details}) + return {'pages': pages_data, 'total-components': total_components, 'total-macros': total_macros, 'pure-count': pure_count, 'io-count': io_count} + +# build-routing-analysis +def build_routing_analysis(pages_raw): + _cells = {} + pages_data = [] + _cells['client_count'] = 0 + _cells['server_count'] = 0 + for page in pages_raw: + has_data = get(page, 'has-data') + content_src = (get(page, 'content-src') if sx_truthy(get(page, 'content-src')) else '') + _cells['mode'] = NIL + _cells['reason'] = '' + if sx_truthy(has_data): + _cells['mode'] = 'server' + _cells['reason'] = 'Has :data expression — needs server IO' + _cells['server_count'] = (_cells['server_count'] + 1) + elif sx_truthy(empty_p(content_src)): + _cells['mode'] = 'server' + _cells['reason'] = 'No content expression' + _cells['server_count'] = (_cells['server_count'] + 1) + else: + _cells['mode'] = 'client' + _cells['client_count'] = (_cells['client_count'] + 1) + pages_data.append({'name': get(page, 'name'), 'path': get(page, 'path'), 'mode': _cells['mode'], 'has-data': has_data, 'content-expr': (sx_str(slice(content_src, 0, 80), '...') if sx_truthy((len(content_src) > 80)) else content_src), 'reason': _cells['reason']}) + return {'pages': pages_data, 'total-pages': (_cells['client_count'] + _cells['server_count']), 'client-count': _cells['client_count'], 'server-count': _cells['server_count']} + +# build-affinity-analysis +def build_affinity_analysis(demo_components, page_plans): + return {'components': demo_components, 'page-plans': page_plans} + + # === Transpiled from signals (reactive signal runtime) === # signal @@ -2440,7 +2835,9 @@ def batch(thunk): if sx_truthy((not sx_truthy(contains_p(seen, sub)))): seen.append(sub) pending.append(sub) - return for_each(lambda sub: sub(), pending) + for sub in pending: + sub() + return NIL return NIL # notify-subscribers @@ -2454,7 +2851,9 @@ def notify_subscribers(s): # flush-subscribers def flush_subscribers(s): - return for_each(lambda sub: sub(), signal_subscribers(s)) + for sub in signal_subscribers(s): + sub() + return NIL # dispose-computed def dispose_computed(s): @@ -2536,6 +2935,826 @@ def resource(fetch_fn): return state +# === Transpiled from adapter-async === + +# async-render +async def async_render(expr, env, ctx): + _match = type_of(expr) + if _match == 'nil': + return '' + elif _match == 'boolean': + return '' + elif _match == 'string': + return escape_html(expr) + elif _match == 'number': + return escape_html(sx_str(expr)) + elif _match == 'raw-html': + return raw_html_content(expr) + elif _match == 'symbol': + val = (await async_eval(expr, env, ctx)) + return (await async_render(val, env, ctx)) + elif _match == 'keyword': + return escape_html(keyword_name(expr)) + elif _match == 'list': + if sx_truthy(empty_p(expr)): + return '' + else: + return (await async_render_list(expr, env, ctx)) + elif _match == 'dict': + return '' + else: + return escape_html(sx_str(expr)) + +# async-render-list +async def async_render_list(expr, env, ctx): + head = first(expr) + if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))): + if sx_truthy((is_lambda(head) if sx_truthy(is_lambda(head)) else (type_of(head) == 'list'))): + return (await async_render((await async_eval(expr, env, ctx)), env, ctx)) + else: + return join('', (await async_map_render(expr, env, ctx))) + else: + name = symbol_name(head) + args = rest(expr) + if sx_truthy(io_primitive_p(name)): + return (await async_render((await async_eval(expr, env, ctx)), env, ctx)) + elif sx_truthy((name == 'raw!')): + return (await async_render_raw(args, env, ctx)) + elif sx_truthy((name == '<>')): + return join('', (await async_map_render(args, env, ctx))) + elif sx_truthy(starts_with_p(name, 'html:')): + return (await async_render_element(slice(name, 5), args, env, ctx)) + elif sx_truthy(async_render_form_p(name)): + if sx_truthy((contains_p(HTML_TAGS, name) if not sx_truthy(contains_p(HTML_TAGS, name)) else (((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword')) if sx_truthy(((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword'))) else svg_context_p()))): + return (await async_render_element(name, args, env, ctx)) + else: + return (await dispatch_async_render_form(name, expr, env, ctx)) + elif sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))): + return (await async_render(trampoline(expand_macro(env_get(env, name), args, env)), env, ctx)) + elif sx_truthy(contains_p(HTML_TAGS, name)): + return (await async_render_element(name, args, env, ctx)) + elif sx_truthy((starts_with_p(name, '~') if not sx_truthy(starts_with_p(name, '~')) else (env_has(env, name) if not sx_truthy(env_has(env, name)) else is_island(env_get(env, name))))): + return (await async_render_island(env_get(env, name), args, env, ctx)) + elif sx_truthy(starts_with_p(name, '~')): + val = (env_get(env, name) if sx_truthy(env_has(env, name)) else NIL) + if sx_truthy(is_component(val)): + return (await async_render_component(val, args, env, ctx)) + elif sx_truthy(is_macro(val)): + return (await async_render(trampoline(expand_macro(val, args, env)), env, ctx)) + else: + return (await async_render((await async_eval(expr, env, ctx)), env, ctx)) + elif sx_truthy(((index_of(name, '-') > 0) if not sx_truthy((index_of(name, '-') > 0)) else ((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword')))): + return (await async_render_element(name, args, env, ctx)) + elif sx_truthy(svg_context_p()): + return (await async_render_element(name, args, env, ctx)) + else: + return (await async_render((await async_eval(expr, env, ctx)), env, ctx)) + +# async-render-raw +async def async_render_raw(args, env, ctx): + parts = [] + for arg in args: + val = (await async_eval(arg, env, ctx)) + if sx_truthy(is_raw_html(val)): + parts.append(raw_html_content(val)) + elif sx_truthy((type_of(val) == 'string')): + parts.append(val) + elif sx_truthy(((not sx_truthy(is_nil(val))) if not sx_truthy((not sx_truthy(is_nil(val)))) else (not sx_truthy((val == False))))): + parts.append(sx_str(val)) + return join('', parts) + +# async-render-element +async def async_render_element(tag, args, env, ctx): + attrs = {} + children = [] + (await async_parse_element_args(args, attrs, children, env, ctx)) + class_val = dict_get(attrs, 'class') + if sx_truthy(((not sx_truthy(is_nil(class_val))) if not sx_truthy((not sx_truthy(is_nil(class_val)))) else (not sx_truthy((class_val == False))))): + css_class_collect(sx_str(class_val)) + opening = sx_str('<', tag, render_attrs(attrs), '>') + if sx_truthy(contains_p(VOID_ELEMENTS, tag)): + return opening + else: + token = (svg_context_set(True) if sx_truthy(((tag == 'svg') if sx_truthy((tag == 'svg')) else (tag == 'math'))) else NIL) + child_html = join('', (await async_map_render(children, env, ctx))) + if sx_truthy(token): + svg_context_reset(token) + return sx_str(opening, child_html, '') + +# async-parse-element-args +async def async_parse_element_args(args, attrs, children, env, ctx): + _cells = {} + _cells['skip'] = False + _cells['i'] = 0 + for arg in args: + if sx_truthy(_cells['skip']): + _cells['skip'] = False + _cells['i'] = (_cells['i'] + 1) + else: + if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))): + val = (await async_eval(nth(args, (_cells['i'] + 1)), env, ctx)) + attrs[keyword_name(arg)] = val + _cells['skip'] = True + _cells['i'] = (_cells['i'] + 1) + else: + children.append(arg) + _cells['i'] = (_cells['i'] + 1) + return NIL + +# async-render-component +async def async_render_component(comp, args, env, ctx): + kwargs = {} + children = [] + (await async_parse_kw_args(args, kwargs, children, env, ctx)) + local = env_merge(component_closure(comp), env) + for p in component_params(comp): + local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL) + if sx_truthy(component_has_children(comp)): + local['children'] = make_raw_html(join('', (await async_map_render(children, env, ctx)))) + return (await async_render(component_body(comp), local, ctx)) + +# async-render-island +async def async_render_island(island, args, env, ctx): + kwargs = {} + children = [] + (await async_parse_kw_args(args, kwargs, children, env, ctx)) + local = env_merge(component_closure(island), env) + island_name = component_name(island) + for p in component_params(island): + local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL) + if sx_truthy(component_has_children(island)): + local['children'] = make_raw_html(join('', (await async_map_render(children, env, ctx)))) + body_html = (await async_render(component_body(island), local, ctx)) + state_json = serialize_island_state(kwargs) + return sx_str('', body_html, '') + +# async-render-lambda +async def async_render_lambda(f, args, env, ctx): + local = env_merge(lambda_closure(f), env) + for_each_indexed(lambda i, p: _sx_dict_set(local, p, nth(args, i)), lambda_params(f)) + return (await async_render(lambda_body(f), local, ctx)) + +# async-parse-kw-args +async def async_parse_kw_args(args, kwargs, children, env, ctx): + _cells = {} + _cells['skip'] = False + _cells['i'] = 0 + for arg in args: + if sx_truthy(_cells['skip']): + _cells['skip'] = False + _cells['i'] = (_cells['i'] + 1) + else: + if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))): + val = (await async_eval(nth(args, (_cells['i'] + 1)), env, ctx)) + kwargs[keyword_name(arg)] = val + _cells['skip'] = True + _cells['i'] = (_cells['i'] + 1) + else: + children.append(arg) + _cells['i'] = (_cells['i'] + 1) + return NIL + +# async-map-render +async def async_map_render(exprs, env, ctx): + results = [] + for x in exprs: + results.append((await async_render(x, env, ctx))) + return results + +# ASYNC_RENDER_FORMS +ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each'] + +# async-render-form? +def async_render_form_p(name): + return contains_p(ASYNC_RENDER_FORMS, name) + +# dispatch-async-render-form +async def dispatch_async_render_form(name, expr, env, ctx): + if sx_truthy((name == 'if')): + cond_val = (await async_eval(nth(expr, 1), env, ctx)) + if sx_truthy(cond_val): + return (await async_render(nth(expr, 2), env, ctx)) + else: + if sx_truthy((len(expr) > 3)): + return (await async_render(nth(expr, 3), env, ctx)) + else: + return '' + elif sx_truthy((name == 'when')): + if sx_truthy((not sx_truthy((await async_eval(nth(expr, 1), env, ctx))))): + return '' + else: + return join('', (await async_map_render(slice(expr, 2), env, ctx))) + elif sx_truthy((name == 'cond')): + clauses = rest(expr) + if sx_truthy(cond_scheme_p(clauses)): + return (await async_render_cond_scheme(clauses, env, ctx)) + else: + return (await async_render_cond_clojure(clauses, env, ctx)) + elif sx_truthy((name == 'case')): + return (await async_render((await async_eval(expr, env, ctx)), env, ctx)) + elif sx_truthy(((name == 'let') if sx_truthy((name == 'let')) else (name == 'let*'))): + local = (await async_process_bindings(nth(expr, 1), env, ctx)) + return join('', (await async_map_render(slice(expr, 2), local, ctx))) + elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))): + return join('', (await async_map_render(rest(expr), env, ctx))) + elif sx_truthy(is_definition_form(name)): + (await async_eval(expr, env, ctx)) + return '' + elif sx_truthy((name == 'map')): + f = (await async_eval(nth(expr, 1), env, ctx)) + coll = (await async_eval(nth(expr, 2), env, ctx)) + return join('', (await async_map_fn_render(f, coll, env, ctx))) + elif sx_truthy((name == 'map-indexed')): + f = (await async_eval(nth(expr, 1), env, ctx)) + coll = (await async_eval(nth(expr, 2), env, ctx)) + return join('', (await async_map_indexed_fn_render(f, coll, env, ctx))) + elif sx_truthy((name == 'filter')): + return (await async_render((await async_eval(expr, env, ctx)), env, ctx)) + elif sx_truthy((name == 'for-each')): + f = (await async_eval(nth(expr, 1), env, ctx)) + coll = (await async_eval(nth(expr, 2), env, ctx)) + return join('', (await async_map_fn_render(f, coll, env, ctx))) + else: + return (await async_render((await async_eval(expr, env, ctx)), env, ctx)) + +# async-render-cond-scheme +async def async_render_cond_scheme(clauses, env, ctx): + if sx_truthy(empty_p(clauses)): + return '' + else: + clause = first(clauses) + test = first(clause) + body = nth(clause, 1) + if sx_truthy((((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))) if sx_truthy(((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else')))) else ((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')))): + return (await async_render(body, env, ctx)) + else: + if sx_truthy((await async_eval(test, env, ctx))): + return (await async_render(body, env, ctx)) + else: + return (await async_render_cond_scheme(rest(clauses), env, ctx)) + +# async-render-cond-clojure +async def async_render_cond_clojure(clauses, env, ctx): + if sx_truthy((len(clauses) < 2)): + return '' + else: + test = first(clauses) + body = nth(clauses, 1) + if sx_truthy((((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')) if sx_truthy(((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else'))) else ((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))))): + return (await async_render(body, env, ctx)) + else: + if sx_truthy((await async_eval(test, env, ctx))): + return (await async_render(body, env, ctx)) + else: + return (await async_render_cond_clojure(slice(clauses, 2), env, ctx)) + +# async-process-bindings +async def async_process_bindings(bindings, env, ctx): + local = env_extend(env) + if sx_truthy(((type_of(bindings) == 'list') if not sx_truthy((type_of(bindings) == 'list')) else (not sx_truthy(empty_p(bindings))))): + if sx_truthy((type_of(first(bindings)) == 'list')): + for pair in bindings: + if sx_truthy(((type_of(pair) == 'list') if not sx_truthy((type_of(pair) == 'list')) else (len(pair) >= 2))): + name = (symbol_name(first(pair)) if sx_truthy((type_of(first(pair)) == 'symbol')) else sx_str(first(pair))) + local[name] = (await async_eval(nth(pair, 1), local, ctx)) + else: + (await async_process_bindings_flat(bindings, local, ctx)) + return local + +# async-process-bindings-flat +async def async_process_bindings_flat(bindings, local, ctx): + _cells = {} + _cells['skip'] = False + _cells['i'] = 0 + for item in bindings: + if sx_truthy(_cells['skip']): + _cells['skip'] = False + _cells['i'] = (_cells['i'] + 1) + else: + name = (symbol_name(item) if sx_truthy((type_of(item) == 'symbol')) else sx_str(item)) + if sx_truthy(((_cells['i'] + 1) < len(bindings))): + local[name] = (await async_eval(nth(bindings, (_cells['i'] + 1)), local, ctx)) + _cells['skip'] = True + _cells['i'] = (_cells['i'] + 1) + return NIL + +# async-map-fn-render +async def async_map_fn_render(f, coll, env, ctx): + results = [] + for item in coll: + if sx_truthy(is_lambda(f)): + results.append((await async_render_lambda(f, [item], env, ctx))) + else: + r = (await async_invoke(f, item)) + results.append((await async_render(r, env, ctx))) + return results + +# async-map-indexed-fn-render +async def async_map_indexed_fn_render(f, coll, env, ctx): + _cells = {} + results = [] + _cells['i'] = 0 + for item in coll: + if sx_truthy(is_lambda(f)): + results.append((await async_render_lambda(f, [_cells['i'], item], env, ctx))) + else: + r = (await async_invoke(f, _cells['i'], item)) + results.append((await async_render(r, env, ctx))) + _cells['i'] = (_cells['i'] + 1) + return results + +# async-invoke +async def async_invoke(f, *args): + r = apply(f, args) + if sx_truthy(is_async_coroutine(r)): + return (await async_await(r)) + else: + return r + +# async-aser +async def async_aser(expr, env, ctx): + _match = type_of(expr) + if _match == 'number': + return expr + elif _match == 'string': + return expr + elif _match == 'boolean': + return expr + elif _match == 'nil': + return NIL + elif _match == 'symbol': + name = symbol_name(expr) + if sx_truthy(env_has(env, name)): + return env_get(env, name) + elif sx_truthy(is_primitive(name)): + return get_primitive(name) + elif sx_truthy((name == 'true')): + return True + elif sx_truthy((name == 'false')): + return False + elif sx_truthy((name == 'nil')): + return NIL + else: + return error(sx_str('Undefined symbol: ', name)) + elif _match == 'keyword': + return keyword_name(expr) + elif _match == 'dict': + return (await async_aser_dict(expr, env, ctx)) + elif _match == 'list': + if sx_truthy(empty_p(expr)): + return [] + else: + return (await async_aser_list(expr, env, ctx)) + else: + return expr + +# async-aser-dict +async def async_aser_dict(expr, env, ctx): + result = {} + for key in keys(expr): + result[key] = (await async_aser(dict_get(expr, key), env, ctx)) + return result + +# async-aser-list +async def async_aser_list(expr, env, ctx): + head = first(expr) + args = rest(expr) + if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))): + if sx_truthy((is_lambda(head) if sx_truthy(is_lambda(head)) else (type_of(head) == 'list'))): + return (await async_aser_eval_call(head, args, env, ctx)) + else: + return (await async_aser_map_list(expr, env, ctx)) + else: + name = symbol_name(head) + if sx_truthy(io_primitive_p(name)): + return (await async_eval(expr, env, ctx)) + elif sx_truthy((name == '<>')): + return (await async_aser_fragment(args, env, ctx)) + elif sx_truthy((name == 'raw!')): + return (await async_aser_call('raw!', args, env, ctx)) + elif sx_truthy(starts_with_p(name, 'html:')): + return (await async_aser_call(slice(name, 5), args, env, ctx)) + elif sx_truthy(starts_with_p(name, '~')): + val = (env_get(env, name) if sx_truthy(env_has(env, name)) else NIL) + if sx_truthy(is_macro(val)): + return (await async_aser(trampoline(expand_macro(val, args, env)), env, ctx)) + elif sx_truthy((is_component(val) if not sx_truthy(is_component(val)) else (expand_components_p() if sx_truthy(expand_components_p()) else (component_affinity(val) == 'server')))): + return (await async_aser_component(val, args, env, ctx)) + else: + return (await async_aser_call(name, args, env, ctx)) + elif sx_truthy(async_aser_form_p(name)): + if sx_truthy((contains_p(HTML_TAGS, name) if not sx_truthy(contains_p(HTML_TAGS, name)) else (((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword')) if sx_truthy(((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword'))) else svg_context_p()))): + return (await async_aser_call(name, args, env, ctx)) + else: + return (await dispatch_async_aser_form(name, expr, env, ctx)) + elif sx_truthy(contains_p(HTML_TAGS, name)): + return (await async_aser_call(name, args, env, ctx)) + elif sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))): + return (await async_aser(trampoline(expand_macro(env_get(env, name), args, env)), env, ctx)) + elif sx_truthy(((index_of(name, '-') > 0) if not sx_truthy((index_of(name, '-') > 0)) else ((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword')))): + return (await async_aser_call(name, args, env, ctx)) + elif sx_truthy(svg_context_p()): + return (await async_aser_call(name, args, env, ctx)) + else: + return (await async_aser_eval_call(head, args, env, ctx)) + +# async-aser-eval-call +async def async_aser_eval_call(head, args, env, ctx): + f = (await async_eval(head, env, ctx)) + evaled_args = (await async_eval_args(args, env, ctx)) + if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))): + r = apply(f, evaled_args) + if sx_truthy(is_async_coroutine(r)): + return (await async_await(r)) + else: + return r + elif sx_truthy(is_lambda(f)): + local = env_merge(lambda_closure(f), env) + for_each_indexed(lambda i, p: _sx_dict_set(local, p, nth(evaled_args, i)), lambda_params(f)) + return (await async_aser(lambda_body(f), local, ctx)) + elif sx_truthy(is_component(f)): + return (await async_aser_call(sx_str('~', component_name(f)), args, env, ctx)) + elif sx_truthy(is_island(f)): + return (await async_aser_call(sx_str('~', component_name(f)), args, env, ctx)) + else: + return error(sx_str('Not callable: ', inspect(f))) + +# async-eval-args +async def async_eval_args(args, env, ctx): + results = [] + for a in args: + results.append((await async_eval(a, env, ctx))) + return results + +# async-aser-map-list +async def async_aser_map_list(exprs, env, ctx): + results = [] + for x in exprs: + results.append((await async_aser(x, env, ctx))) + return results + +# async-aser-fragment +async def async_aser_fragment(children, env, ctx): + parts = [] + for c in children: + result = (await async_aser(c, env, ctx)) + if sx_truthy((type_of(result) == 'list')): + for item in result: + if sx_truthy((not sx_truthy(is_nil(item)))): + parts.append(serialize(item)) + else: + if sx_truthy((not sx_truthy(is_nil(result)))): + parts.append(serialize(result)) + if sx_truthy(empty_p(parts)): + return make_sx_expr('') + else: + return make_sx_expr(sx_str('(<> ', join(' ', parts), ')')) + +# async-aser-component +async def async_aser_component(comp, args, env, ctx): + kwargs = {} + children = [] + (await async_parse_aser_kw_args(args, kwargs, children, env, ctx)) + local = env_merge(component_closure(comp), env) + for p in component_params(comp): + local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL) + if sx_truthy(component_has_children(comp)): + child_parts = [] + for c in children: + result = (await async_aser(c, env, ctx)) + if sx_truthy(list_p(result)): + for item in result: + if sx_truthy((not sx_truthy(is_nil(item)))): + child_parts.append(serialize(item)) + else: + if sx_truthy((not sx_truthy(is_nil(result)))): + child_parts.append(serialize(result)) + local['children'] = make_sx_expr(sx_str('(<> ', join(' ', child_parts), ')')) + return (await async_aser(component_body(comp), local, ctx)) + +# async-parse-aser-kw-args +async def async_parse_aser_kw_args(args, kwargs, children, env, ctx): + _cells = {} + _cells['skip'] = False + _cells['i'] = 0 + for arg in args: + if sx_truthy(_cells['skip']): + _cells['skip'] = False + _cells['i'] = (_cells['i'] + 1) + else: + if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))): + val = (await async_aser(nth(args, (_cells['i'] + 1)), env, ctx)) + kwargs[keyword_name(arg)] = val + _cells['skip'] = True + _cells['i'] = (_cells['i'] + 1) + else: + children.append(arg) + _cells['i'] = (_cells['i'] + 1) + return NIL + +# async-aser-call +async def async_aser_call(name, args, env, ctx): + _cells = {} + token = (svg_context_set(True) if sx_truthy(((name == 'svg') if sx_truthy((name == 'svg')) else (name == 'math'))) else NIL) + parts = [name] + _cells['skip'] = False + _cells['i'] = 0 + for arg in args: + if sx_truthy(_cells['skip']): + _cells['skip'] = False + _cells['i'] = (_cells['i'] + 1) + else: + if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))): + val = (await async_aser(nth(args, (_cells['i'] + 1)), env, ctx)) + if sx_truthy((not sx_truthy(is_nil(val)))): + parts.append(sx_str(':', keyword_name(arg))) + if sx_truthy((type_of(val) == 'list')): + live = filter(lambda v: (not sx_truthy(is_nil(v))), val) + if sx_truthy(empty_p(live)): + parts.append('nil') + else: + items = map(serialize, live) + if sx_truthy(some(lambda v: is_sx_expr(v), live)): + parts.append(sx_str('(<> ', join(' ', items), ')')) + else: + parts.append(sx_str('(list ', join(' ', items), ')')) + else: + parts.append(serialize(val)) + _cells['skip'] = True + _cells['i'] = (_cells['i'] + 1) + else: + result = (await async_aser(arg, env, ctx)) + if sx_truthy((not sx_truthy(is_nil(result)))): + if sx_truthy((type_of(result) == 'list')): + for item in result: + if sx_truthy((not sx_truthy(is_nil(item)))): + parts.append(serialize(item)) + else: + parts.append(serialize(result)) + _cells['i'] = (_cells['i'] + 1) + if sx_truthy(token): + svg_context_reset(token) + return make_sx_expr(sx_str('(', join(' ', parts), ')')) + +# ASYNC_ASER_FORM_NAMES +ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland'] + +# ASYNC_ASER_HO_NAMES +ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each'] + +# async-aser-form? +def async_aser_form_p(name): + return (contains_p(ASYNC_ASER_FORM_NAMES, name) if sx_truthy(contains_p(ASYNC_ASER_FORM_NAMES, name)) else contains_p(ASYNC_ASER_HO_NAMES, name)) + +# dispatch-async-aser-form +async def dispatch_async_aser_form(name, expr, env, ctx): + _cells = {} + args = rest(expr) + if sx_truthy((name == 'if')): + cond_val = (await async_eval(first(args), env, ctx)) + if sx_truthy(cond_val): + return (await async_aser(nth(args, 1), env, ctx)) + else: + if sx_truthy((len(args) > 2)): + return (await async_aser(nth(args, 2), env, ctx)) + else: + return NIL + elif sx_truthy((name == 'when')): + if sx_truthy((not sx_truthy((await async_eval(first(args), env, ctx))))): + return NIL + else: + _cells['result'] = NIL + for body in rest(args): + _cells['result'] = (await async_aser(body, env, ctx)) + return _cells['result'] + elif sx_truthy((name == 'cond')): + if sx_truthy(cond_scheme_p(args)): + return (await async_aser_cond_scheme(args, env, ctx)) + else: + return (await async_aser_cond_clojure(args, env, ctx)) + elif sx_truthy((name == 'case')): + match_val = (await async_eval(first(args), env, ctx)) + return (await async_aser_case_loop(match_val, rest(args), env, ctx)) + elif sx_truthy(((name == 'let') if sx_truthy((name == 'let')) else (name == 'let*'))): + local = (await async_process_bindings(first(args), env, ctx)) + _cells['result'] = NIL + for body in rest(args): + _cells['result'] = (await async_aser(body, local, ctx)) + return _cells['result'] + elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))): + _cells['result'] = NIL + for body in args: + _cells['result'] = (await async_aser(body, env, ctx)) + return _cells['result'] + elif sx_truthy((name == 'and')): + _cells['result'] = True + _cells['stop'] = False + for arg in args: + if sx_truthy((not sx_truthy(_cells['stop']))): + _cells['result'] = (await async_eval(arg, env, ctx)) + if sx_truthy((not sx_truthy(_cells['result']))): + _cells['stop'] = True + return _cells['result'] + elif sx_truthy((name == 'or')): + _cells['result'] = False + _cells['stop'] = False + for arg in args: + if sx_truthy((not sx_truthy(_cells['stop']))): + _cells['result'] = (await async_eval(arg, env, ctx)) + if sx_truthy(_cells['result']): + _cells['stop'] = True + return _cells['result'] + elif sx_truthy(((name == 'lambda') if sx_truthy((name == 'lambda')) else (name == 'fn'))): + return sf_lambda(args, env) + elif sx_truthy((name == 'quote')): + if sx_truthy(empty_p(args)): + return NIL + else: + return first(args) + elif sx_truthy((name == '->')): + return (await async_aser_thread_first(args, env, ctx)) + elif sx_truthy((name == 'set!')): + value = (await async_eval(nth(args, 1), env, ctx)) + env[symbol_name(first(args))] = value + return value + elif sx_truthy((name == 'map')): + return (await async_aser_ho_map(args, env, ctx)) + elif sx_truthy((name == 'map-indexed')): + return (await async_aser_ho_map_indexed(args, env, ctx)) + elif sx_truthy((name == 'filter')): + return (await async_eval(expr, env, ctx)) + elif sx_truthy((name == 'for-each')): + return (await async_aser_ho_for_each(args, env, ctx)) + elif sx_truthy((name == 'defisland')): + (await async_eval(expr, env, ctx)) + return serialize(expr) + elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else (name == 'defaction'))))))))): + (await async_eval(expr, env, ctx)) + return NIL + else: + return (await async_eval(expr, env, ctx)) + +# async-aser-cond-scheme +async def async_aser_cond_scheme(clauses, env, ctx): + if sx_truthy(empty_p(clauses)): + return NIL + else: + clause = first(clauses) + test = first(clause) + body = nth(clause, 1) + if sx_truthy((((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))) if sx_truthy(((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else')))) else ((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')))): + return (await async_aser(body, env, ctx)) + else: + if sx_truthy((await async_eval(test, env, ctx))): + return (await async_aser(body, env, ctx)) + else: + return (await async_aser_cond_scheme(rest(clauses), env, ctx)) + +# async-aser-cond-clojure +async def async_aser_cond_clojure(clauses, env, ctx): + if sx_truthy((len(clauses) < 2)): + return NIL + else: + test = first(clauses) + body = nth(clauses, 1) + if sx_truthy((((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')) if sx_truthy(((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else'))) else ((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))))): + return (await async_aser(body, env, ctx)) + else: + if sx_truthy((await async_eval(test, env, ctx))): + return (await async_aser(body, env, ctx)) + else: + return (await async_aser_cond_clojure(slice(clauses, 2), env, ctx)) + +# async-aser-case-loop +async def async_aser_case_loop(match_val, clauses, env, ctx): + if sx_truthy((len(clauses) < 2)): + return NIL + else: + test = first(clauses) + body = nth(clauses, 1) + if sx_truthy((((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')) if sx_truthy(((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else'))) else ((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == ':else') if sx_truthy((symbol_name(test) == ':else')) else (symbol_name(test) == 'else'))))): + return (await async_aser(body, env, ctx)) + else: + if sx_truthy((match_val == (await async_eval(test, env, ctx)))): + return (await async_aser(body, env, ctx)) + else: + return (await async_aser_case_loop(match_val, slice(clauses, 2), env, ctx)) + +# async-aser-thread-first +async def async_aser_thread_first(args, env, ctx): + _cells = {} + _cells['result'] = (await async_eval(first(args), env, ctx)) + for form in rest(args): + if sx_truthy((type_of(form) == 'list')): + f = (await async_eval(first(form), env, ctx)) + fn_args = cons(_cells['result'], (await async_eval_args(rest(form), env, ctx))) + _cells['result'] = (await async_invoke_or_lambda(f, fn_args, env, ctx)) + else: + f = (await async_eval(form, env, ctx)) + _cells['result'] = (await async_invoke_or_lambda(f, [_cells['result']], env, ctx)) + return _cells['result'] + +# async-invoke-or-lambda +async def async_invoke_or_lambda(f, args, env, ctx): + if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))): + r = apply(f, args) + if sx_truthy(is_async_coroutine(r)): + return (await async_await(r)) + else: + return r + elif sx_truthy(is_lambda(f)): + local = env_merge(lambda_closure(f), env) + for_each_indexed(lambda i, p: _sx_dict_set(local, p, nth(args, i)), lambda_params(f)) + return (await async_eval(lambda_body(f), local, ctx)) + else: + return error(sx_str('-> form not callable: ', inspect(f))) + +# async-aser-ho-map +async def async_aser_ho_map(args, env, ctx): + f = (await async_eval(first(args), env, ctx)) + coll = (await async_eval(nth(args, 1), env, ctx)) + results = [] + for item in coll: + if sx_truthy(is_lambda(f)): + local = env_merge(lambda_closure(f), env) + local[first(lambda_params(f))] = item + results.append((await async_aser(lambda_body(f), local, ctx))) + else: + results.append((await async_invoke(f, item))) + return results + +# async-aser-ho-map-indexed +async def async_aser_ho_map_indexed(args, env, ctx): + _cells = {} + f = (await async_eval(first(args), env, ctx)) + coll = (await async_eval(nth(args, 1), env, ctx)) + results = [] + _cells['i'] = 0 + for item in coll: + if sx_truthy(is_lambda(f)): + local = env_merge(lambda_closure(f), env) + local[first(lambda_params(f))] = _cells['i'] + local[nth(lambda_params(f), 1)] = item + results.append((await async_aser(lambda_body(f), local, ctx))) + else: + results.append((await async_invoke(f, _cells['i'], item))) + _cells['i'] = (_cells['i'] + 1) + return results + +# async-aser-ho-for-each +async def async_aser_ho_for_each(args, env, ctx): + f = (await async_eval(first(args), env, ctx)) + coll = (await async_eval(nth(args, 1), env, ctx)) + results = [] + for item in coll: + if sx_truthy(is_lambda(f)): + local = env_merge(lambda_closure(f), env) + local[first(lambda_params(f))] = item + results.append((await async_aser(lambda_body(f), local, ctx))) + else: + results.append((await async_invoke(f, item))) + return results + +# async-eval-slot-inner +async def async_eval_slot_inner(expr, env, ctx): + result = NIL + if sx_truthy((list_p(expr) if not sx_truthy(list_p(expr)) else (not sx_truthy(empty_p(expr))))): + head = first(expr) + if sx_truthy(((type_of(head) == 'symbol') if not sx_truthy((type_of(head) == 'symbol')) else starts_with_p(symbol_name(head), '~'))): + name = symbol_name(head) + val = (env_get(env, name) if sx_truthy(env_has(env, name)) else NIL) + if sx_truthy(is_component(val)): + result = (await async_aser_component(val, rest(expr), env, ctx)) + else: + result = (await async_maybe_expand_result((await async_aser(expr, env, ctx)), env, ctx)) + else: + result = (await async_maybe_expand_result((await async_aser(expr, env, ctx)), env, ctx)) + else: + result = (await async_maybe_expand_result((await async_aser(expr, env, ctx)), env, ctx)) + if sx_truthy(is_sx_expr(result)): + return result + else: + if sx_truthy(is_nil(result)): + return make_sx_expr('') + else: + if sx_truthy(string_p(result)): + return make_sx_expr(result) + else: + return make_sx_expr(serialize(result)) + +# async-maybe-expand-result +async def async_maybe_expand_result(result, env, ctx): + raw = (trim(sx_str(result)) if sx_truthy(is_sx_expr(result)) else (trim(result) if sx_truthy(string_p(result)) else NIL)) + if sx_truthy((raw if not sx_truthy(raw) else starts_with_p(raw, '(~'))): + parsed = sx_parse(raw) + if sx_truthy((parsed if not sx_truthy(parsed) else (not sx_truthy(empty_p(parsed))))): + return (await async_eval_slot_inner(first(parsed), env, ctx)) + else: + return result + else: + return result + + # ========================================================================= # Fixups -- wire up render adapter dispatch # ========================================================================= diff --git a/shared/sx/ref/test-aser.sx b/shared/sx/ref/test-aser.sx new file mode 100644 index 00000000..b70ae768 --- /dev/null +++ b/shared/sx/ref/test-aser.sx @@ -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 ), + ;; 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)")))) diff --git a/shared/sx/ref/test-eval.sx b/shared/sx/ref/test-eval.sx index 7e8c4a8f..33885f7a 100644 --- a/shared/sx/ref/test-eval.sx +++ b/shared/sx/ref/test-eval.sx @@ -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)) diff --git a/shared/sx/ref/test-render.sx b/shared/sx/ref/test-render.sx index c714fc7d..1097d7a2 100644 --- a/shared/sx/ref/test-render.sx +++ b/shared/sx/ref/test-render.sx @@ -149,7 +149,27 @@ (deftest "let in render context" (assert-equal "

hello

" - (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 "

yes

" + (render-html "(cond (nil? nil) (p \"yes\") :else (p \"no\"))")) + (assert-equal "

no

" + (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 "

outer

" + (render-html "(do (define theme \"outer\") (let ((x 1)) (p theme)))"))) + + (deftest "nested let preserves outer scope" + (assert-equal "
helloworld
" + (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 "

inside

"))))) + + +;; -------------------------------------------------------------------------- +;; Map/filter producing multiple children (aser-adjacent regression tests) +;; -------------------------------------------------------------------------- + +(defsuite "render-map-children" + (deftest "map producing multiple children inside tag" + (assert-equal "
  • a
  • b
  • c
" + (render-html "(do (define items (list \"a\" \"b\" \"c\")) + (ul (map (fn (x) (li x)) items)))"))) + + (deftest "map with other siblings" + (assert-equal "
  • first
  • a
  • b
" + (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 "
  • a
  • c
" + (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 "
12
" + (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 "

x

")) + (assert-true (string-contains? html "

y

")))) + + (deftest "map-indexed renders with index" + (assert-equal "
  • 0: a
  • 1: b
  • " + (render-html "(map-indexed (fn (i x) (li (str i \": \" x))) (list \"a\" \"b\"))"))) + + (deftest "for-each renders each item" + (assert-equal "

    1

    2

    " + (render-html "(for-each (fn (x) (p x)) (list 1 2))")))) diff --git a/shared/sx/resolver.py b/shared/sx/resolver.py index 93622499..2b90c6fa 100644 --- a/shared/sx/resolver.py +++ b/shared/sx/resolver.py @@ -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.""" diff --git a/shared/sx/tests/run.py b/shared/sx/tests/run.py index 25c7f19b..eaca44bd 100644 --- a/shared/sx/tests/run.py +++ b/shared/sx/tests/run.py @@ -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) diff --git a/shared/sx/tests/test_bootstrapper.py b/shared/sx/tests/test_bootstrapper.py index 4fb6416b..635943ea 100644 --- a/shared/sx/tests/test_bootstrapper.py +++ b/shared/sx/tests/test_bootstrapper.py @@ -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 diff --git a/shared/sx/tests/test_deps.py b/shared/sx/tests/test_deps.py index c5ca5262..a648a1ea 100644 --- a/shared/sx/tests/test_deps.py +++ b/shared/sx/tests/test_deps.py @@ -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) diff --git a/shared/sx/tests/test_io_detection.py b/shared/sx/tests/test_io_detection.py index bac513ae..412c8996 100644 --- a/shared/sx/tests/test_io_detection.py +++ b/shared/sx/tests/test_io_detection.py @@ -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) diff --git a/shared/sx/tests/test_io_proxy.py b/shared/sx/tests/test_io_proxy.py index 1516797f..8ccfd366 100644 --- a/shared/sx/tests/test_io_proxy.py +++ b/shared/sx/tests/test_io_proxy.py @@ -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)) diff --git a/shared/sx/tests/test_page_data.py b/shared/sx/tests/test_page_data.py index 7cd22800..5789814d 100644 --- a/shared/sx/tests/test_page_data.py +++ b/shared/sx/tests/test_page_data.py @@ -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)) diff --git a/shared/sx/tests/test_parity.py b/shared/sx/tests/test_parity.py index db809b02..0f079f51 100644 --- a/shared/sx/tests/test_parity.py +++ b/shared/sx/tests/test_parity.py @@ -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") diff --git a/shared/sx/tests/test_sx_js.py b/shared/sx/tests/test_sx_js.py index 135adaec..43b59aae 100644 --- a/shared/sx/tests/test_sx_js.py +++ b/shared/sx/tests/test_sx_js.py @@ -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" diff --git a/shared/sx/tests/test_sx_spec.py b/shared/sx/tests/test_sx_spec.py index db885cce..51592761 100644 --- a/shared/sx/tests/test_sx_spec.py +++ b/shared/sx/tests/test_sx_spec.py @@ -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))))) diff --git a/shared/sx/types.py b/shared/sx/types.py index 74ca4982..15bd49ff 100644 --- a/shared/sx/types.py +++ b/shared/sx/types.py @@ -376,6 +376,15 @@ class _ShiftSignal(BaseException): self.env = env +# --------------------------------------------------------------------------- +# EvalError +# --------------------------------------------------------------------------- + +class EvalError(Exception): + """Error during expression evaluation.""" + pass + + # --------------------------------------------------------------------------- # Type alias # --------------------------------------------------------------------------- diff --git a/sx/sx/boundary.sx b/sx/sx/boundary.sx index 0269f113..d39c940b 100644 --- a/sx/sx/boundary.sx +++ b/sx/sx/boundary.sx @@ -104,3 +104,8 @@ :params () :returns "dict" :service "sx") + +(define-page-helper "page-helpers-demo-data" + :params () + :returns "dict" + :service "sx") diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index bb2c30f4..1d2dfa71 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -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. diff --git a/sx/sx/page-helpers-demo.sx b/sx/sx/page-helpers-demo.sx new file mode 100644 index 00000000..2bf8e8c7 --- /dev/null +++ b/sx/sx/page-helpers-demo.sx @@ -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)))) diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index c938f938..b6402e20 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -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 ;; --------------------------------------------------------------------------- diff --git a/sx/sxc/pages/helpers.py b/sx/sxc/pages/helpers.py index 102147d5..a9c8cbb2 100644 --- a/sx/sxc/pages/helpers.py +++ b/sx/sxc/pages/helpers.py @@ -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 - ], + 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], } - # 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 - ], - } + 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): - 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, - "affinity": val.affinity, - "render-target": val.render_target, - "io-refs": sorted(val.io_refs), - "deps": sorted(val.deps), - "source": source, - }) + for comp_name in needed: + if comp_name not in components_raw: + val = env.get(comp_name) + if isinstance(val, Component): + 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) + 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": 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