From 189a0258d9a48466e2f51680cf81073ec23d67c9 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 8 Mar 2026 15:18:45 +0000 Subject: [PATCH] Fix reactive islands client-side navigation and hydration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs prevented islands from working during SX wire navigation: 1. components_for_request() only bundled Component and Macro defs, not Island defs — client never received defisland definitions during navigation (components_for_page for initial HTML shell was correct). 2. hydrate-island used morph-children which can't transfer addEventListener event handlers from freshly rendered DOM to existing nodes. Changed to clear+append so reactive DOM with live signal subscriptions is inserted directly. 3. asyncRenderToDom (client-side async page eval) checked _component but not _island on ~-prefixed names — islands fell through to generic eval which failed. Now delegates to renderDomIsland. 4. setInterval_/setTimeout_ passed SX Lambda objects directly to native timers. JS coerced them to "[object Object]" and tried to eval as code, causing "missing ] after element list". Added _wrapSxFn to convert SX lambdas to JS functions before passing to timers. Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-browser.js | 479 +++++++++++++++++++------- shared/static/scripts/sx-ref.js | 516 +++++++++------------------- shared/sx/async_eval.py | 14 +- shared/sx/helpers.py | 16 +- shared/sx/ref/adapter-dom.sx | 3 +- shared/sx/ref/adapter-sx.sx | 191 +++++++++- shared/sx/ref/boot.sx | 7 +- shared/sx/ref/bootstrap_js.py | 165 +++------ shared/sx/ref/bootstrap_py.py | 224 +++--------- shared/sx/ref/deps.sx | 16 +- shared/sx/ref/parser.sx | 4 + shared/sx/ref/render.sx | 33 +- shared/sx/ref/signals.sx | 23 +- shared/sx/ref/sx_ref.py | 281 ++++++--------- 14 files changed, 971 insertions(+), 1001 deletions(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 2298595..812b8e2 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-08T11:56:02Z"; + var SX_VERSION = "2026-03-08T15:15:32Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -191,6 +191,17 @@ function trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); } function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; } + // invoke — call any callable (native fn or SX lambda) with args. + // Transpiled code emits direct calls f(args) which fail on SX lambdas + // from runtime-evaluated island bodies. invoke dispatches correctly. + function invoke() { + var f = arguments[0]; + var args = Array.prototype.slice.call(arguments, 1); + if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f))); + if (typeof f === 'function') return f.apply(null, args); + return NIL; + } + // JSON / dict helpers for island state serialization function jsonSerialize(obj) { try { return JSON.stringify(obj); } catch(e) { return "{}"; } @@ -216,17 +227,8 @@ // Render-expression detection — lets the evaluator delegate to the active adapter. // Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements. - function isRenderExpr(expr) { - if (!Array.isArray(expr) || !expr.length) return false; - var h = expr[0]; - if (!h || !h._sym) return false; - var n = h.name; - return !!(n === "<>" || n === "raw!" || - n.charAt(0) === "~" || n.indexOf("html:") === 0 || - (typeof HTML_TAGS !== "undefined" && HTML_TAGS.indexOf(n) >= 0) || - (typeof SVG_TAGS !== "undefined" && SVG_TAGS.indexOf(n) >= 0) || - (n.indexOf("-") > 0 && expr.length > 1 && expr[1] && expr[1]._kw)); - } + // 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). @@ -322,6 +324,8 @@ 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["concat"] = function() { var out = []; for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]); @@ -465,7 +469,10 @@ 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) { return f.apply(null, args); }; + 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"]; @@ -484,28 +491,12 @@ 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); } - // Serializer - function serialize(val) { - if (isNil(val)) return "nil"; - if (typeof val === "boolean") return val ? "true" : "false"; - if (typeof val === "number") return String(val); - if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; - if (isSym(val)) return val.name; - if (isKw(val)) return ":" + val.name; - if (Array.isArray(val)) return "(" + val.map(serialize).join(" ") + ")"; - return String(val); - } - - function isSpecialForm(n) { return n in { - "if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1, - "lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1, - "defhandler":1,"begin":1,"do":1, - "quote":1,"quasiquote":1,"->":1,"set!":1 - }; } - function isHoForm(n) { return n in { - "map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1 - }; } + // 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 @@ -1077,6 +1068,15 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai return local; })(); }; + // is-render-expr? + var isRenderExpr = function(expr) { return (isSxTruthy(sxOr(!isSxTruthy((typeOf(expr) == "list")), isEmpty(expr))) ? false : (function() { + var h = first(expr); + return (isSxTruthy(!isSxTruthy((typeOf(h) == "symbol"))) ? false : (function() { + var n = symbolName(h); + return sxOr((n == "<>"), (n == "raw!"), startsWith(n, "~"), startsWith(n, "html:"), contains(HTML_TAGS, n), (isSxTruthy((indexOf_(n, "-") > 0)) && isSxTruthy((len(expr) > 1)) && (typeOf(nth(expr, 1)) == "keyword"))); +})()); +})()); }; + // === Transpiled from parser === @@ -1188,6 +1188,269 @@ continue; } else { return NIL; } } }; // sx-serialize-dict var sxSerializeDict = function(d) { return (String("{") + String(join(" ", reduce(function(acc, key) { return concat(acc, [(String(":") + String(key)), sxSerialize(dictGet(d, key))]); }, [], keys(d)))) + String("}")); }; + // serialize + var serialize = sxSerialize; + + + // === Transpiled from adapter-html === + + // render-to-html + var renderToHtml = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); if (_m == "raw-html") return rawHtmlContent(expr); return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); }; + + // render-value-to-html + var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); return escapeHtml((String(val))); })(); }; + + // RENDER_HTML_FORMS + var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each"]; + + // render-html-form? + var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); }; + + // render-list-to-html + var renderListToHtml = function(expr, env) { return (isSxTruthy(isEmpty(expr)) ? "" : (function() { + var head = first(expr); + return (isSxTruthy(!isSxTruthy((typeOf(head) == "symbol"))) ? join("", map(function(x) { return renderValueToHtml(x, env); }, expr)) : (function() { + var name = symbolName(head); + var args = rest(expr); + return (isSxTruthy((name == "<>")) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy((name == "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy((isSxTruthy(startsWith(name, "~")) && isSxTruthy(envHas(env, name)) && isIsland(envGet(env, name)))) ? renderHtmlIsland(envGet(env, name), args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() { + var val = envGet(env, name); + return (isSxTruthy(isComponent(val)) ? renderHtmlComponent(val, args, env) : (isSxTruthy(isMacro(val)) ? renderToHtml(expandMacro(val, args, env), env) : error((String("Unknown component: ") + String(name))))); +})() : (isSxTruthy(isRenderHtmlForm(name)) ? dispatchHtmlForm(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(expandMacro(envGet(env, name), args, env), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))); +})()); +})()); }; + + // dispatch-html-form + var dispatchHtmlForm = function(name, expr, env) { return (isSxTruthy((name == "if")) ? (function() { + var condVal = trampoline(evalExpr(nth(expr, 1), env)); + return (isSxTruthy(condVal) ? renderToHtml(nth(expr, 2), env) : (isSxTruthy((len(expr) > 3)) ? renderToHtml(nth(expr, 3), env) : "")); +})() : (isSxTruthy((name == "when")) ? (isSxTruthy(!isSxTruthy(trampoline(evalExpr(nth(expr, 1), env)))) ? "" : join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(2, len(expr))))) : (isSxTruthy((name == "cond")) ? (function() { + var branch = evalCond(rest(expr), env); + return (isSxTruthy(branch) ? renderToHtml(branch, env) : ""); +})() : (isSxTruthy((name == "case")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy(sxOr((name == "let"), (name == "let*"))) ? (function() { + var local = processBindings(nth(expr, 1), env); + return join("", map(function(i) { return renderToHtml(nth(expr, i), local); }, range(2, len(expr)))); +})() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(1, len(expr)))) : (isSxTruthy(isDefinitionForm(name)) ? (trampoline(evalExpr(expr, env)), "") : (isSxTruthy((name == "map")) ? (function() { + var f = trampoline(evalExpr(nth(expr, 1), env)); + var coll = trampoline(evalExpr(nth(expr, 2), env)); + return join("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll)); +})() : (isSxTruthy((name == "map-indexed")) ? (function() { + var f = trampoline(evalExpr(nth(expr, 1), env)); + var coll = trampoline(evalExpr(nth(expr, 2), env)); + return join("", mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [i, item], env) : renderToHtml(apply(f, [i, item]), env)); }, coll)); +})() : (isSxTruthy((name == "filter")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy((name == "for-each")) ? (function() { + var f = trampoline(evalExpr(nth(expr, 1), env)); + var coll = trampoline(evalExpr(nth(expr, 2), env)); + return join("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll)); +})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))))); }; + + // render-lambda-html + var renderLambdaHtml = function(f, args, env) { return (function() { + var local = envMerge(lambdaClosure(f), env); + forEachIndexed(function(i, p) { return envSet(local, p, nth(args, i)); }, lambdaParams(f)); + return renderToHtml(lambdaBody(f), local); +})(); }; + + // render-html-component + var renderHtmlComponent = function(comp, args, env) { return (function() { + var kwargs = {}; + var children = []; + 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 = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env)); + kwargs[keywordName(arg)] = val; + return assoc(state, "skip", true, "i", (get(state, "i") + 1)); +})() : (append_b(children, arg), assoc(state, "i", (get(state, "i") + 1))))); +})(); }, {["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); } } + if (isSxTruthy(componentHasChildren(comp))) { + local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))); +} + return renderToHtml(componentBody(comp), local); +})(); +})(); }; + + // render-html-element + var renderHtmlElement = function(tag, args, env) { return (function() { + var parsed = parseElementArgs(args, env); + var attrs = first(parsed); + var children = nth(parsed, 1); + var isVoid = contains(VOID_ELEMENTS, tag); + return (String("<") + String(tag) + String(renderAttrs(attrs)) + String((isSxTruthy(isVoid) ? " />" : (String(">") + String(join("", map(function(c) { return renderToHtml(c, env); }, children))) + String(""))))); +})(); }; + + // render-html-island + var renderHtmlIsland = function(island, args, env) { return (function() { + var kwargs = {}; + var children = []; + 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 = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env)); + kwargs[keywordName(arg)] = val; + return assoc(state, "skip", true, "i", (get(state, "i") + 1)); +})() : (append_b(children, arg), assoc(state, "i", (get(state, "i") + 1))))); +})(); }, {["i"]: 0, ["skip"]: false}, args); + 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); } } + if (isSxTruthy(componentHasChildren(island))) { + 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("
")); +})(); +})(); +})(); }; + + // serialize-island-state + var serializeIslandState = function(kwargs) { return (isSxTruthy(isEmptyDict(kwargs)) ? NIL : jsonSerialize(kwargs)); }; + + + // === Transpiled from adapter-sx === + + // render-to-sx + var renderToSx = function(expr, env) { return (function() { + var result = aser(expr, env); + return (isSxTruthy((typeOf(result) == "string")) ? result : serialize(result)); +})(); }; + + // aser + var aser = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() { + var name = symbolName(expr); + return (isSxTruthy(envHas(env, name)) ? envGet(env, name) : (isSxTruthy(isPrimitive(name)) ? getPrimitive(name) : (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : error((String("Undefined symbol: ") + String(name)))))))); +})(); if (_m == "keyword") return keywordName(expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : aserList(expr, env)); return expr; })(); }; + + // aser-list + var aserList = function(expr, env) { return (function() { + var head = first(expr); + var args = rest(expr); + return (isSxTruthy(!isSxTruthy((typeOf(head) == "symbol"))) ? map(function(x) { return aser(x, env); }, expr) : (function() { + var name = symbolName(head); + return (isSxTruthy((name == "<>")) ? aserFragment(args, env) : (isSxTruthy(startsWith(name, "~")) ? aserCall(name, args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? aserCall(name, args, env) : (isSxTruthy(sxOr(isSpecialForm(name), isHoForm(name))) ? aserSpecial(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? aser(expandMacro(envGet(env, name), args, env), env) : (function() { + var f = trampoline(evalExpr(head, env)); + var evaledArgs = map(function(a) { return trampoline(evalExpr(a, env)); }, args); + return (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isSxTruthy(isLambda(f))) && isSxTruthy(!isSxTruthy(isComponent(f))) && !isSxTruthy(isIsland(f)))) ? apply(f, evaledArgs) : (isSxTruthy(isLambda(f)) ? trampoline(callLambda(f, evaledArgs, env)) : (isSxTruthy(isComponent(f)) ? aserCall((String("~") + String(componentName(f))), args, env) : (isSxTruthy(isIsland(f)) ? aserCall((String("~") + String(componentName(f))), args, env) : error((String("Not callable: ") + String(inspect(f)))))))); +})()))))); +})()); +})(); }; + + // 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(")"))); +})(); }; + + // 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); + 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)); +})() : (function() { + var val = aser(arg, env); + if (isSxTruthy(!isSxTruthy(isNil(val)))) { + parts.push(serialize(val)); +} + return assoc(state, "i", (get(state, "i") + 1)); +})())); +})(); }, {["i"]: 0, ["skip"]: false}, args); + return (String("(") + String(join(" ", parts)) + String(")")); +})(); }; + + // SPECIAL_FORM_NAMES + var SPECIAL_FORM_NAMES = ["if", "when", "cond", "case", "and", "or", "let", "let*", "lambda", "fn", "define", "defcomp", "defmacro", "defstyle", "defhandler", "defpage", "defquery", "defaction", "defrelation", "begin", "do", "quote", "quasiquote", "->", "set!", "letrec", "dynamic-wind", "defisland"]; + + // HO_FORM_NAMES + var HO_FORM_NAMES = ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"]; + + // special-form? + var isSpecialForm = function(name) { return contains(SPECIAL_FORM_NAMES, name); }; + + // ho-form? + var isHoForm = function(name) { return contains(HO_FORM_NAMES, name); }; + + // aser-special + var aserSpecial = function(name, expr, env) { return (function() { + var args = rest(expr); + return (isSxTruthy((name == "if")) ? (isSxTruthy(trampoline(evalExpr(first(args), env))) ? aser(nth(args, 1), env) : (isSxTruthy((len(args) > 2)) ? aser(nth(args, 2), env) : NIL)) : (isSxTruthy((name == "when")) ? (isSxTruthy(!isSxTruthy(trampoline(evalExpr(first(args), env)))) ? NIL : (function() { + var result = NIL; + { var _c = rest(args); for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } } + return result; +})()) : (isSxTruthy((name == "cond")) ? (function() { + var branch = evalCond(args, env); + return (isSxTruthy(branch) ? aser(branch, env) : NIL); +})() : (isSxTruthy((name == "case")) ? (function() { + var matchVal = trampoline(evalExpr(first(args), env)); + var clauses = rest(args); + return evalCaseAser(matchVal, clauses, env); +})() : (isSxTruthy(sxOr((name == "let"), (name == "let*"))) ? (function() { + var local = processBindings(first(args), env); + var result = NIL; + { var _c = rest(args); for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, local); } } + return result; +})() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? (function() { + var result = NIL; + { var _c = args; for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } } + return result; +})() : (isSxTruthy((name == "and")) ? (function() { + var result = true; + some(function(arg) { result = trampoline(evalExpr(arg, env)); +return !isSxTruthy(result); }, args); + return result; +})() : (isSxTruthy((name == "or")) ? (function() { + var result = false; + some(function(arg) { result = trampoline(evalExpr(arg, env)); +return result; }, args); + return result; +})() : (isSxTruthy((name == "map")) ? (function() { + var f = trampoline(evalExpr(first(args), env)); + 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; + return aser(lambdaBody(f), local); +})() : invoke(f, item)); }, coll); +})() : (isSxTruthy((name == "map-indexed")) ? (function() { + var f = trampoline(evalExpr(first(args), env)); + 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; + return aser(lambdaBody(f), local); +})() : invoke(f, i, item)); }, coll); +})() : (isSxTruthy((name == "for-each")) ? (function() { + var f = trampoline(evalExpr(first(args), env)); + var coll = trampoline(evalExpr(nth(args, 1), env)); + 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; + return append_b(results, aser(lambdaBody(f), local)); +})() : invoke(f, item)); } } + return (isSxTruthy(isEmpty(results)) ? NIL : results); +})() : (isSxTruthy((name == "defisland")) ? (trampoline(evalExpr(expr, env)), serialize(expr)) : (isSxTruthy(sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler"), (name == "defpage"), (name == "defquery"), (name == "defaction"), (name == "defrelation"))) ? (trampoline(evalExpr(expr, env)), NIL) : trampoline(evalExpr(expr, env))))))))))))))); +})(); }; + + // eval-case-aser + var evalCaseAser = function(matchVal, clauses, env) { return (isSxTruthy((len(clauses) < 2)) ? NIL : (function() { + var test = first(clauses); + var body = nth(clauses, 1); + return (isSxTruthy(sxOr((isSxTruthy((typeOf(test) == "keyword")) && (keywordName(test) == "else")), (isSxTruthy((typeOf(test) == "symbol")) && sxOr((symbolName(test) == ":else"), (symbolName(test) == "else"))))) ? aser(body, env) : (isSxTruthy((matchVal == trampoline(evalExpr(test, env)))) ? aser(body, env) : evalCaseAser(matchVal, slice(clauses, 2), env))); +})()); }; + // === Transpiled from adapter-dom === @@ -1229,10 +1492,7 @@ continue; } else { return NIL; } } }; 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 attrName = keywordName(arg); var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env)); - (isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy(startsWith(attrName, "on-")) && isCallable(attrVal))) ? (function() { - var eventName = substring(attrName, 3, stringLength(attrName)); - return domListen(el, eventName, attrVal); -})() : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal))))))); + (isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy(startsWith(attrName, "on-")) && isCallable(attrVal))) ? domListen(el, slice(attrName, 3), attrVal) : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal))))))); return assoc(state, "skip", true, "i", (get(state, "i") + 1)); })() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? domAppend(el, renderToDom(arg, env, newNs)) : NIL), assoc(state, "i", (get(state, "i") + 1))))); })(); }, {["i"]: 0, ["skip"]: false}, args); @@ -2445,7 +2705,8 @@ callExpr.push(dictGet(kwargs, k)); } } { 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); } } return (function() { var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); }); - morphChildren(el, bodyDom); + domSetTextContent(el, ""); + domAppend(el, bodyDom); domSetData(el, "sx-disposers", disposers); processElements(el); return logInfo((String("hydrated island: ") + String(compName) + String(" (") + String(len(disposers)) + String(" disposers)"))); @@ -2577,7 +2838,7 @@ return (function() { var prev = getTrackingContext(); setTrackingContext(ctx); return (function() { - var newVal = computeFn(); + var newVal = invoke(computeFn); setTrackingContext(prev); signalSetDeps(s, trackingContextDeps(ctx)); return (function() { @@ -2599,13 +2860,13 @@ return (function() { var disposed = false; var cleanupFn = NIL; return (function() { - var runEffect = function() { return (isSxTruthy(!isSxTruthy(disposed)) ? ((isSxTruthy(cleanupFn) ? cleanupFn() : NIL), forEach(function(dep) { return signalRemoveSub(dep, runEffect); }, deps), (deps = []), (function() { + var runEffect = function() { return (isSxTruthy(!isSxTruthy(disposed)) ? ((isSxTruthy(cleanupFn) ? invoke(cleanupFn) : NIL), forEach(function(dep) { return signalRemoveSub(dep, runEffect); }, deps), (deps = []), (function() { var ctx = makeTrackingContext(runEffect); return (function() { var prev = getTrackingContext(); setTrackingContext(ctx); return (function() { - var result = effectFn(); + var result = invoke(effectFn); setTrackingContext(prev); deps = trackingContextDeps(ctx); return (isSxTruthy(isCallable(result)) ? (cleanupFn = result) : NIL); @@ -2615,7 +2876,7 @@ return (function() { runEffect(); return function() { disposed = true; if (isSxTruthy(cleanupFn)) { - cleanupFn(); + invoke(cleanupFn); } { var _c = deps; for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, runEffect); } } return (deps = []); }; @@ -2630,7 +2891,7 @@ return (deps = []); }; // batch var batch = function(thunk) { _batchDepth = (_batchDepth + 1); -thunk(); +invoke(thunk); _batchDepth = (_batchDepth - 1); return (isSxTruthy((_batchDepth == 0)) ? (function() { var queue = _batchQueue; @@ -2679,7 +2940,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { var defStore = function(name, initFn) { return (function() { var registry = _storeRegistry; if (isSxTruthy(!isSxTruthy(hasKey_p(registry, name)))) { - _storeRegistry = assoc(registry, name, initFn()); + _storeRegistry = assoc(registry, name, invoke(initFn)); } return get(_storeRegistry, name); })(); }; @@ -2700,7 +2961,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { var bridgeEvent = function(el, eventName, targetSignal, transformFn) { return effect(function() { return (function() { var remove = domListen(el, eventName, function(e) { return (function() { var detail = eventDetail(e); - var newVal = (isSxTruthy(transformFn) ? transformFn(detail) : detail); + var newVal = (isSxTruthy(transformFn) ? invoke(transformFn, detail) : detail); return reset_b(targetSignal, newVal); })(); }); return remove; @@ -2858,8 +3119,12 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { function domListen(el, name, handler) { if (!_hasDom || !el) return function() {}; - el.addEventListener(name, handler); - return function() { el.removeEventListener(name, handler); }; + // Wrap SX lambdas from runtime-evaluated island code into native fns + var wrapped = isLambda(handler) + ? function(e) { invoke(handler, e); } + : handler; + el.addEventListener(name, wrapped); + return function() { el.removeEventListener(name, wrapped); }; } function eventDetail(e) { @@ -2900,79 +3165,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { try { return JSON.parse(s); } catch(e) { return {}; } } - // ========================================================================= - // Performance overrides — replace transpiled spec with imperative JS - // ========================================================================= - - // Override renderDomComponent: imperative kwarg parsing, no reduce/assoc - renderDomComponent = function(comp, args, env, ns) { - // Parse keyword args imperatively - var kwargs = {}; - var children = []; - for (var i = 0; i < args.length; i++) { - var arg = args[i]; - if (arg && arg._kw && (i + 1) < args.length) { - kwargs[arg.name] = trampoline(evalExpr(args[i + 1], env)); - i++; // skip value - } else { - children.push(arg); - } - } - // Build local env via prototype chain - var local = Object.create(componentClosure(comp)); - // Copy caller env own properties - for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k]; - // Bind params - var params = componentParams(comp); - for (var j = 0; j < params.length; j++) { - var p = params[j]; - local[p] = p in kwargs ? kwargs[p] : NIL; - } - // Bind children - if (componentHasChildren(comp)) { - var childFrag = document.createDocumentFragment(); - for (var c = 0; c < children.length; c++) { - var rendered = renderToDom(children[c], env, ns); - if (rendered) childFrag.appendChild(rendered); - } - local["children"] = childFrag; - } - return renderToDom(componentBody(comp), local, ns); - }; - - // Override renderDomElement: imperative attr parsing, no reduce/assoc - renderDomElement = function(tag, args, env, ns) { - var newNs = tag === "svg" ? SVG_NS : tag === "math" ? MATH_NS : ns; - var el = domCreateElement(tag, newNs); - var extraClasses = []; - 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 = trampoline(evalExpr(args[i + 1], env)); - i++; // skip value - if (isNil(attrVal) || attrVal === false) continue; - 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 = renderToDom(arg, env, newNs); - if (child) el.appendChild(child); - } - } - } - if (extraClasses.length) { - var existing = el.getAttribute("class") || ""; - el.setAttribute("class", (existing ? existing + " " : "") + extraClasses.join(" ")); - } - return el; - }; + // renderDomComponent and renderDomElement are transpiled from + // adapter-dom.sx — no imperative overrides needed. // ========================================================================= @@ -3092,8 +3286,14 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { // --- Timers --- - function setTimeout_(fn, ms) { return setTimeout(fn, ms || 0); } - function setInterval_(fn, ms) { return setInterval(fn, ms || 1000); } + 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) { @@ -3619,6 +3819,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { logInfo("sx:route server " + pathname); executeRequest(link, { method: "GET", url: liveHref }).then(function() { try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} + }).catch(function(err) { + logWarn("sx:route server fetch error: " + (err && err.message ? err.message : err)); }); } }); @@ -4004,11 +4206,14 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { }; // Expose render functions as primitives so SX code can call them + if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml; + if (typeof renderToSx === "function") PRIMITIVES["render-to-sx"] = renderToSx; + if (typeof aser === "function") PRIMITIVES["aser"] = aser; if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom; // Expose signal functions as primitives so runtime-evaluated SX code // (e.g. island bodies from .sx files) can call them - PRIMITIVES["signal"] = createSignal; + PRIMITIVES["signal"] = signal; PRIMITIVES["signal?"] = isSignal; PRIMITIVES["deref"] = deref; PRIMITIVES["reset!"] = reset_b; @@ -4016,7 +4221,9 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { PRIMITIVES["computed"] = computed; PRIMITIVES["effect"] = effect; PRIMITIVES["batch"] = batch; - PRIMITIVES["dispose"] = dispose; + // 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; @@ -4200,9 +4407,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { return null; } - // Component + // 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)); @@ -4691,12 +4899,25 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { } 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; } + 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(""); + } + var Sx = { VERSION: "ref-2.0", parse: parse, @@ -4704,7 +4925,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { eval: function(expr, env) { return trampoline(evalExpr(expr, env || merge(componentEnv))); }, loadComponents: loadComponents, render: render, - + renderToString: renderToString, serialize: serialize, NIL: NIL, Symbol: Symbol, @@ -4712,6 +4933,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { isTruthy: isSxTruthy, isNil: isNil, componentEnv: componentEnv, + renderToHtml: function(expr, env) { return renderToHtml(expr, env || merge(componentEnv)); }, + renderToSx: function(expr, env) { return renderToSx(expr, env || merge(componentEnv)); }, renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null, parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null, parseTime: typeof parseTime === "function" ? parseTime : null, @@ -4759,7 +4982,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { emitEvent: emitEvent, onEvent: onEvent, bridgeEvent: bridgeEvent, - _version: "ref-2.0 (boot+dom+engine+orchestration+parser, bootstrap-compiled)" + _version: "ref-2.0 (boot+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)" }; diff --git a/shared/static/scripts/sx-ref.js b/shared/static/scripts/sx-ref.js index 64b3d5a..812b8e2 100644 --- a/shared/static/scripts/sx-ref.js +++ b/shared/static/scripts/sx-ref.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-08T11:49:09Z"; + var SX_VERSION = "2026-03-08T15:15:32Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -191,6 +191,17 @@ function trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); } function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; } + // invoke — call any callable (native fn or SX lambda) with args. + // Transpiled code emits direct calls f(args) which fail on SX lambdas + // from runtime-evaluated island bodies. invoke dispatches correctly. + function invoke() { + var f = arguments[0]; + var args = Array.prototype.slice.call(arguments, 1); + if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f))); + if (typeof f === 'function') return f.apply(null, args); + return NIL; + } + // JSON / dict helpers for island state serialization function jsonSerialize(obj) { try { return JSON.stringify(obj); } catch(e) { return "{}"; } @@ -216,17 +227,8 @@ // Render-expression detection — lets the evaluator delegate to the active adapter. // Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements. - function isRenderExpr(expr) { - if (!Array.isArray(expr) || !expr.length) return false; - var h = expr[0]; - if (!h || !h._sym) return false; - var n = h.name; - return !!(n === "<>" || n === "raw!" || - n.charAt(0) === "~" || n.indexOf("html:") === 0 || - (typeof HTML_TAGS !== "undefined" && HTML_TAGS.indexOf(n) >= 0) || - (typeof SVG_TAGS !== "undefined" && SVG_TAGS.indexOf(n) >= 0) || - (n.indexOf("-") > 0 && expr.length > 1 && expr[1] && expr[1]._kw)); - } + // 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). @@ -322,6 +324,8 @@ 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["concat"] = function() { var out = []; for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]); @@ -465,7 +469,10 @@ 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) { return f.apply(null, args); }; + 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"]; @@ -484,28 +491,12 @@ 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); } - // Serializer - function serialize(val) { - if (isNil(val)) return "nil"; - if (typeof val === "boolean") return val ? "true" : "false"; - if (typeof val === "number") return String(val); - if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; - if (isSym(val)) return val.name; - if (isKw(val)) return ":" + val.name; - if (Array.isArray(val)) return "(" + val.map(serialize).join(" ") + ")"; - return String(val); - } - - function isSpecialForm(n) { return n in { - "if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1, - "lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1, - "defhandler":1,"begin":1,"do":1, - "quote":1,"quasiquote":1,"->":1,"set!":1 - }; } - function isHoForm(n) { return n in { - "map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1 - }; } + // 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 @@ -572,92 +563,6 @@ return makeThunk(componentBody(comp), local); }; - // ========================================================================= - // 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 interface — Parser // ========================================================================= @@ -1163,6 +1068,15 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai return local; })(); }; + // is-render-expr? + var isRenderExpr = function(expr) { return (isSxTruthy(sxOr(!isSxTruthy((typeOf(expr) == "list")), isEmpty(expr))) ? false : (function() { + var h = first(expr); + return (isSxTruthy(!isSxTruthy((typeOf(h) == "symbol"))) ? false : (function() { + var n = symbolName(h); + return sxOr((n == "<>"), (n == "raw!"), startsWith(n, "~"), startsWith(n, "html:"), contains(HTML_TAGS, n), (isSxTruthy((indexOf_(n, "-") > 0)) && isSxTruthy((len(expr) > 1)) && (typeOf(nth(expr, 1)) == "keyword"))); +})()); +})()); }; + // === Transpiled from parser === @@ -1274,6 +1188,9 @@ continue; } else { return NIL; } } }; // sx-serialize-dict var sxSerializeDict = function(d) { return (String("{") + String(join(" ", reduce(function(acc, key) { return concat(acc, [(String(":") + String(key)), sxSerialize(dictGet(d, key))]); }, [], keys(d)))) + String("}")); }; + // serialize + var serialize = sxSerialize; + // === Transpiled from adapter-html === @@ -1452,6 +1369,88 @@ continue; } else { return NIL; } } }; return (String("(") + String(join(" ", parts)) + String(")")); })(); }; + // SPECIAL_FORM_NAMES + var SPECIAL_FORM_NAMES = ["if", "when", "cond", "case", "and", "or", "let", "let*", "lambda", "fn", "define", "defcomp", "defmacro", "defstyle", "defhandler", "defpage", "defquery", "defaction", "defrelation", "begin", "do", "quote", "quasiquote", "->", "set!", "letrec", "dynamic-wind", "defisland"]; + + // HO_FORM_NAMES + var HO_FORM_NAMES = ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"]; + + // special-form? + var isSpecialForm = function(name) { return contains(SPECIAL_FORM_NAMES, name); }; + + // ho-form? + var isHoForm = function(name) { return contains(HO_FORM_NAMES, name); }; + + // aser-special + var aserSpecial = function(name, expr, env) { return (function() { + var args = rest(expr); + return (isSxTruthy((name == "if")) ? (isSxTruthy(trampoline(evalExpr(first(args), env))) ? aser(nth(args, 1), env) : (isSxTruthy((len(args) > 2)) ? aser(nth(args, 2), env) : NIL)) : (isSxTruthy((name == "when")) ? (isSxTruthy(!isSxTruthy(trampoline(evalExpr(first(args), env)))) ? NIL : (function() { + var result = NIL; + { var _c = rest(args); for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } } + return result; +})()) : (isSxTruthy((name == "cond")) ? (function() { + var branch = evalCond(args, env); + return (isSxTruthy(branch) ? aser(branch, env) : NIL); +})() : (isSxTruthy((name == "case")) ? (function() { + var matchVal = trampoline(evalExpr(first(args), env)); + var clauses = rest(args); + return evalCaseAser(matchVal, clauses, env); +})() : (isSxTruthy(sxOr((name == "let"), (name == "let*"))) ? (function() { + var local = processBindings(first(args), env); + var result = NIL; + { var _c = rest(args); for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, local); } } + return result; +})() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? (function() { + var result = NIL; + { var _c = args; for (var _i = 0; _i < _c.length; _i++) { var body = _c[_i]; result = aser(body, env); } } + return result; +})() : (isSxTruthy((name == "and")) ? (function() { + var result = true; + some(function(arg) { result = trampoline(evalExpr(arg, env)); +return !isSxTruthy(result); }, args); + return result; +})() : (isSxTruthy((name == "or")) ? (function() { + var result = false; + some(function(arg) { result = trampoline(evalExpr(arg, env)); +return result; }, args); + return result; +})() : (isSxTruthy((name == "map")) ? (function() { + var f = trampoline(evalExpr(first(args), env)); + 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; + return aser(lambdaBody(f), local); +})() : invoke(f, item)); }, coll); +})() : (isSxTruthy((name == "map-indexed")) ? (function() { + var f = trampoline(evalExpr(first(args), env)); + 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; + return aser(lambdaBody(f), local); +})() : invoke(f, i, item)); }, coll); +})() : (isSxTruthy((name == "for-each")) ? (function() { + var f = trampoline(evalExpr(first(args), env)); + var coll = trampoline(evalExpr(nth(args, 1), env)); + 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; + return append_b(results, aser(lambdaBody(f), local)); +})() : invoke(f, item)); } } + return (isSxTruthy(isEmpty(results)) ? NIL : results); +})() : (isSxTruthy((name == "defisland")) ? (trampoline(evalExpr(expr, env)), serialize(expr)) : (isSxTruthy(sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler"), (name == "defpage"), (name == "defquery"), (name == "defaction"), (name == "defrelation"))) ? (trampoline(evalExpr(expr, env)), NIL) : trampoline(evalExpr(expr, env))))))))))))))); +})(); }; + + // eval-case-aser + var evalCaseAser = function(matchVal, clauses, env) { return (isSxTruthy((len(clauses) < 2)) ? NIL : (function() { + var test = first(clauses); + var body = nth(clauses, 1); + return (isSxTruthy(sxOr((isSxTruthy((typeOf(test) == "keyword")) && (keywordName(test) == "else")), (isSxTruthy((typeOf(test) == "symbol")) && sxOr((symbolName(test) == ":else"), (symbolName(test) == "else"))))) ? aser(body, env) : (isSxTruthy((matchVal == trampoline(evalExpr(test, env)))) ? aser(body, env) : evalCaseAser(matchVal, slice(clauses, 2), env))); +})()); }; + // === Transpiled from adapter-dom === @@ -1493,10 +1492,7 @@ continue; } else { return NIL; } } }; 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 attrName = keywordName(arg); var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env)); - (isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy(startsWith(attrName, "on-")) && isCallable(attrVal))) ? (function() { - var eventName = substring(attrName, 3, stringLength(attrName)); - return domListen(el, eventName, attrVal); -})() : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal))))))); + (isSxTruthy(sxOr(isNil(attrVal), (attrVal == false))) ? NIL : (isSxTruthy((isSxTruthy(startsWith(attrName, "on-")) && isCallable(attrVal))) ? domListen(el, slice(attrName, 3), attrVal) : (isSxTruthy(contains(BOOLEAN_ATTRS, attrName)) ? (isSxTruthy(attrVal) ? domSetAttr(el, attrName, "") : NIL) : (isSxTruthy((attrVal == true)) ? domSetAttr(el, attrName, "") : domSetAttr(el, attrName, (String(attrVal))))))); return assoc(state, "skip", true, "i", (get(state, "i") + 1)); })() : ((isSxTruthy(!isSxTruthy(contains(VOID_ELEMENTS, tag))) ? domAppend(el, renderToDom(arg, env, newNs)) : NIL), assoc(state, "i", (get(state, "i") + 1))))); })(); }, {["i"]: 0, ["skip"]: false}, args); @@ -2709,7 +2705,8 @@ callExpr.push(dictGet(kwargs, k)); } } { 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); } } return (function() { var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); }); - morphChildren(el, bodyDom); + domSetTextContent(el, ""); + domAppend(el, bodyDom); domSetData(el, "sx-disposers", disposers); processElements(el); return logInfo((String("hydrated island: ") + String(compName) + String(" (") + String(len(disposers)) + String(" disposers)"))); @@ -2729,146 +2726,6 @@ callExpr.push(dictGet(kwargs, k)); } } var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), sxHydrateIslands(NIL), processElements(NIL)); }; - // === Transpiled from deps (component dependency analysis) === - - // scan-refs - var scanRefs = function(node) { return (function() { - var refs = []; - scanRefsWalk(node, refs); - return refs; -})(); }; - - // scan-refs-walk - var scanRefsWalk = function(node, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() { - var name = symbolName(node); - return (isSxTruthy(startsWith(name, "~")) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL); -})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanRefsWalk(item, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanRefsWalk(dictGet(node, key), refs); }, keys(node)) : NIL))); }; - - // transitive-deps-walk - var transitiveDepsWalk = function(n, seen, env) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() { - var val = envGet(env, n); - return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(componentBody(val))) : (isSxTruthy((typeOf(val) == "macro")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(macroBody(val))) : NIL)); -})()) : NIL); }; - - // transitive-deps - var transitiveDeps = function(name, env) { return (function() { - var seen = []; - var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); - transitiveDepsWalk(key, seen, env); - return filter(function(x) { return !isSxTruthy((x == key)); }, seen); -})(); }; - - // compute-all-deps - var computeAllDeps = function(env) { return forEach(function(name) { return (function() { - var val = envGet(env, name); - return (isSxTruthy((typeOf(val) == "component")) ? componentSetDeps(val, transitiveDeps(name, env)) : NIL); -})(); }, envComponents(env)); }; - - // scan-components-from-source - var scanComponentsFromSource = function(source) { return (function() { - var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)", source); - return map(function(m) { return (String("~") + String(m)); }, matches); -})(); }; - - // components-needed - var componentsNeeded = function(pageSource, env) { return (function() { - var direct = scanComponentsFromSource(pageSource); - var allNeeded = []; - { var _c = direct; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(allNeeded, name)))) { - allNeeded.push(name); -} -(function() { - var val = envGet(env, name); - return (function() { - var deps = (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && !isSxTruthy(isEmpty(componentDeps(val))))) ? componentDeps(val) : transitiveDeps(name, env)); - return forEach(function(dep) { return (isSxTruthy(!isSxTruthy(contains(allNeeded, dep))) ? append_b(allNeeded, dep) : NIL); }, deps); -})(); -})(); } } - return allNeeded; -})(); }; - - // page-component-bundle - var pageComponentBundle = function(pageSource, env) { return componentsNeeded(pageSource, env); }; - - // page-css-classes - var pageCssClasses = function(pageSource, env) { return (function() { - var needed = componentsNeeded(pageSource, env); - var classes = []; - { var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() { - var val = envGet(env, name); - return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(cls) { return (isSxTruthy(!isSxTruthy(contains(classes, cls))) ? append_b(classes, cls) : NIL); }, componentCssClasses(val)) : NIL); -})(); } } - { var _c = scanCssClasses(pageSource); for (var _i = 0; _i < _c.length; _i++) { var cls = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(classes, cls)))) { - classes.push(cls); -} } } - return classes; -})(); }; - - // scan-io-refs-walk - var scanIoRefsWalk = function(node, ioNames, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() { - var name = symbolName(node); - return (isSxTruthy(contains(ioNames, name)) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL); -})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanIoRefsWalk(item, ioNames, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanIoRefsWalk(dictGet(node, key), ioNames, refs); }, keys(node)) : NIL))); }; - - // scan-io-refs - var scanIoRefs = function(node, ioNames) { return (function() { - var refs = []; - scanIoRefsWalk(node, ioNames, refs); - return refs; -})(); }; - - // transitive-io-refs-walk - var transitiveIoRefsWalk = function(n, seen, allRefs, env, ioNames) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() { - var val = envGet(env, n); - return (isSxTruthy((typeOf(val) == "component")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(componentBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(componentBody(val)))) : (isSxTruthy((typeOf(val) == "macro")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(macroBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(macroBody(val)))) : NIL)); -})()) : NIL); }; - - // transitive-io-refs - var transitiveIoRefs = function(name, env, ioNames) { return (function() { - var allRefs = []; - var seen = []; - var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); - transitiveIoRefsWalk(key, seen, allRefs, env, ioNames); - return allRefs; -})(); }; - - // compute-all-io-refs - var computeAllIoRefs = function(env, ioNames) { return forEach(function(name) { return (function() { - var val = envGet(env, name); - return (isSxTruthy((typeOf(val) == "component")) ? componentSetIoRefs(val, transitiveIoRefs(name, env, ioNames)) : NIL); -})(); }, envComponents(env)); }; - - // component-pure? - var componentPure_p = function(name, env, ioNames) { return isEmpty(transitiveIoRefs(name, env, ioNames)); }; - - // render-target - var renderTarget = function(name, env, ioNames) { return (function() { - var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); - return (function() { - var val = envGet(env, key); - return (isSxTruthy(!isSxTruthy((typeOf(val) == "component"))) ? "server" : (function() { - var affinity = componentAffinity(val); - return (isSxTruthy((affinity == "server")) ? "server" : (isSxTruthy((affinity == "client")) ? "client" : (isSxTruthy(!isSxTruthy(componentPure_p(name, env, ioNames))) ? "server" : "client"))); -})()); -})(); -})(); }; - - // page-render-plan - var pageRenderPlan = function(pageSource, env, ioNames) { return (function() { - var needed = componentsNeeded(pageSource, env); - var compTargets = {}; - var serverList = []; - var clientList = []; - var ioDeps = []; - { var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() { - var target = renderTarget(name, env, ioNames); - compTargets[name] = target; - return (isSxTruthy((target == "server")) ? (append_b(serverList, name), forEach(function(ioRef) { return (isSxTruthy(!isSxTruthy(contains(ioDeps, ioRef))) ? append_b(ioDeps, ioRef) : NIL); }, transitiveIoRefs(name, env, ioNames))) : append_b(clientList, name)); -})(); } } - return {"components": compTargets, "server": serverList, "client": clientList, "io-deps": ioDeps}; -})(); }; - - // === Transpiled from router (client-side route matching) === // split-path-segments @@ -2981,7 +2838,7 @@ return (function() { var prev = getTrackingContext(); setTrackingContext(ctx); return (function() { - var newVal = computeFn(); + var newVal = invoke(computeFn); setTrackingContext(prev); signalSetDeps(s, trackingContextDeps(ctx)); return (function() { @@ -3003,13 +2860,13 @@ return (function() { var disposed = false; var cleanupFn = NIL; return (function() { - var runEffect = function() { return (isSxTruthy(!isSxTruthy(disposed)) ? ((isSxTruthy(cleanupFn) ? cleanupFn() : NIL), forEach(function(dep) { return signalRemoveSub(dep, runEffect); }, deps), (deps = []), (function() { + var runEffect = function() { return (isSxTruthy(!isSxTruthy(disposed)) ? ((isSxTruthy(cleanupFn) ? invoke(cleanupFn) : NIL), forEach(function(dep) { return signalRemoveSub(dep, runEffect); }, deps), (deps = []), (function() { var ctx = makeTrackingContext(runEffect); return (function() { var prev = getTrackingContext(); setTrackingContext(ctx); return (function() { - var result = effectFn(); + var result = invoke(effectFn); setTrackingContext(prev); deps = trackingContextDeps(ctx); return (isSxTruthy(isCallable(result)) ? (cleanupFn = result) : NIL); @@ -3019,7 +2876,7 @@ return (function() { runEffect(); return function() { disposed = true; if (isSxTruthy(cleanupFn)) { - cleanupFn(); + invoke(cleanupFn); } { var _c = deps; for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, runEffect); } } return (deps = []); }; @@ -3034,7 +2891,7 @@ return (deps = []); }; // batch var batch = function(thunk) { _batchDepth = (_batchDepth + 1); -thunk(); +invoke(thunk); _batchDepth = (_batchDepth - 1); return (isSxTruthy((_batchDepth == 0)) ? (function() { var queue = _batchQueue; @@ -3083,7 +2940,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { var defStore = function(name, initFn) { return (function() { var registry = _storeRegistry; if (isSxTruthy(!isSxTruthy(hasKey_p(registry, name)))) { - _storeRegistry = assoc(registry, name, initFn()); + _storeRegistry = assoc(registry, name, invoke(initFn)); } return get(_storeRegistry, name); })(); }; @@ -3104,7 +2961,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { var bridgeEvent = function(el, eventName, targetSignal, transformFn) { return effect(function() { return (function() { var remove = domListen(el, eventName, function(e) { return (function() { var detail = eventDetail(e); - var newVal = (isSxTruthy(transformFn) ? transformFn(detail) : detail); + var newVal = (isSxTruthy(transformFn) ? invoke(transformFn, detail) : detail); return reset_b(targetSignal, newVal); })(); }); return remove; @@ -3262,8 +3119,12 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { function domListen(el, name, handler) { if (!_hasDom || !el) return function() {}; - el.addEventListener(name, handler); - return function() { el.removeEventListener(name, handler); }; + // Wrap SX lambdas from runtime-evaluated island code into native fns + var wrapped = isLambda(handler) + ? function(e) { invoke(handler, e); } + : handler; + el.addEventListener(name, wrapped); + return function() { el.removeEventListener(name, wrapped); }; } function eventDetail(e) { @@ -3304,79 +3165,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { try { return JSON.parse(s); } catch(e) { return {}; } } - // ========================================================================= - // Performance overrides — replace transpiled spec with imperative JS - // ========================================================================= - - // Override renderDomComponent: imperative kwarg parsing, no reduce/assoc - renderDomComponent = function(comp, args, env, ns) { - // Parse keyword args imperatively - var kwargs = {}; - var children = []; - for (var i = 0; i < args.length; i++) { - var arg = args[i]; - if (arg && arg._kw && (i + 1) < args.length) { - kwargs[arg.name] = trampoline(evalExpr(args[i + 1], env)); - i++; // skip value - } else { - children.push(arg); - } - } - // Build local env via prototype chain - var local = Object.create(componentClosure(comp)); - // Copy caller env own properties - for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k]; - // Bind params - var params = componentParams(comp); - for (var j = 0; j < params.length; j++) { - var p = params[j]; - local[p] = p in kwargs ? kwargs[p] : NIL; - } - // Bind children - if (componentHasChildren(comp)) { - var childFrag = document.createDocumentFragment(); - for (var c = 0; c < children.length; c++) { - var rendered = renderToDom(children[c], env, ns); - if (rendered) childFrag.appendChild(rendered); - } - local["children"] = childFrag; - } - return renderToDom(componentBody(comp), local, ns); - }; - - // Override renderDomElement: imperative attr parsing, no reduce/assoc - renderDomElement = function(tag, args, env, ns) { - var newNs = tag === "svg" ? SVG_NS : tag === "math" ? MATH_NS : ns; - var el = domCreateElement(tag, newNs); - var extraClasses = []; - 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 = trampoline(evalExpr(args[i + 1], env)); - i++; // skip value - if (isNil(attrVal) || attrVal === false) continue; - 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 = renderToDom(arg, env, newNs); - if (child) el.appendChild(child); - } - } - } - if (extraClasses.length) { - var existing = el.getAttribute("class") || ""; - el.setAttribute("class", (existing ? existing + " " : "") + extraClasses.join(" ")); - } - return el; - }; + // renderDomComponent and renderDomElement are transpiled from + // adapter-dom.sx — no imperative overrides needed. // ========================================================================= @@ -3496,8 +3286,14 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { // --- Timers --- - function setTimeout_(fn, ms) { return setTimeout(fn, ms || 0); } - function setInterval_(fn, ms) { return setInterval(fn, ms || 1000); } + 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) { @@ -4023,6 +3819,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { logInfo("sx:route server " + pathname); executeRequest(link, { method: "GET", url: liveHref }).then(function() { try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} + }).catch(function(err) { + logWarn("sx:route server fetch error: " + (err && err.message ? err.message : err)); }); } }); @@ -4415,7 +4213,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { // Expose signal functions as primitives so runtime-evaluated SX code // (e.g. island bodies from .sx files) can call them - PRIMITIVES["signal"] = createSignal; + PRIMITIVES["signal"] = signal; PRIMITIVES["signal?"] = isSignal; PRIMITIVES["deref"] = deref; PRIMITIVES["reset!"] = reset_b; @@ -4423,7 +4221,9 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { PRIMITIVES["computed"] = computed; PRIMITIVES["effect"] = effect; PRIMITIVES["batch"] = batch; - PRIMITIVES["dispose"] = dispose; + // 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; @@ -4607,9 +4407,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { return null; } - // Component + // 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)); @@ -5158,17 +4959,6 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() { hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null, disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null, init: typeof bootInit === "function" ? bootInit : null, - scanRefs: scanRefs, - scanComponentsFromSource: scanComponentsFromSource, - transitiveDeps: transitiveDeps, - computeAllDeps: computeAllDeps, - componentsNeeded: componentsNeeded, - pageComponentBundle: pageComponentBundle, - pageCssClasses: pageCssClasses, - scanIoRefs: scanIoRefs, - transitiveIoRefs: transitiveIoRefs, - computeAllIoRefs: computeAllIoRefs, - componentPure_p: componentPure_p, splitPathSegments: splitPathSegments, parseRoutePattern: parseRoutePattern, matchRoute: matchRoute, diff --git a/shared/sx/async_eval.py b/shared/sx/async_eval.py index c3621bf..3613a12 100644 --- a/shared/sx/async_eval.py +++ b/shared/sx/async_eval.py @@ -45,7 +45,7 @@ import contextvars import inspect from typing import Any -from .types import Component, Keyword, Lambda, Macro, NIL, Symbol +from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol # When True, _aser expands known components server-side instead of serializing # them for client rendering. Set during page slot evaluation so Python-only @@ -1219,6 +1219,8 @@ async def _eval_slot_inner( if isinstance(result, str): return SxExpr(result) return SxExpr(serialize(result)) + elif isinstance(comp, Island): + pass # Islands serialize as SX for client-side rendering else: import logging logging.getLogger("sx.eval").error( @@ -1596,6 +1598,14 @@ async def _assf_define(expr, env, ctx): return NIL +async def _assf_defisland(expr, env, ctx): + """Evaluate defisland AND serialize it — client needs the definition.""" + import logging + logging.getLogger("sx.eval").info("_assf_defisland called for: %s", expr[1] if len(expr) > 1 else expr) + await async_eval(expr, env, ctx) + return serialize(expr) + + async def _assf_lambda(expr, env, ctx): return await _asf_lambda(expr, env, ctx) @@ -1703,7 +1713,7 @@ _ASER_FORMS: dict[str, Any] = { "defcomp": _assf_define, "defmacro": _assf_define, "defhandler": _assf_define, - "defisland": _assf_define, + "defisland": _assf_defisland, "begin": _assf_begin, "do": _assf_begin, "quote": _assf_quote, diff --git a/shared/sx/helpers.py b/shared/sx/helpers.py index 98f1353..813f5ad 100644 --- a/shared/sx/helpers.py +++ b/shared/sx/helpers.py @@ -473,7 +473,7 @@ def components_for_request(source: str = "", from quart import request from .jinja_bridge import _COMPONENT_ENV from .deps import components_needed - from .types import Component, Macro + from .types import Component, Island, Macro from .parser import serialize # Determine which components the page needs @@ -493,7 +493,19 @@ def components_for_request(source: str = "", parts = [] for key, val in _COMPONENT_ENV.items(): - if isinstance(val, Component): + if isinstance(val, Island): + comp_name = f"~{val.name}" + if needed is not None and comp_name not in needed and key not in needed: + continue + if comp_name in loaded or val.name in loaded: + continue + 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) + parts.append(f"(defisland ~{val.name} {params_sx} {body_sx})") + elif isinstance(val, Component): comp_name = f"~{val.name}" # Skip if not needed for this page if needed is not None and comp_name not in needed and key not in needed: diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx index 2ebc1f8..86a5b22 100644 --- a/shared/sx/ref/adapter-dom.sx +++ b/shared/sx/ref/adapter-dom.sx @@ -181,8 +181,7 @@ ;; Value must be callable (lambda/function) (and (starts-with? attr-name "on-") (callable? attr-val)) - (let ((event-name (substring attr-name 3 (string-length attr-name)))) - (dom-listen el event-name attr-val)) + (dom-listen el (slice attr-name 3) attr-val) ;; Boolean attr (contains? BOOLEAN_ATTRS attr-name) (when attr-val (dom-set-attr el attr-name "")) diff --git a/shared/sx/ref/adapter-sx.sx b/shared/sx/ref/adapter-sx.sx index 1af36ac..e996c64 100644 --- a/shared/sx/ref/adapter-sx.sx +++ b/shared/sx/ref/adapter-sx.sx @@ -130,20 +130,191 @@ (str "(" (join " " parts) ")")))) +;; -------------------------------------------------------------------------- +;; Form classification +;; -------------------------------------------------------------------------- + +(define SPECIAL_FORM_NAMES + (list "if" "when" "cond" "case" "and" "or" + "let" "let*" "lambda" "fn" + "define" "defcomp" "defmacro" "defstyle" + "defhandler" "defpage" "defquery" "defaction" "defrelation" + "begin" "do" "quote" "quasiquote" + "->" "set!" "letrec" "dynamic-wind" "defisland")) + +(define HO_FORM_NAMES + (list "map" "map-indexed" "filter" "reduce" + "some" "every?" "for-each")) + +(define special-form? + (fn (name) + (contains? SPECIAL_FORM_NAMES name))) + +(define ho-form? + (fn (name) + (contains? HO_FORM_NAMES name))) + + +;; -------------------------------------------------------------------------- +;; aser-special — evaluate special/HO forms in aser mode +;; -------------------------------------------------------------------------- +;; +;; Control flow forms evaluate conditions normally but render branches +;; through aser (serializing tags/components instead of rendering HTML). +;; Definition forms evaluate for side effects and return nil. + +(define aser-special + (fn (name expr env) + (let ((args (rest expr))) + (cond + ;; if — evaluate condition, aser chosen branch + (= name "if") + (if (trampoline (eval-expr (first args) env)) + (aser (nth args 1) env) + (if (> (len args) 2) + (aser (nth args 2) env) + nil)) + + ;; when — evaluate condition, aser body if true + (= name "when") + (if (not (trampoline (eval-expr (first args) env))) + nil + (let ((result nil)) + (for-each (fn (body) (set! result (aser body env))) + (rest args)) + result)) + + ;; cond — evaluate conditions, aser matching branch + (= name "cond") + (let ((branch (eval-cond args env))) + (if branch (aser branch env) nil)) + + ;; case — evaluate match value, check each pair + (= name "case") + (let ((match-val (trampoline (eval-expr (first args) env))) + (clauses (rest args))) + (eval-case-aser match-val clauses env)) + + ;; let / let* + (or (= name "let") (= name "let*")) + (let ((local (process-bindings (first args) env)) + (result nil)) + (for-each (fn (body) (set! result (aser body local))) + (rest args)) + result) + + ;; begin / do + (or (= name "begin") (= name "do")) + (let ((result nil)) + (for-each (fn (body) (set! result (aser body env))) args) + result) + + ;; and — short-circuit + (= name "and") + (let ((result true)) + (some (fn (arg) + (set! result (trampoline (eval-expr arg env))) + (not result)) + args) + result) + + ;; or — short-circuit + (= name "or") + (let ((result false)) + (some (fn (arg) + (set! result (trampoline (eval-expr arg env))) + result) + args) + result) + + ;; map — evaluate function and collection, map through aser + (= name "map") + (let ((f (trampoline (eval-expr (first args) env))) + (coll (trampoline (eval-expr (nth args 1) env)))) + (map (fn (item) + (if (lambda? f) + (let ((local (env-merge (lambda-closure f) env))) + (env-set! local (first (lambda-params f)) item) + (aser (lambda-body f) local)) + (invoke f item))) + coll)) + + ;; map-indexed + (= name "map-indexed") + (let ((f (trampoline (eval-expr (first args) env))) + (coll (trampoline (eval-expr (nth args 1) env)))) + (map-indexed (fn (i 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) + (aser (lambda-body f) local)) + (invoke f i item))) + coll)) + + ;; for-each — evaluate for side effects, aser each body + (= name "for-each") + (let ((f (trampoline (eval-expr (first args) env))) + (coll (trampoline (eval-expr (nth args 1) env))) + (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 (aser (lambda-body f) local))) + (invoke f item))) + coll) + (if (empty? results) nil results)) + + ;; defisland — evaluate AND serialize (client needs the definition) + (= name "defisland") + (do (trampoline (eval-expr expr env)) + (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") (= name "defrelation")) + (do (trampoline (eval-expr expr env)) nil) + + ;; Everything else — evaluate normally + :else + (trampoline (eval-expr expr env)))))) + + +;; Helper: case dispatch for aser mode +(define eval-case-aser + (fn (match-val clauses env) + (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")))) + (aser body env) + (if (= match-val (trampoline (eval-expr test env))) + (aser body env) + (eval-case-aser match-val (slice clauses 2) env))))))) + + ;; -------------------------------------------------------------------------- ;; Platform interface — SX wire adapter ;; -------------------------------------------------------------------------- ;; -;; Serialization: -;; (serialize val) → SX source string representation of val -;; -;; Form classification: -;; (special-form? name) → boolean -;; (ho-form? name) → boolean -;; (aser-special name expr env) → evaluate special/HO form through aser -;; ;; From eval.sx: ;; eval-expr, trampoline, call-lambda, expand-macro -;; env-has?, env-get, callable?, lambda?, component?, macro? -;; primitive?, get-primitive, component-name +;; env-has?, env-get, env-set!, env-merge, callable?, lambda?, component?, +;; macro?, island?, primitive?, get-primitive, component-name +;; lambda-closure, lambda-params, lambda-body +;; +;; From render.sx: +;; HTML_TAGS, eval-cond, process-bindings +;; +;; From parser.sx: +;; serialize (= sx-serialize) +;; +;; From signals.sx (optional): +;; invoke ;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/boot.sx b/shared/sx/ref/boot.sx index f4cf787..a27980a 100644 --- a/shared/sx/ref/boot.sx +++ b/shared/sx/ref/boot.sx @@ -361,8 +361,11 @@ (fn (disposable) (append! disposers disposable)) (fn () (render-to-dom (component-body comp) local nil))))) - ;; Morph existing DOM against reactive output - (morph-children el body-dom) + ;; Clear existing content and append reactive DOM directly. + ;; Unlike morph-children, this preserves addEventListener-based + ;; event handlers on the freshly rendered nodes. + (dom-set-text-content el "") + (dom-append el body-dom) ;; Store disposers for cleanup (dom-set-data el "sx-disposers" disposers) diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py index 65d1a24..eddeacf 100644 --- a/shared/sx/ref/bootstrap_js.py +++ b/shared/sx/ref/bootstrap_js.py @@ -209,6 +209,10 @@ class JSEmitter: "aser-fragment": "aserFragment", "aser-call": "aserCall", "aser-special": "aserSpecial", + "eval-case-aser": "evalCaseAser", + "sx-serialize": "sxSerialize", + "sx-serialize-dict": "sxSerializeDict", + "sx-expr-source": "sxExprSource", "sf-if": "sfIf", "sf-when": "sfWhen", "sf-cond": "sfCond", @@ -1384,9 +1388,10 @@ ASYNC_IO_JS = ''' return null; } - // Component + // 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)); @@ -2206,6 +2211,8 @@ PRIMITIVES_JS_MODULES: dict[str, str] = { 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["concat"] = function() { var out = []; for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]); @@ -2422,6 +2429,17 @@ PLATFORM_JS_PRE = ''' function trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); } function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; } + // invoke — call any callable (native fn or SX lambda) with args. + // Transpiled code emits direct calls f(args) which fail on SX lambdas + // from runtime-evaluated island bodies. invoke dispatches correctly. + function invoke() { + var f = arguments[0]; + var args = Array.prototype.slice.call(arguments, 1); + if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f))); + if (typeof f === 'function') return f.apply(null, args); + return NIL; + } + // JSON / dict helpers for island state serialization function jsonSerialize(obj) { try { return JSON.stringify(obj); } catch(e) { return "{}"; } @@ -2447,17 +2465,8 @@ PLATFORM_JS_PRE = ''' // Render-expression detection — lets the evaluator delegate to the active adapter. // Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements. - function isRenderExpr(expr) { - if (!Array.isArray(expr) || !expr.length) return false; - var h = expr[0]; - if (!h || !h._sym) return false; - var n = h.name; - return !!(n === "<>" || n === "raw!" || - n.charAt(0) === "~" || n.indexOf("html:") === 0 || - (typeof HTML_TAGS !== "undefined" && HTML_TAGS.indexOf(n) >= 0) || - (typeof SVG_TAGS !== "undefined" && SVG_TAGS.indexOf(n) >= 0) || - (n.indexOf("-") > 0 && expr.length > 1 && expr[1] && expr[1]._kw)); - } + // 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). @@ -2522,7 +2531,10 @@ PLATFORM_JS_POST = ''' 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) { return f.apply(null, args); }; + 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"]; @@ -2541,28 +2553,12 @@ PLATFORM_JS_POST = ''' 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); } - // Serializer - function serialize(val) { - if (isNil(val)) return "nil"; - if (typeof val === "boolean") return val ? "true" : "false"; - if (typeof val === "number") return String(val); - if (typeof val === "string") return \'"\' + val.replace(/\\\\/g, "\\\\\\\\").replace(/"/g, \'\\\\"\') + \'"\'; - if (isSym(val)) return val.name; - if (isKw(val)) return ":" + val.name; - if (Array.isArray(val)) return "(" + val.map(serialize).join(" ") + ")"; - return String(val); - } - - function isSpecialForm(n) { return n in { - "if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1, - "lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1, - "defhandler":1,"begin":1,"do":1, - "quote":1,"quasiquote":1,"->":1,"set!":1 - }; } - function isHoForm(n) { return n in { - "map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1 - }; } + // 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 @@ -2888,8 +2884,12 @@ PLATFORM_DOM_JS = """ function domListen(el, name, handler) { if (!_hasDom || !el) return function() {}; - el.addEventListener(name, handler); - return function() { el.removeEventListener(name, handler); }; + // Wrap SX lambdas from runtime-evaluated island code into native fns + var wrapped = isLambda(handler) + ? function(e) { invoke(handler, e); } + : handler; + el.addEventListener(name, wrapped); + return function() { el.removeEventListener(name, wrapped); }; } function eventDetail(e) { @@ -2930,79 +2930,8 @@ PLATFORM_DOM_JS = """ try { return JSON.parse(s); } catch(e) { return {}; } } - // ========================================================================= - // Performance overrides — replace transpiled spec with imperative JS - // ========================================================================= - - // Override renderDomComponent: imperative kwarg parsing, no reduce/assoc - renderDomComponent = function(comp, args, env, ns) { - // Parse keyword args imperatively - var kwargs = {}; - var children = []; - for (var i = 0; i < args.length; i++) { - var arg = args[i]; - if (arg && arg._kw && (i + 1) < args.length) { - kwargs[arg.name] = trampoline(evalExpr(args[i + 1], env)); - i++; // skip value - } else { - children.push(arg); - } - } - // Build local env via prototype chain - var local = Object.create(componentClosure(comp)); - // Copy caller env own properties - for (var k in env) if (env.hasOwnProperty(k)) local[k] = env[k]; - // Bind params - var params = componentParams(comp); - for (var j = 0; j < params.length; j++) { - var p = params[j]; - local[p] = p in kwargs ? kwargs[p] : NIL; - } - // Bind children - if (componentHasChildren(comp)) { - var childFrag = document.createDocumentFragment(); - for (var c = 0; c < children.length; c++) { - var rendered = renderToDom(children[c], env, ns); - if (rendered) childFrag.appendChild(rendered); - } - local["children"] = childFrag; - } - return renderToDom(componentBody(comp), local, ns); - }; - - // Override renderDomElement: imperative attr parsing, no reduce/assoc - renderDomElement = function(tag, args, env, ns) { - var newNs = tag === "svg" ? SVG_NS : tag === "math" ? MATH_NS : ns; - var el = domCreateElement(tag, newNs); - var extraClasses = []; - 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 = trampoline(evalExpr(args[i + 1], env)); - i++; // skip value - if (isNil(attrVal) || attrVal === false) continue; - 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 = renderToDom(arg, env, newNs); - if (child) el.appendChild(child); - } - } - } - if (extraClasses.length) { - var existing = el.getAttribute("class") || ""; - el.setAttribute("class", (existing ? existing + " " : "") + extraClasses.join(" ")); - } - return el; - }; + // renderDomComponent and renderDomElement are transpiled from + // adapter-dom.sx — no imperative overrides needed. """ PLATFORM_ENGINE_PURE_JS = """ @@ -3124,8 +3053,14 @@ PLATFORM_ORCHESTRATION_JS = """ // --- Timers --- - function setTimeout_(fn, ms) { return setTimeout(fn, ms || 0); } - function setInterval_(fn, ms) { return setInterval(fn, ms || 1000); } + 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) { @@ -3651,6 +3586,8 @@ PLATFORM_ORCHESTRATION_JS = """ logInfo("sx:route server " + pathname); executeRequest(link, { method: "GET", url: liveHref }).then(function() { try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} + }).catch(function(err) { + logWarn("sx:route server fetch error: " + (err && err.message ? err.message : err)); }); } }); @@ -4050,7 +3987,7 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False): lines.append(''' // Expose signal functions as primitives so runtime-evaluated SX code // (e.g. island bodies from .sx files) can call them - PRIMITIVES["signal"] = createSignal; + PRIMITIVES["signal"] = signal; PRIMITIVES["signal?"] = isSignal; PRIMITIVES["deref"] = deref; PRIMITIVES["reset!"] = reset_b; @@ -4058,7 +3995,9 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False): PRIMITIVES["computed"] = computed; PRIMITIVES["effect"] = effect; PRIMITIVES["batch"] = batch; - PRIMITIVES["dispose"] = dispose; + // 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; diff --git a/shared/sx/ref/bootstrap_py.py b/shared/sx/ref/bootstrap_py.py index 26c966d..ff9c43b 100644 --- a/shared/sx/ref/bootstrap_py.py +++ b/shared/sx/ref/bootstrap_py.py @@ -276,6 +276,10 @@ class PyEmitter: # adapter-sx.sx "render-to-sx": "render_to_sx", "aser": "aser", + "eval-case-aser": "eval_case_aser", + "sx-serialize": "sx_serialize", + "sx-serialize-dict": "sx_serialize_dict", + "sx-expr-source": "sx_expr_source", # Primitives that need exact aliases "contains?": "contains_p", "starts-with?": "starts_with_p", @@ -1540,6 +1544,16 @@ def tracking_context_notify_fn(ctx): return ctx.notify_fn if isinstance(ctx, _TrackingContext) else NIL +def invoke(f, *args): + """Call f with args — handles both native callables and SX lambdas. + + In Python, all transpiled lambdas are natively callable, so this is + just a direct call. The JS host needs dispatch logic here because + SX lambdas from runtime-evaluated code are objects, not functions. + """ + return f(*args) + + def json_serialize(obj): import json try: @@ -1605,17 +1619,8 @@ def dict_delete(d, k): def is_render_expr(expr): - """Check if expression is an HTML element, component, or fragment.""" - if not isinstance(expr, list) or not expr: - return False - h = expr[0] - if not isinstance(h, Symbol): - return False - n = h.name - return (n == "<>" or n == "raw!" or - n.startswith("~") or n.startswith("html:") or - n in HTML_TAGS or - ("-" in n and len(expr) > 1 and isinstance(expr[1], Keyword))) + """Placeholder — overridden by transpiled version from render.sx.""" + return False # Render dispatch -- set by adapter @@ -1657,6 +1662,10 @@ def make_raw_html(s): return _RawHTML(s) +def sx_expr_source(x): + return x.source if isinstance(x, SxExpr) else str(x) + + class EvalError(Exception): pass @@ -1696,7 +1705,12 @@ def escape_string(s): def serialize(val): - """Serialize an SX value to SX source text.""" + """Serialize an SX value to SX source text. + + Note: parser.sx defines sx-serialize with a serialize alias, but parser.sx + is only included in JS builds (for client-side parsing). Python builds + provide this as a platform function. + """ t = type_of(val) if t == "sx-expr": return val.source @@ -1730,179 +1744,26 @@ def serialize(val): return "nil" return str(val) +# Aliases for transpiled code — parser.sx defines sx-serialize/sx-serialize-dict +# but parser.sx is JS-only. Provide aliases so transpiled render.sx works. +sx_serialize = serialize +sx_serialize_dict = lambda d: serialize(d) -_SPECIAL_FORM_NAMES = frozenset([ - "if", "when", "cond", "case", "and", "or", - "let", "let*", "lambda", "fn", - "define", "defcomp", "defmacro", "defstyle", - "defhandler", "defpage", "defquery", "defaction", "defrelation", - "begin", "do", "quote", "quasiquote", - "->", "set!", -]) - -_HO_FORM_NAMES = frozenset([ - "map", "map-indexed", "filter", "reduce", - "some", "every?", "for-each", -]) +_SPECIAL_FORM_NAMES = frozenset() # Placeholder — overridden by transpiled adapter-sx.sx +_HO_FORM_NAMES = frozenset() def is_special_form(name): - return name in _SPECIAL_FORM_NAMES + """Placeholder — overridden by transpiled version from adapter-sx.sx.""" + return False def is_ho_form(name): - return name in _HO_FORM_NAMES + """Placeholder — overridden by transpiled version from adapter-sx.sx.""" + return False def aser_special(name, expr, env): - """Evaluate a special/HO form in aser mode. - - Control flow forms evaluate conditions normally but render branches - through aser (serializing tags/components instead of rendering HTML). - Definition forms evaluate for side effects and return nil. - """ - # Control flow — evaluate conditions, aser branches - args = expr[1:] - if name == "if": - cond_val = trampoline(eval_expr(args[0], env)) - if sx_truthy(cond_val): - return aser(args[1], env) - return aser(args[2], env) if _b_len(args) > 2 else NIL - if name == "when": - cond_val = trampoline(eval_expr(args[0], env)) - if sx_truthy(cond_val): - result = NIL - for body in args[1:]: - result = aser(body, env) - return result - return NIL - if name == "cond": - clauses = args - if clauses and isinstance(clauses[0], _b_list) and _b_len(clauses[0]) == 2: - for clause in clauses: - test = clause[0] - if isinstance(test, Symbol) and test.name in ("else", ":else"): - return aser(clause[1], env) - if isinstance(test, Keyword) and test.name == "else": - return aser(clause[1], env) - if sx_truthy(trampoline(eval_expr(test, env))): - return aser(clause[1], env) - else: - i = 0 - while i < _b_len(clauses) - 1: - test = clauses[i] - result = clauses[i + 1] - if isinstance(test, Keyword) and test.name == "else": - return aser(result, env) - if isinstance(test, Symbol) and test.name in (":else", "else"): - return aser(result, env) - if sx_truthy(trampoline(eval_expr(test, env))): - return aser(result, env) - i += 2 - return NIL - if name == "case": - match_val = trampoline(eval_expr(args[0], env)) - clauses = args[1:] - i = 0 - while i < _b_len(clauses) - 1: - test = clauses[i] - result = clauses[i + 1] - if isinstance(test, Keyword) and test.name == "else": - return aser(result, env) - if isinstance(test, Symbol) and test.name in (":else", "else"): - return aser(result, env) - if match_val == trampoline(eval_expr(test, env)): - return aser(result, env) - i += 2 - return NIL - if name in ("let", "let*"): - bindings = args[0] - local = _b_dict(env) - if isinstance(bindings, _b_list): - if bindings and isinstance(bindings[0], _b_list): - for b in bindings: - var = b[0] - vname = var.name if isinstance(var, Symbol) else var - local[vname] = trampoline(eval_expr(b[1], local)) - else: - for i in _b_range(0, _b_len(bindings), 2): - var = bindings[i] - vname = var.name if isinstance(var, Symbol) else var - local[vname] = trampoline(eval_expr(bindings[i + 1], local)) - result = NIL - for body in args[1:]: - result = aser(body, local) - return result - if name in ("begin", "do"): - result = NIL - for body in args: - result = aser(body, env) - return result - if name == "and": - result = True - for arg in args: - result = trampoline(eval_expr(arg, env)) - if not sx_truthy(result): - return result - return result - if name == "or": - result = False - for arg in args: - result = trampoline(eval_expr(arg, env)) - if sx_truthy(result): - return result - return result - # HO forms in aser mode — map/for-each render through aser - if name == "map": - fn = trampoline(eval_expr(args[0], env)) - coll = trampoline(eval_expr(args[1], env)) - results = [] - for item in coll: - if isinstance(fn, Lambda): - local = _b_dict(fn.closure) - local.update(env) - local[fn.params[0]] = item - results.append(aser(fn.body, local)) - elif callable(fn): - results.append(fn(item)) - else: - raise EvalError("map requires callable") - return results - if name == "map-indexed": - fn = trampoline(eval_expr(args[0], env)) - coll = trampoline(eval_expr(args[1], env)) - results = [] - for i, item in enumerate(coll): - if isinstance(fn, Lambda): - local = _b_dict(fn.closure) - local.update(env) - local[fn.params[0]] = i - local[fn.params[1]] = item - results.append(aser(fn.body, local)) - elif callable(fn): - results.append(fn(i, item)) - else: - raise EvalError("map-indexed requires callable") - return results - if name == "for-each": - fn = trampoline(eval_expr(args[0], env)) - coll = trampoline(eval_expr(args[1], env)) - results = [] - for item in coll: - if isinstance(fn, Lambda): - local = _b_dict(fn.closure) - local.update(env) - local[fn.params[0]] = item - results.append(aser(fn.body, local)) - elif callable(fn): - fn(item) - return results if results else NIL - # Definition forms — evaluate for side effects - if name in ("define", "defcomp", "defmacro", "defstyle", - "defhandler", "defpage", "defquery", "defaction", "defrelation"): - trampoline(eval_expr(expr, env)) - return NIL - # Lambda/fn, quote, quasiquote, set!, -> : evaluate normally - result = eval_expr(expr, env) - return trampoline(result) + """Placeholder — overridden by transpiled version from adapter-sx.sx.""" + return trampoline(eval_expr(expr, env)) ''' # --------------------------------------------------------------------------- @@ -2193,6 +2054,7 @@ parse_int = PRIMITIVES["parse-int"] upper = PRIMITIVES["upper"] has_key_p = PRIMITIVES["has-key?"] dissoc = PRIMITIVES["dissoc"] +index_of = PRIMITIVES["index-of"] ''' @@ -2217,7 +2079,7 @@ PLATFORM_DEPS_PY = ( ' return list(c.css_classes) if hasattr(c, "css_classes") and c.css_classes else []\n' '\n' 'def env_components(env):\n' - ' """Return list of component/macro names in an environment."""\n' + ' """Placeholder — overridden by transpiled version from deps.sx."""\n' ' return [k for k, v in env.items()\n' ' if isinstance(v, (Component, Macro))]\n' '\n' @@ -2287,7 +2149,11 @@ CONTINUATIONS_PY = ''' _RESET_RESUME = [] # stack of resume values; empty = not resuming -_SPECIAL_FORM_NAMES = _SPECIAL_FORM_NAMES | frozenset(["reset", "shift"]) +# Extend the transpiled form name lists with continuation forms +if isinstance(SPECIAL_FORM_NAMES, list): + SPECIAL_FORM_NAMES.extend(["reset", "shift"]) +else: + _SPECIAL_FORM_NAMES = _SPECIAL_FORM_NAMES | frozenset(["reset", "shift"]) def sf_reset(args, env): """(reset body) -- establish a continuation delimiter.""" diff --git a/shared/sx/ref/deps.sx b/shared/sx/ref/deps.sx index b0190d8..55d6bdc 100644 --- a/shared/sx/ref/deps.sx +++ b/shared/sx/ref/deps.sx @@ -20,7 +20,6 @@ ;; (component-deps c) → cached deps list (may be empty) ;; (component-set-deps! c d)→ cache deps on component ;; (component-css-classes c)→ pre-scanned CSS class list -;; (env-components env) → list of component/macro names in env ;; (regex-find-all pat src) → list of capture group 1 matches ;; (scan-css-classes src) → list of CSS class strings from source ;; ========================================================================== @@ -423,7 +422,20 @@ ;; (component-set-io-refs! c r)→ cache IO refs on component ;; (component-affinity c) → "auto" | "client" | "server" ;; (macro-body m) → AST body of macro -;; (env-components env) → list of component names in env ;; (regex-find-all pat src) → list of capture group matches ;; (scan-css-classes src) → list of CSS class strings from source ;; -------------------------------------------------------------------------- + + +;; -------------------------------------------------------------------------- +;; env-components — list component/macro names in an environment +;; -------------------------------------------------------------------------- +;; Moved from platform to spec: pure logic using type predicates. + +(define env-components + (fn (env) + (filter + (fn (k) + (let ((v (env-get env k))) + (or (component? v) (macro? v)))) + (keys env)))) diff --git a/shared/sx/ref/parser.sx b/shared/sx/ref/parser.sx index 2149b0c..f0541e9 100644 --- a/shared/sx/ref/parser.sx +++ b/shared/sx/ref/parser.sx @@ -305,6 +305,10 @@ "}"))) +;; Alias: adapters use (serialize val) — canonicalize to sx-serialize +(define serialize sx-serialize) + + ;; -------------------------------------------------------------------------- ;; Platform parser interface ;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/render.sx b/shared/sx/ref/render.sx index ec69479..66384c5 100644 --- a/shared/sx/ref/render.sx +++ b/shared/sx/ref/render.sx @@ -190,6 +190,30 @@ local))) +;; -------------------------------------------------------------------------- +;; is-render-expr? — check if expression is a rendering form +;; -------------------------------------------------------------------------- +;; Used by eval-list to dispatch rendering forms to the active adapter +;; (HTML, SX wire, or DOM) rather than evaluating them as function calls. + +(define is-render-expr? + (fn (expr) + (if (or (not (= (type-of expr) "list")) (empty? expr)) + false + (let ((h (first expr))) + (if (not (= (type-of h) "symbol")) + false + (let ((n (symbol-name h))) + (or (= n "<>") + (= n "raw!") + (starts-with? n "~") + (starts-with? n "html:") + (contains? HTML_TAGS n) + (and (> (index-of n "-") 0) + (> (len expr) 1) + (= (type-of (nth expr 1)) "keyword"))))))))) + + ;; -------------------------------------------------------------------------- ;; Platform interface (shared across adapters) ;; -------------------------------------------------------------------------- @@ -199,11 +223,6 @@ ;; (escape-attr s) → attribute-value-escaped string ;; (raw-html-content r) → unwrap RawHTML marker to string ;; -;; Serialization: -;; (serialize val) → SX source string representation of val -;; -;; Form classification (used by SX wire adapter): -;; (special-form? name) → boolean -;; (ho-form? name) → boolean -;; (aser-special name expr env) → evaluate special/HO form through aser +;; From parser.sx: +;; (sx-serialize val) → SX source string (aliased as serialize above) ;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/signals.sx b/shared/sx/ref/signals.sx index 22ea528..cee16e2 100644 --- a/shared/sx/ref/signals.sx +++ b/shared/sx/ref/signals.sx @@ -25,6 +25,15 @@ ;; (set-tracking-context! c) → void ;; (get-tracking-context) → context or nil ;; +;; Runtime callable dispatch: +;; (invoke f &rest args) → any — call f with args; handles both +;; native host functions AND SX lambdas +;; from runtime-evaluated code (islands). +;; Transpiled code emits direct calls +;; f(args) which fail on SX lambdas. +;; invoke goes through the evaluator's +;; dispatch (call-fn) so either works. +;; ;; ========================================================================== @@ -112,7 +121,7 @@ (let ((ctx (make-tracking-context recompute))) (let ((prev (get-tracking-context))) (set-tracking-context! ctx) - (let ((new-val (compute-fn))) + (let ((new-val (invoke compute-fn))) (set-tracking-context! prev) ;; Save discovered deps (signal-set-deps! s (tracking-context-deps ctx)) @@ -144,7 +153,7 @@ (fn () (when (not disposed) ;; Run previous cleanup if any - (when cleanup-fn (cleanup-fn)) + (when cleanup-fn (invoke cleanup-fn)) ;; Unsubscribe from old deps (for-each @@ -156,7 +165,7 @@ (let ((ctx (make-tracking-context run-effect))) (let ((prev (get-tracking-context))) (set-tracking-context! ctx) - (let ((result (effect-fn))) + (let ((result (invoke effect-fn))) (set-tracking-context! prev) (set! deps (tracking-context-deps ctx)) ;; If effect returns a function, it's the cleanup @@ -169,7 +178,7 @@ ;; Return dispose function (fn () (set! disposed true) - (when cleanup-fn (cleanup-fn)) + (when cleanup-fn (invoke cleanup-fn)) (for-each (fn (dep) (signal-remove-sub! dep run-effect)) deps) @@ -189,7 +198,7 @@ (define batch (fn (thunk) (set! *batch-depth* (+ *batch-depth* 1)) - (thunk) + (invoke thunk) (set! *batch-depth* (- *batch-depth* 1)) (when (= *batch-depth* 0) (let ((queue *batch-queue*)) @@ -308,7 +317,7 @@ (let ((registry *store-registry*)) ;; Only create the store once — subsequent calls return existing (when (not (has-key? registry name)) - (set! *store-registry* (assoc registry name (init-fn)))) + (set! *store-registry* (assoc registry name (invoke init-fn)))) (get *store-registry* name)))) (define use-store @@ -367,7 +376,7 @@ (fn (e) (let ((detail (event-detail e)) (new-val (if transform-fn - (transform-fn detail) + (invoke transform-fn detail) detail))) (reset! target-signal new-val)))))) ;; Return cleanup — removes listener on dispose/re-run diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index 2acb2f9..2c43bb8 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -1,4 +1,5 @@ # WARNING: special-forms.sx declares forms not in eval.sx: reset, shift +# WARNING: eval.sx dispatches forms not in special-forms.sx: form? """ sx_ref.py -- Generated from reference SX evaluator specification. @@ -402,6 +403,16 @@ def tracking_context_notify_fn(ctx): return ctx.notify_fn if isinstance(ctx, _TrackingContext) else NIL +def invoke(f, *args): + """Call f with args — handles both native callables and SX lambdas. + + In Python, all transpiled lambdas are natively callable, so this is + just a direct call. The JS host needs dispatch logic here because + SX lambdas from runtime-evaluated code are objects, not functions. + """ + return f(*args) + + def json_serialize(obj): import json try: @@ -467,17 +478,8 @@ def dict_delete(d, k): def is_render_expr(expr): - """Check if expression is an HTML element, component, or fragment.""" - if not isinstance(expr, list) or not expr: - return False - h = expr[0] - if not isinstance(h, Symbol): - return False - n = h.name - return (n == "<>" or n == "raw!" or - n.startswith("~") or n.startswith("html:") or - n in HTML_TAGS or - ("-" in n and len(expr) > 1 and isinstance(expr[1], Keyword))) + """Placeholder — overridden by transpiled version from render.sx.""" + return False # Render dispatch -- set by adapter @@ -519,6 +521,10 @@ def make_raw_html(s): return _RawHTML(s) +def sx_expr_source(x): + return x.source if isinstance(x, SxExpr) else str(x) + + class EvalError(Exception): pass @@ -558,7 +564,12 @@ def escape_string(s): def serialize(val): - """Serialize an SX value to SX source text.""" + """Serialize an SX value to SX source text. + + Note: parser.sx defines sx-serialize with a serialize alias, but parser.sx + is only included in JS builds (for client-side parsing). Python builds + provide this as a platform function. + """ t = type_of(val) if t == "sx-expr": return val.source @@ -592,179 +603,26 @@ def serialize(val): return "nil" return str(val) +# Aliases for transpiled code — parser.sx defines sx-serialize/sx-serialize-dict +# but parser.sx is JS-only. Provide aliases so transpiled render.sx works. +sx_serialize = serialize +sx_serialize_dict = lambda d: serialize(d) -_SPECIAL_FORM_NAMES = frozenset([ - "if", "when", "cond", "case", "and", "or", - "let", "let*", "lambda", "fn", - "define", "defcomp", "defmacro", "defstyle", - "defhandler", "defpage", "defquery", "defaction", "defrelation", - "begin", "do", "quote", "quasiquote", - "->", "set!", -]) - -_HO_FORM_NAMES = frozenset([ - "map", "map-indexed", "filter", "reduce", - "some", "every?", "for-each", -]) +_SPECIAL_FORM_NAMES = frozenset() # Placeholder — overridden by transpiled adapter-sx.sx +_HO_FORM_NAMES = frozenset() def is_special_form(name): - return name in _SPECIAL_FORM_NAMES + """Placeholder — overridden by transpiled version from adapter-sx.sx.""" + return False def is_ho_form(name): - return name in _HO_FORM_NAMES + """Placeholder — overridden by transpiled version from adapter-sx.sx.""" + return False def aser_special(name, expr, env): - """Evaluate a special/HO form in aser mode. - - Control flow forms evaluate conditions normally but render branches - through aser (serializing tags/components instead of rendering HTML). - Definition forms evaluate for side effects and return nil. - """ - # Control flow — evaluate conditions, aser branches - args = expr[1:] - if name == "if": - cond_val = trampoline(eval_expr(args[0], env)) - if sx_truthy(cond_val): - return aser(args[1], env) - return aser(args[2], env) if _b_len(args) > 2 else NIL - if name == "when": - cond_val = trampoline(eval_expr(args[0], env)) - if sx_truthy(cond_val): - result = NIL - for body in args[1:]: - result = aser(body, env) - return result - return NIL - if name == "cond": - clauses = args - if clauses and isinstance(clauses[0], _b_list) and _b_len(clauses[0]) == 2: - for clause in clauses: - test = clause[0] - if isinstance(test, Symbol) and test.name in ("else", ":else"): - return aser(clause[1], env) - if isinstance(test, Keyword) and test.name == "else": - return aser(clause[1], env) - if sx_truthy(trampoline(eval_expr(test, env))): - return aser(clause[1], env) - else: - i = 0 - while i < _b_len(clauses) - 1: - test = clauses[i] - result = clauses[i + 1] - if isinstance(test, Keyword) and test.name == "else": - return aser(result, env) - if isinstance(test, Symbol) and test.name in (":else", "else"): - return aser(result, env) - if sx_truthy(trampoline(eval_expr(test, env))): - return aser(result, env) - i += 2 - return NIL - if name == "case": - match_val = trampoline(eval_expr(args[0], env)) - clauses = args[1:] - i = 0 - while i < _b_len(clauses) - 1: - test = clauses[i] - result = clauses[i + 1] - if isinstance(test, Keyword) and test.name == "else": - return aser(result, env) - if isinstance(test, Symbol) and test.name in (":else", "else"): - return aser(result, env) - if match_val == trampoline(eval_expr(test, env)): - return aser(result, env) - i += 2 - return NIL - if name in ("let", "let*"): - bindings = args[0] - local = _b_dict(env) - if isinstance(bindings, _b_list): - if bindings and isinstance(bindings[0], _b_list): - for b in bindings: - var = b[0] - vname = var.name if isinstance(var, Symbol) else var - local[vname] = trampoline(eval_expr(b[1], local)) - else: - for i in _b_range(0, _b_len(bindings), 2): - var = bindings[i] - vname = var.name if isinstance(var, Symbol) else var - local[vname] = trampoline(eval_expr(bindings[i + 1], local)) - result = NIL - for body in args[1:]: - result = aser(body, local) - return result - if name in ("begin", "do"): - result = NIL - for body in args: - result = aser(body, env) - return result - if name == "and": - result = True - for arg in args: - result = trampoline(eval_expr(arg, env)) - if not sx_truthy(result): - return result - return result - if name == "or": - result = False - for arg in args: - result = trampoline(eval_expr(arg, env)) - if sx_truthy(result): - return result - return result - # HO forms in aser mode — map/for-each render through aser - if name == "map": - fn = trampoline(eval_expr(args[0], env)) - coll = trampoline(eval_expr(args[1], env)) - results = [] - for item in coll: - if isinstance(fn, Lambda): - local = _b_dict(fn.closure) - local.update(env) - local[fn.params[0]] = item - results.append(aser(fn.body, local)) - elif callable(fn): - results.append(fn(item)) - else: - raise EvalError("map requires callable") - return results - if name == "map-indexed": - fn = trampoline(eval_expr(args[0], env)) - coll = trampoline(eval_expr(args[1], env)) - results = [] - for i, item in enumerate(coll): - if isinstance(fn, Lambda): - local = _b_dict(fn.closure) - local.update(env) - local[fn.params[0]] = i - local[fn.params[1]] = item - results.append(aser(fn.body, local)) - elif callable(fn): - results.append(fn(i, item)) - else: - raise EvalError("map-indexed requires callable") - return results - if name == "for-each": - fn = trampoline(eval_expr(args[0], env)) - coll = trampoline(eval_expr(args[1], env)) - results = [] - for item in coll: - if isinstance(fn, Lambda): - local = _b_dict(fn.closure) - local.update(env) - local[fn.params[0]] = item - results.append(aser(fn.body, local)) - elif callable(fn): - fn(item) - return results if results else NIL - # Definition forms — evaluate for side effects - if name in ("define", "defcomp", "defmacro", "defstyle", - "defhandler", "defpage", "defquery", "defaction", "defrelation"): - trampoline(eval_expr(expr, env)) - return NIL - # Lambda/fn, quote, quasiquote, set!, -> : evaluate normally - result = eval_expr(expr, env) - return trampoline(result) + """Placeholder — overridden by transpiled version from adapter-sx.sx.""" + return trampoline(eval_expr(expr, env)) # ========================================================================= @@ -1021,6 +879,7 @@ parse_int = PRIMITIVES["parse-int"] upper = PRIMITIVES["upper"] has_key_p = PRIMITIVES["has-key?"] dissoc = PRIMITIVES["dissoc"] +index_of = PRIMITIVES["index-of"] # ========================================================================= @@ -1042,7 +901,7 @@ def component_css_classes(c): return list(c.css_classes) if hasattr(c, "css_classes") and c.css_classes else [] def env_components(env): - """Return list of component/macro names in an environment.""" + """Placeholder — overridden by transpiled version from deps.sx.""" return [k for k, v in env.items() if isinstance(v, (Component, Macro))] @@ -1320,6 +1179,9 @@ eval_cond_clojure = lambda clauses, env: (NIL if sx_truthy((len(clauses) < 2)) e # process-bindings process_bindings = lambda bindings, env: (lambda local: _sx_begin(for_each(lambda pair: ((lambda name: _sx_dict_set(local, name, trampoline(eval_expr(nth(pair, 1), local))))((symbol_name(first(pair)) if sx_truthy((type_of(first(pair)) == 'symbol')) else sx_str(first(pair)))) if sx_truthy(((type_of(pair) == 'list') if not sx_truthy((type_of(pair) == 'list')) else (len(pair) >= 2))) else NIL), bindings), local))(merge(env)) +# is-render-expr? +is_render_expr = lambda expr: (False if sx_truthy(((not sx_truthy((type_of(expr) == 'list'))) if sx_truthy((not sx_truthy((type_of(expr) == 'list')))) else empty_p(expr))) else (lambda h: (False if sx_truthy((not sx_truthy((type_of(h) == 'symbol')))) else (lambda n: ((n == '<>') if sx_truthy((n == '<>')) else ((n == 'raw!') if sx_truthy((n == 'raw!')) else (starts_with_p(n, '~') if sx_truthy(starts_with_p(n, '~')) else (starts_with_p(n, 'html:') if sx_truthy(starts_with_p(n, 'html:')) else (contains_p(HTML_TAGS, n) if sx_truthy(contains_p(HTML_TAGS, n)) else ((index_of(n, '-') > 0) if not sx_truthy((index_of(n, '-') > 0)) else ((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword')))))))))(symbol_name(h))))(first(expr))) + # === Transpiled from adapter-html === @@ -1357,6 +1219,51 @@ render_html_island = lambda island, args, env: (lambda kwargs: (lambda children: serialize_island_state = lambda kwargs: (NIL if sx_truthy(is_empty_dict(kwargs)) else json_serialize(kwargs)) +# === Transpiled from adapter-sx === + +# render-to-sx +render_to_sx = lambda expr, env: (lambda result: (result if sx_truthy((type_of(result) == 'string')) else serialize(result)))(aser(expr, env)) + +# aser +aser = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else aser_list(expr, env))), (None, lambda: expr)]) + +# aser-list +aser_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: aser(x, env), expr) if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))) else (lambda name: (aser_fragment(args, env) if sx_truthy((name == '<>')) else (aser_call(name, args, env) if sx_truthy(starts_with_p(name, '~')) else (aser_call(name, args, env) if sx_truthy(contains_p(HTML_TAGS, name)) else (aser_special(name, expr, env) if sx_truthy((is_special_form(name) if sx_truthy(is_special_form(name)) else is_ho_form(name))) else (aser(expand_macro(env_get(env, name), args, env), env) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (lambda f: (lambda evaled_args: (apply(f, evaled_args) 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))) if not sx_truthy((not sx_truthy(is_component(f)))) else (not sx_truthy(is_island(f))))))) else (trampoline(call_lambda(f, evaled_args, env)) if sx_truthy(is_lambda(f)) else (aser_call(sx_str('~', component_name(f)), args, env) if sx_truthy(is_component(f)) else (aser_call(sx_str('~', component_name(f)), args, env) if sx_truthy(is_island(f)) else error(sx_str('Not callable: ', inspect(f))))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))))))))(symbol_name(head))))(rest(expr)))(first(expr)) + +# aser-fragment +aser_fragment = lambda children, env: (lambda parts: ('' if sx_truthy(empty_p(parts)) else sx_str('(<> ', join(' ', map(serialize, parts)), ')')))(filter(lambda x: (not sx_truthy(is_nil(x))), map(lambda c: aser(c, env), children))) + +# aser-call +aser_call = lambda name, args, env: (lambda parts: _sx_begin(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), sx_str('(', join(' ', parts), ')')))([name]) + +# SPECIAL_FORM_NAMES +SPECIAL_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'defrelation', 'begin', 'do', 'quote', 'quasiquote', '->', 'set!', 'letrec', 'dynamic-wind', 'defisland'] + +# HO_FORM_NAMES +HO_FORM_NAMES = ['map', 'map-indexed', 'filter', 'reduce', 'some', 'every?', 'for-each'] + +# special-form? +is_special_form = lambda name: contains_p(SPECIAL_FORM_NAMES, name) + +# ho-form? +is_ho_form = lambda name: contains_p(HO_FORM_NAMES, name) + +# aser-special +def aser_special(name, expr, env): + _cells = {} + args = rest(expr) + return ((aser(nth(args, 1), env) if sx_truthy(trampoline(eval_expr(first(args), env))) else (aser(nth(args, 2), env) if sx_truthy((len(args) > 2)) else NIL)) if sx_truthy((name == 'if')) else ((NIL if sx_truthy((not sx_truthy(trampoline(eval_expr(first(args), env))))) else _sx_begin(_sx_cell_set(_cells, 'result', NIL), _sx_begin(for_each(lambda body: _sx_cell_set(_cells, 'result', aser(body, env)), rest(args)), _cells['result']))) if sx_truthy((name == 'when')) else ((lambda branch: (aser(branch, env) if sx_truthy(branch) else NIL))(eval_cond(args, env)) if sx_truthy((name == 'cond')) else ((lambda match_val: (lambda clauses: eval_case_aser(match_val, clauses, env))(rest(args)))(trampoline(eval_expr(first(args), env))) if sx_truthy((name == 'case')) else ((lambda local: _sx_begin(_sx_cell_set(_cells, 'result', NIL), _sx_begin(for_each(lambda body: _sx_cell_set(_cells, 'result', aser(body, local)), rest(args)), _cells['result'])))(process_bindings(first(args), env)) if sx_truthy(((name == 'let') if sx_truthy((name == 'let')) else (name == 'let*'))) else (_sx_begin(_sx_cell_set(_cells, 'result', NIL), _sx_begin(for_each(lambda body: _sx_cell_set(_cells, 'result', aser(body, env)), args), _cells['result'])) if sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))) else (_sx_begin(_sx_cell_set(_cells, 'result', True), _sx_begin(some(_sx_fn(lambda arg: ( + _sx_cell_set(_cells, 'result', trampoline(eval_expr(arg, env))), + (not sx_truthy(_cells['result'])) +)[-1]), args), _cells['result'])) if sx_truthy((name == 'and')) else (_sx_begin(_sx_cell_set(_cells, 'result', False), _sx_begin(some(_sx_fn(lambda arg: ( + _sx_cell_set(_cells, 'result', trampoline(eval_expr(arg, env))), + _cells['result'] +)[-1]), args), _cells['result'])) if sx_truthy((name == 'or')) else ((lambda f: (lambda coll: map(lambda item: ((lambda local: _sx_begin(_sx_dict_set(local, first(lambda_params(f)), item), aser(lambda_body(f), local)))(env_merge(lambda_closure(f), env)) if sx_truthy(is_lambda(f)) else invoke(f, item)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env))) if sx_truthy((name == 'map')) else ((lambda f: (lambda coll: map_indexed(lambda i, item: ((lambda local: _sx_begin(_sx_dict_set(local, first(lambda_params(f)), i), _sx_dict_set(local, nth(lambda_params(f), 1), item), aser(lambda_body(f), local)))(env_merge(lambda_closure(f), env)) if sx_truthy(is_lambda(f)) else invoke(f, i, item)), coll))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env))) if sx_truthy((name == 'map-indexed')) else ((lambda f: (lambda coll: (lambda results: _sx_begin(for_each(lambda item: ((lambda local: _sx_begin(_sx_dict_set(local, first(lambda_params(f)), item), _sx_append(results, aser(lambda_body(f), local))))(env_merge(lambda_closure(f), env)) if sx_truthy(is_lambda(f)) else invoke(f, item)), coll), (NIL if sx_truthy(empty_p(results)) else results)))([]))(trampoline(eval_expr(nth(args, 1), env))))(trampoline(eval_expr(first(args), env))) if sx_truthy((name == 'for-each')) else (_sx_begin(trampoline(eval_expr(expr, env)), serialize(expr)) if sx_truthy((name == 'defisland')) else (_sx_begin(trampoline(eval_expr(expr, env)), NIL) if 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') if sx_truthy((name == 'defaction')) else (name == 'defrelation')))))))))) else trampoline(eval_expr(expr, env))))))))))))))) + +# eval-case-aser +eval_case_aser = lambda match_val, clauses, env: (NIL if sx_truthy((len(clauses) < 2)) else (lambda test: (lambda body: (aser(body, env) 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'))))) else (aser(body, env) if sx_truthy((match_val == trampoline(eval_expr(test, env)))) else eval_case_aser(match_val, slice(clauses, 2), env))))(nth(clauses, 1)))(first(clauses))) + + # === Transpiled from deps (component dependency analysis) === # scan-refs @@ -1413,6 +1320,9 @@ render_target = lambda name, env, io_names: (lambda key: (lambda val: ('server' # page-render-plan page_render_plan = lambda page_source, env, io_names: (lambda needed: (lambda comp_targets: (lambda server_list: (lambda client_list: (lambda io_deps: _sx_begin(for_each(lambda name: (lambda target: _sx_begin(_sx_dict_set(comp_targets, name, target), (_sx_begin(_sx_append(server_list, name), for_each(lambda io_ref: (_sx_append(io_deps, io_ref) if sx_truthy((not sx_truthy(contains_p(io_deps, io_ref)))) else NIL), transitive_io_refs(name, env, io_names))) if sx_truthy((target == 'server')) else _sx_append(client_list, name))))(render_target(name, env, io_names)), needed), {'components': comp_targets, 'server': server_list, 'client': client_list, 'io-deps': io_deps}))([]))([]))([]))({}))(components_needed(page_source, env)) +# env-components +env_components = lambda env: 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 signals (reactive signal runtime) === @@ -1432,7 +1342,7 @@ swap_b = lambda s, f, *args: ((lambda old: (lambda new_val: (_sx_begin(signal_se computed = lambda compute_fn: (lambda s: (lambda deps: (lambda compute_ctx: (lambda recompute: _sx_begin(recompute(), s))(_sx_fn(lambda : ( for_each(lambda dep: signal_remove_sub(dep, recompute), signal_deps(s)), signal_set_deps(s, []), - (lambda ctx: (lambda prev: _sx_begin(set_tracking_context(ctx), (lambda new_val: _sx_begin(set_tracking_context(prev), signal_set_deps(s, tracking_context_deps(ctx)), (lambda old: _sx_begin(signal_set_value(s, new_val), (notify_subscribers(s) if sx_truthy((not sx_truthy(is_identical(old, new_val)))) else NIL)))(signal_value(s))))(compute_fn())))(get_tracking_context()))(make_tracking_context(recompute)) + (lambda ctx: (lambda prev: _sx_begin(set_tracking_context(ctx), (lambda new_val: _sx_begin(set_tracking_context(prev), signal_set_deps(s, tracking_context_deps(ctx)), (lambda old: _sx_begin(signal_set_value(s, new_val), (notify_subscribers(s) if sx_truthy((not sx_truthy(is_identical(old, new_val)))) else NIL)))(signal_value(s))))(invoke(compute_fn))))(get_tracking_context()))(make_tracking_context(recompute)) )[-1])))(NIL))([]))(make_signal(NIL)) # effect @@ -1441,11 +1351,11 @@ def effect(effect_fn): _cells['deps'] = [] _cells['disposed'] = False _cells['cleanup_fn'] = NIL - run_effect = lambda : (_sx_begin((cleanup_fn() if sx_truthy(_cells['cleanup_fn']) else NIL), for_each(lambda dep: signal_remove_sub(dep, run_effect), _cells['deps']), _sx_cell_set(_cells, 'deps', []), (lambda ctx: (lambda prev: _sx_begin(set_tracking_context(ctx), (lambda result: _sx_begin(set_tracking_context(prev), _sx_cell_set(_cells, 'deps', tracking_context_deps(ctx)), (_sx_cell_set(_cells, 'cleanup_fn', result) if sx_truthy(is_callable(result)) else NIL)))(effect_fn())))(get_tracking_context()))(make_tracking_context(run_effect))) if sx_truthy((not sx_truthy(_cells['disposed']))) else NIL) + run_effect = lambda : (_sx_begin((invoke(_cells['cleanup_fn']) if sx_truthy(_cells['cleanup_fn']) else NIL), for_each(lambda dep: signal_remove_sub(dep, run_effect), _cells['deps']), _sx_cell_set(_cells, 'deps', []), (lambda ctx: (lambda prev: _sx_begin(set_tracking_context(ctx), (lambda result: _sx_begin(set_tracking_context(prev), _sx_cell_set(_cells, 'deps', tracking_context_deps(ctx)), (_sx_cell_set(_cells, 'cleanup_fn', result) if sx_truthy(is_callable(result)) else NIL)))(invoke(effect_fn))))(get_tracking_context()))(make_tracking_context(run_effect))) if sx_truthy((not sx_truthy(_cells['disposed']))) else NIL) run_effect() return _sx_fn(lambda : ( _sx_cell_set(_cells, 'disposed', True), - (cleanup_fn() if sx_truthy(_cells['cleanup_fn']) else NIL), + (invoke(_cells['cleanup_fn']) if sx_truthy(_cells['cleanup_fn']) else NIL), for_each(lambda dep: signal_remove_sub(dep, run_effect), _cells['deps']), _sx_cell_set(_cells, 'deps', []) )[-1]) @@ -1459,7 +1369,7 @@ _batch_queue = [] # batch def batch(thunk): _batch_depth = (_batch_depth + 1) - thunk() + invoke(thunk) _batch_depth = (_batch_depth - 1) return ((lambda queue: _sx_begin(_sx_cell_set(_cells, '_batch_queue', []), (lambda seen: (lambda pending: _sx_begin(for_each(lambda s: for_each(lambda sub: (_sx_begin(_sx_append(seen, sub), _sx_append(pending, sub)) if sx_truthy((not sx_truthy(contains_p(seen, sub)))) else NIL), signal_subscribers(s)), queue), for_each(lambda sub: sub(), pending)))([]))([])))(_batch_queue) if sx_truthy((_batch_depth == 0)) else NIL) @@ -1493,7 +1403,7 @@ _store_registry = {} def def_store(name, init_fn): registry = _store_registry if sx_truthy((not sx_truthy(has_key_p(registry, name)))): - _store_registry = assoc(registry, name, init_fn()) + _store_registry = assoc(registry, name, invoke(init_fn)) return get(_store_registry, name) # use-store @@ -1510,7 +1420,7 @@ emit_event = lambda el, event_name, detail: dom_dispatch(el, event_name, detail) on_event = lambda el, event_name, handler: dom_listen(el, event_name, handler) # bridge-event -bridge_event = lambda el, event_name, target_signal, transform_fn: effect(lambda : (lambda remove: remove)(dom_listen(el, event_name, lambda e: (lambda detail: (lambda new_val: reset_b(target_signal, new_val))((transform_fn(detail) if sx_truthy(transform_fn) else detail)))(event_detail(e))))) +bridge_event = lambda el, event_name, target_signal, transform_fn: effect(lambda : (lambda remove: remove)(dom_listen(el, event_name, lambda e: (lambda detail: (lambda new_val: reset_b(target_signal, new_val))((invoke(transform_fn, detail) if sx_truthy(transform_fn) else detail)))(event_detail(e))))) # ========================================================================= @@ -1549,6 +1459,9 @@ def _wrap_aser_outputs(): # Public API # ========================================================================= +# Wrap aser outputs to return SxExpr +_wrap_aser_outputs() + # Set HTML as default adapter _setup_html_adapter()