Fix reactive islands client-side navigation and hydration

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 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 15:18:45 +00:00
parent 9a0173419a
commit 189a0258d9
14 changed files with 971 additions and 1001 deletions

View File

@@ -14,7 +14,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); 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 isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } 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 trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); }
function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; } 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 // JSON / dict helpers for island state serialization
function jsonSerialize(obj) { function jsonSerialize(obj) {
try { return JSON.stringify(obj); } catch(e) { return "{}"; } try { return JSON.stringify(obj); } catch(e) { return "{}"; }
@@ -216,17 +227,8 @@
// Render-expression detection — lets the evaluator delegate to the active adapter. // Render-expression detection — lets the evaluator delegate to the active adapter.
// Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements. // Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements.
function isRenderExpr(expr) { // Placeholder — overridden by transpiled version from render.sx
if (!Array.isArray(expr) || !expr.length) return false; function isRenderExpr(expr) { 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));
}
// Render dispatch — call the active adapter's render function. // Render dispatch — call the active adapter's render function.
// Set by each adapter when loaded; defaults to identity (no rendering). // 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["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["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["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() { PRIMITIVES["concat"] = function() {
var out = []; var out = [];
for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]); 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"]; 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 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; } 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 // Additional primitive aliases used by adapter/engine transpiled code
var split = PRIMITIVES["split"]; var split = PRIMITIVES["split"];
@@ -484,28 +491,12 @@
function escapeAttr(s) { return escapeHtml(s); } function escapeAttr(s) { return escapeHtml(s); }
function rawHtmlContent(r) { return r.html; } function rawHtmlContent(r) { return r.html; }
function makeRawHtml(s) { return { _raw: true, html: s }; } function makeRawHtml(s) { return { _raw: true, html: s }; }
function sxExprSource(x) { return x && x.source ? x.source : String(x); }
// Serializer // Placeholders — overridden by transpiled spec from parser.sx / adapter-sx.sx
function serialize(val) { function serialize(val) { return String(val); }
if (isNil(val)) return "nil"; function isSpecialForm(n) { return false; }
if (typeof val === "boolean") return val ? "true" : "false"; function isHoForm(n) { return 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
}; }
// processBindings and evalCond — now specced in render.sx, bootstrapped above // 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; 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 === // === Transpiled from parser ===
@@ -1188,6 +1188,269 @@ continue; } else { return NIL; } } };
// sx-serialize-dict // 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("}")); }; 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("</") + String(tag) + 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("<div data-sx-island=\"") + String(escapeAttr(islandName)) + String("\"") + String((isSxTruthy(stateJson) ? (String(" data-sx-state=\"") + String(escapeAttr(stateJson)) + String("\"")) : "")) + String(">") + String(bodyHtml) + String("</div>"));
})();
})();
})(); };
// 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 === // === 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() { 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 attrName = keywordName(arg);
var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env)); 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() { (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)))))));
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)))))));
return assoc(state, "skip", true, "i", (get(state, "i") + 1)); 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))))); })() : ((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); })(); }, {["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); } } { 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() { return (function() {
var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); }); 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); domSetData(el, "sx-disposers", disposers);
processElements(el); processElements(el);
return logInfo((String("hydrated island: ") + String(compName) + String(" (") + String(len(disposers)) + String(" disposers)"))); return logInfo((String("hydrated island: ") + String(compName) + String(" (") + String(len(disposers)) + String(" disposers)")));
@@ -2577,7 +2838,7 @@ return (function() {
var prev = getTrackingContext(); var prev = getTrackingContext();
setTrackingContext(ctx); setTrackingContext(ctx);
return (function() { return (function() {
var newVal = computeFn(); var newVal = invoke(computeFn);
setTrackingContext(prev); setTrackingContext(prev);
signalSetDeps(s, trackingContextDeps(ctx)); signalSetDeps(s, trackingContextDeps(ctx));
return (function() { return (function() {
@@ -2599,13 +2860,13 @@ return (function() {
var disposed = false; var disposed = false;
var cleanupFn = NIL; var cleanupFn = NIL;
return (function() { 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); var ctx = makeTrackingContext(runEffect);
return (function() { return (function() {
var prev = getTrackingContext(); var prev = getTrackingContext();
setTrackingContext(ctx); setTrackingContext(ctx);
return (function() { return (function() {
var result = effectFn(); var result = invoke(effectFn);
setTrackingContext(prev); setTrackingContext(prev);
deps = trackingContextDeps(ctx); deps = trackingContextDeps(ctx);
return (isSxTruthy(isCallable(result)) ? (cleanupFn = result) : NIL); return (isSxTruthy(isCallable(result)) ? (cleanupFn = result) : NIL);
@@ -2615,7 +2876,7 @@ return (function() {
runEffect(); runEffect();
return function() { disposed = true; return function() { disposed = true;
if (isSxTruthy(cleanupFn)) { if (isSxTruthy(cleanupFn)) {
cleanupFn(); invoke(cleanupFn);
} }
{ var _c = deps; for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, runEffect); } } { var _c = deps; for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, runEffect); } }
return (deps = []); }; return (deps = []); };
@@ -2630,7 +2891,7 @@ return (deps = []); };
// batch // batch
var batch = function(thunk) { _batchDepth = (_batchDepth + 1); var batch = function(thunk) { _batchDepth = (_batchDepth + 1);
thunk(); invoke(thunk);
_batchDepth = (_batchDepth - 1); _batchDepth = (_batchDepth - 1);
return (isSxTruthy((_batchDepth == 0)) ? (function() { return (isSxTruthy((_batchDepth == 0)) ? (function() {
var queue = _batchQueue; var queue = _batchQueue;
@@ -2679,7 +2940,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
var defStore = function(name, initFn) { return (function() { var defStore = function(name, initFn) { return (function() {
var registry = _storeRegistry; var registry = _storeRegistry;
if (isSxTruthy(!isSxTruthy(hasKey_p(registry, name)))) { if (isSxTruthy(!isSxTruthy(hasKey_p(registry, name)))) {
_storeRegistry = assoc(registry, name, initFn()); _storeRegistry = assoc(registry, name, invoke(initFn));
} }
return get(_storeRegistry, name); 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 bridgeEvent = function(el, eventName, targetSignal, transformFn) { return effect(function() { return (function() {
var remove = domListen(el, eventName, function(e) { return (function() { var remove = domListen(el, eventName, function(e) { return (function() {
var detail = eventDetail(e); 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 reset_b(targetSignal, newVal);
})(); }); })(); });
return remove; return remove;
@@ -2858,8 +3119,12 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function domListen(el, name, handler) { function domListen(el, name, handler) {
if (!_hasDom || !el) return function() {}; if (!_hasDom || !el) return function() {};
el.addEventListener(name, handler); // Wrap SX lambdas from runtime-evaluated island code into native fns
return function() { el.removeEventListener(name, handler); }; var wrapped = isLambda(handler)
? function(e) { invoke(handler, e); }
: handler;
el.addEventListener(name, wrapped);
return function() { el.removeEventListener(name, wrapped); };
} }
function eventDetail(e) { function eventDetail(e) {
@@ -2900,79 +3165,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
try { return JSON.parse(s); } catch(e) { return {}; } try { return JSON.parse(s); } catch(e) { return {}; }
} }
// ========================================================================= // renderDomComponent and renderDomElement are transpiled from
// Performance overrides — replace transpiled spec with imperative JS // adapter-dom.sx — no imperative overrides needed.
// =========================================================================
// 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;
};
// ========================================================================= // =========================================================================
@@ -3092,8 +3286,14 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
// --- Timers --- // --- Timers ---
function setTimeout_(fn, ms) { return setTimeout(fn, ms || 0); } function _wrapSxFn(fn) {
function setInterval_(fn, ms) { return setInterval(fn, ms || 1000); } 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 clearTimeout_(id) { clearTimeout(id); }
function clearInterval_(id) { clearInterval(id); } function clearInterval_(id) { clearInterval(id); }
function requestAnimationFrame_(fn) { function requestAnimationFrame_(fn) {
@@ -3619,6 +3819,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
logInfo("sx:route server " + pathname); logInfo("sx:route server " + pathname);
executeRequest(link, { method: "GET", url: liveHref }).then(function() { executeRequest(link, { method: "GET", url: liveHref }).then(function() {
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} 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 // 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; if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom;
// Expose signal functions as primitives so runtime-evaluated SX code // Expose signal functions as primitives so runtime-evaluated SX code
// (e.g. island bodies from .sx files) can call them // (e.g. island bodies from .sx files) can call them
PRIMITIVES["signal"] = createSignal; PRIMITIVES["signal"] = signal;
PRIMITIVES["signal?"] = isSignal; PRIMITIVES["signal?"] = isSignal;
PRIMITIVES["deref"] = deref; PRIMITIVES["deref"] = deref;
PRIMITIVES["reset!"] = reset_b; PRIMITIVES["reset!"] = reset_b;
@@ -4016,7 +4221,9 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
PRIMITIVES["computed"] = computed; PRIMITIVES["computed"] = computed;
PRIMITIVES["effect"] = effect; PRIMITIVES["effect"] = effect;
PRIMITIVES["batch"] = batch; 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 // Reactive DOM helpers for island code
PRIMITIVES["reactive-text"] = reactiveText; PRIMITIVES["reactive-text"] = reactiveText;
PRIMITIVES["create-text-node"] = createTextNode; PRIMITIVES["create-text-node"] = createTextNode;
@@ -4200,9 +4407,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
return null; return null;
} }
// Component // Component or Island
if (hname.charAt(0) === "~") { if (hname.charAt(0) === "~") {
var comp = env[hname]; 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._component) return asyncRenderComponent(comp, expr.slice(1), env, ns);
if (comp && comp._macro) { if (comp && comp._macro) {
var expanded = trampoline(expandMacro(comp, expr.slice(1), env)); var expanded = trampoline(expandMacro(comp, expr.slice(1), env));
@@ -4691,12 +4899,25 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
} }
function render(source) { 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 exprs = parse(source);
var frag = document.createDocumentFragment(); var frag = document.createDocumentFragment();
for (var i = 0; i < exprs.length; i++) frag.appendChild(renderToDom(exprs[i], merge(componentEnv), null)); for (var i = 0; i < exprs.length; i++) frag.appendChild(renderToDom(exprs[i], merge(componentEnv), null));
return frag; 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 = { var Sx = {
VERSION: "ref-2.0", VERSION: "ref-2.0",
parse: parse, parse: parse,
@@ -4704,7 +4925,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
eval: function(expr, env) { return trampoline(evalExpr(expr, env || merge(componentEnv))); }, eval: function(expr, env) { return trampoline(evalExpr(expr, env || merge(componentEnv))); },
loadComponents: loadComponents, loadComponents: loadComponents,
render: render, render: render,
renderToString: renderToString,
serialize: serialize, serialize: serialize,
NIL: NIL, NIL: NIL,
Symbol: Symbol, Symbol: Symbol,
@@ -4712,6 +4933,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
isTruthy: isSxTruthy, isTruthy: isSxTruthy,
isNil: isNil, isNil: isNil,
componentEnv: componentEnv, 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, renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,
parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null, parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,
parseTime: typeof parseTime === "function" ? parseTime : null, parseTime: typeof parseTime === "function" ? parseTime : null,
@@ -4759,7 +4982,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
emitEvent: emitEvent, emitEvent: emitEvent,
onEvent: onEvent, onEvent: onEvent,
bridgeEvent: bridgeEvent, 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)"
}; };

View File

@@ -14,7 +14,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); 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 isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } 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 trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); }
function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; } 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 // JSON / dict helpers for island state serialization
function jsonSerialize(obj) { function jsonSerialize(obj) {
try { return JSON.stringify(obj); } catch(e) { return "{}"; } try { return JSON.stringify(obj); } catch(e) { return "{}"; }
@@ -216,17 +227,8 @@
// Render-expression detection — lets the evaluator delegate to the active adapter. // Render-expression detection — lets the evaluator delegate to the active adapter.
// Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements. // Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements.
function isRenderExpr(expr) { // Placeholder — overridden by transpiled version from render.sx
if (!Array.isArray(expr) || !expr.length) return false; function isRenderExpr(expr) { 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));
}
// Render dispatch — call the active adapter's render function. // Render dispatch — call the active adapter's render function.
// Set by each adapter when loaded; defaults to identity (no rendering). // 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["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["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["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() { PRIMITIVES["concat"] = function() {
var out = []; var out = [];
for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]); 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"]; 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 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; } 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 // Additional primitive aliases used by adapter/engine transpiled code
var split = PRIMITIVES["split"]; var split = PRIMITIVES["split"];
@@ -484,28 +491,12 @@
function escapeAttr(s) { return escapeHtml(s); } function escapeAttr(s) { return escapeHtml(s); }
function rawHtmlContent(r) { return r.html; } function rawHtmlContent(r) { return r.html; }
function makeRawHtml(s) { return { _raw: true, html: s }; } function makeRawHtml(s) { return { _raw: true, html: s }; }
function sxExprSource(x) { return x && x.source ? x.source : String(x); }
// Serializer // Placeholders — overridden by transpiled spec from parser.sx / adapter-sx.sx
function serialize(val) { function serialize(val) { return String(val); }
if (isNil(val)) return "nil"; function isSpecialForm(n) { return false; }
if (typeof val === "boolean") return val ? "true" : "false"; function isHoForm(n) { return 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
}; }
// processBindings and evalCond — now specced in render.sx, bootstrapped above // processBindings and evalCond — now specced in render.sx, bootstrapped above
@@ -572,92 +563,6 @@
return makeThunk(componentBody(comp), local); 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 // Platform interface — Parser
// ========================================================================= // =========================================================================
@@ -1163,6 +1068,15 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
return local; 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 === // === Transpiled from parser ===
@@ -1274,6 +1188,9 @@ continue; } else { return NIL; } } };
// sx-serialize-dict // 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("}")); }; 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 === // === Transpiled from adapter-html ===
@@ -1452,6 +1369,88 @@ continue; } else { return NIL; } } };
return (String("(") + String(join(" ", parts)) + String(")")); 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 === // === 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() { 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 attrName = keywordName(arg);
var attrVal = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env)); 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() { (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)))))));
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)))))));
return assoc(state, "skip", true, "i", (get(state, "i") + 1)); 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))))); })() : ((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); })(); }, {["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); } } { 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() { return (function() {
var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); }); 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); domSetData(el, "sx-disposers", disposers);
processElements(el); processElements(el);
return logInfo((String("hydrated island: ") + String(compName) + String(" (") + String(len(disposers)) + String(" disposers)"))); 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)); }; 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) === // === Transpiled from router (client-side route matching) ===
// split-path-segments // split-path-segments
@@ -2981,7 +2838,7 @@ return (function() {
var prev = getTrackingContext(); var prev = getTrackingContext();
setTrackingContext(ctx); setTrackingContext(ctx);
return (function() { return (function() {
var newVal = computeFn(); var newVal = invoke(computeFn);
setTrackingContext(prev); setTrackingContext(prev);
signalSetDeps(s, trackingContextDeps(ctx)); signalSetDeps(s, trackingContextDeps(ctx));
return (function() { return (function() {
@@ -3003,13 +2860,13 @@ return (function() {
var disposed = false; var disposed = false;
var cleanupFn = NIL; var cleanupFn = NIL;
return (function() { 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); var ctx = makeTrackingContext(runEffect);
return (function() { return (function() {
var prev = getTrackingContext(); var prev = getTrackingContext();
setTrackingContext(ctx); setTrackingContext(ctx);
return (function() { return (function() {
var result = effectFn(); var result = invoke(effectFn);
setTrackingContext(prev); setTrackingContext(prev);
deps = trackingContextDeps(ctx); deps = trackingContextDeps(ctx);
return (isSxTruthy(isCallable(result)) ? (cleanupFn = result) : NIL); return (isSxTruthy(isCallable(result)) ? (cleanupFn = result) : NIL);
@@ -3019,7 +2876,7 @@ return (function() {
runEffect(); runEffect();
return function() { disposed = true; return function() { disposed = true;
if (isSxTruthy(cleanupFn)) { if (isSxTruthy(cleanupFn)) {
cleanupFn(); invoke(cleanupFn);
} }
{ var _c = deps; for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, runEffect); } } { var _c = deps; for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; signalRemoveSub(dep, runEffect); } }
return (deps = []); }; return (deps = []); };
@@ -3034,7 +2891,7 @@ return (deps = []); };
// batch // batch
var batch = function(thunk) { _batchDepth = (_batchDepth + 1); var batch = function(thunk) { _batchDepth = (_batchDepth + 1);
thunk(); invoke(thunk);
_batchDepth = (_batchDepth - 1); _batchDepth = (_batchDepth - 1);
return (isSxTruthy((_batchDepth == 0)) ? (function() { return (isSxTruthy((_batchDepth == 0)) ? (function() {
var queue = _batchQueue; var queue = _batchQueue;
@@ -3083,7 +2940,7 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
var defStore = function(name, initFn) { return (function() { var defStore = function(name, initFn) { return (function() {
var registry = _storeRegistry; var registry = _storeRegistry;
if (isSxTruthy(!isSxTruthy(hasKey_p(registry, name)))) { if (isSxTruthy(!isSxTruthy(hasKey_p(registry, name)))) {
_storeRegistry = assoc(registry, name, initFn()); _storeRegistry = assoc(registry, name, invoke(initFn));
} }
return get(_storeRegistry, name); 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 bridgeEvent = function(el, eventName, targetSignal, transformFn) { return effect(function() { return (function() {
var remove = domListen(el, eventName, function(e) { return (function() { var remove = domListen(el, eventName, function(e) { return (function() {
var detail = eventDetail(e); 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 reset_b(targetSignal, newVal);
})(); }); })(); });
return remove; return remove;
@@ -3262,8 +3119,12 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function domListen(el, name, handler) { function domListen(el, name, handler) {
if (!_hasDom || !el) return function() {}; if (!_hasDom || !el) return function() {};
el.addEventListener(name, handler); // Wrap SX lambdas from runtime-evaluated island code into native fns
return function() { el.removeEventListener(name, handler); }; var wrapped = isLambda(handler)
? function(e) { invoke(handler, e); }
: handler;
el.addEventListener(name, wrapped);
return function() { el.removeEventListener(name, wrapped); };
} }
function eventDetail(e) { function eventDetail(e) {
@@ -3304,79 +3165,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
try { return JSON.parse(s); } catch(e) { return {}; } try { return JSON.parse(s); } catch(e) { return {}; }
} }
// ========================================================================= // renderDomComponent and renderDomElement are transpiled from
// Performance overrides — replace transpiled spec with imperative JS // adapter-dom.sx — no imperative overrides needed.
// =========================================================================
// 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;
};
// ========================================================================= // =========================================================================
@@ -3496,8 +3286,14 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
// --- Timers --- // --- Timers ---
function setTimeout_(fn, ms) { return setTimeout(fn, ms || 0); } function _wrapSxFn(fn) {
function setInterval_(fn, ms) { return setInterval(fn, ms || 1000); } 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 clearTimeout_(id) { clearTimeout(id); }
function clearInterval_(id) { clearInterval(id); } function clearInterval_(id) { clearInterval(id); }
function requestAnimationFrame_(fn) { function requestAnimationFrame_(fn) {
@@ -4023,6 +3819,8 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
logInfo("sx:route server " + pathname); logInfo("sx:route server " + pathname);
executeRequest(link, { method: "GET", url: liveHref }).then(function() { executeRequest(link, { method: "GET", url: liveHref }).then(function() {
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} 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 // Expose signal functions as primitives so runtime-evaluated SX code
// (e.g. island bodies from .sx files) can call them // (e.g. island bodies from .sx files) can call them
PRIMITIVES["signal"] = createSignal; PRIMITIVES["signal"] = signal;
PRIMITIVES["signal?"] = isSignal; PRIMITIVES["signal?"] = isSignal;
PRIMITIVES["deref"] = deref; PRIMITIVES["deref"] = deref;
PRIMITIVES["reset!"] = reset_b; PRIMITIVES["reset!"] = reset_b;
@@ -4423,7 +4221,9 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
PRIMITIVES["computed"] = computed; PRIMITIVES["computed"] = computed;
PRIMITIVES["effect"] = effect; PRIMITIVES["effect"] = effect;
PRIMITIVES["batch"] = batch; 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 // Reactive DOM helpers for island code
PRIMITIVES["reactive-text"] = reactiveText; PRIMITIVES["reactive-text"] = reactiveText;
PRIMITIVES["create-text-node"] = createTextNode; PRIMITIVES["create-text-node"] = createTextNode;
@@ -4607,9 +4407,10 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
return null; return null;
} }
// Component // Component or Island
if (hname.charAt(0) === "~") { if (hname.charAt(0) === "~") {
var comp = env[hname]; 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._component) return asyncRenderComponent(comp, expr.slice(1), env, ns);
if (comp && comp._macro) { if (comp && comp._macro) {
var expanded = trampoline(expandMacro(comp, expr.slice(1), env)); var expanded = trampoline(expandMacro(comp, expr.slice(1), env));
@@ -5158,17 +4959,6 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null, hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null,
disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null, disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null,
init: typeof bootInit === "function" ? bootInit : 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, splitPathSegments: splitPathSegments,
parseRoutePattern: parseRoutePattern, parseRoutePattern: parseRoutePattern,
matchRoute: matchRoute, matchRoute: matchRoute,

View File

@@ -45,7 +45,7 @@ import contextvars
import inspect import inspect
from typing import Any 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 # When True, _aser expands known components server-side instead of serializing
# them for client rendering. Set during page slot evaluation so Python-only # 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): if isinstance(result, str):
return SxExpr(result) return SxExpr(result)
return SxExpr(serialize(result)) return SxExpr(serialize(result))
elif isinstance(comp, Island):
pass # Islands serialize as SX for client-side rendering
else: else:
import logging import logging
logging.getLogger("sx.eval").error( logging.getLogger("sx.eval").error(
@@ -1596,6 +1598,14 @@ async def _assf_define(expr, env, ctx):
return NIL 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): async def _assf_lambda(expr, env, ctx):
return await _asf_lambda(expr, env, ctx) return await _asf_lambda(expr, env, ctx)
@@ -1703,7 +1713,7 @@ _ASER_FORMS: dict[str, Any] = {
"defcomp": _assf_define, "defcomp": _assf_define,
"defmacro": _assf_define, "defmacro": _assf_define,
"defhandler": _assf_define, "defhandler": _assf_define,
"defisland": _assf_define, "defisland": _assf_defisland,
"begin": _assf_begin, "begin": _assf_begin,
"do": _assf_begin, "do": _assf_begin,
"quote": _assf_quote, "quote": _assf_quote,

View File

@@ -473,7 +473,7 @@ def components_for_request(source: str = "",
from quart import request from quart import request
from .jinja_bridge import _COMPONENT_ENV from .jinja_bridge import _COMPONENT_ENV
from .deps import components_needed from .deps import components_needed
from .types import Component, Macro from .types import Component, Island, Macro
from .parser import serialize from .parser import serialize
# Determine which components the page needs # Determine which components the page needs
@@ -493,7 +493,19 @@ def components_for_request(source: str = "",
parts = [] parts = []
for key, val in _COMPONENT_ENV.items(): 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}" comp_name = f"~{val.name}"
# Skip if not needed for this page # Skip if not needed for this page
if needed is not None and comp_name not in needed and key not in needed: if needed is not None and comp_name not in needed and key not in needed:

View File

@@ -181,8 +181,7 @@
;; Value must be callable (lambda/function) ;; Value must be callable (lambda/function)
(and (starts-with? attr-name "on-") (and (starts-with? attr-name "on-")
(callable? attr-val)) (callable? attr-val))
(let ((event-name (substring attr-name 3 (string-length attr-name)))) (dom-listen el (slice attr-name 3) attr-val)
(dom-listen el event-name attr-val))
;; Boolean attr ;; Boolean attr
(contains? BOOLEAN_ATTRS attr-name) (contains? BOOLEAN_ATTRS attr-name)
(when attr-val (dom-set-attr el attr-name "")) (when attr-val (dom-set-attr el attr-name ""))

View File

@@ -130,20 +130,191 @@
(str "(" (join " " parts) ")")))) (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 ;; 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: ;; From eval.sx:
;; eval-expr, trampoline, call-lambda, expand-macro ;; eval-expr, trampoline, call-lambda, expand-macro
;; env-has?, env-get, callable?, lambda?, component?, macro? ;; env-has?, env-get, env-set!, env-merge, callable?, lambda?, component?,
;; primitive?, get-primitive, component-name ;; 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
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------

View File

@@ -361,8 +361,11 @@
(fn (disposable) (append! disposers disposable)) (fn (disposable) (append! disposers disposable))
(fn () (render-to-dom (component-body comp) local nil))))) (fn () (render-to-dom (component-body comp) local nil)))))
;; Morph existing DOM against reactive output ;; Clear existing content and append reactive DOM directly.
(morph-children el body-dom) ;; 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 ;; Store disposers for cleanup
(dom-set-data el "sx-disposers" disposers) (dom-set-data el "sx-disposers" disposers)

View File

@@ -209,6 +209,10 @@ class JSEmitter:
"aser-fragment": "aserFragment", "aser-fragment": "aserFragment",
"aser-call": "aserCall", "aser-call": "aserCall",
"aser-special": "aserSpecial", "aser-special": "aserSpecial",
"eval-case-aser": "evalCaseAser",
"sx-serialize": "sxSerialize",
"sx-serialize-dict": "sxSerializeDict",
"sx-expr-source": "sxExprSource",
"sf-if": "sfIf", "sf-if": "sfIf",
"sf-when": "sfWhen", "sf-when": "sfWhen",
"sf-cond": "sfCond", "sf-cond": "sfCond",
@@ -1384,9 +1388,10 @@ ASYNC_IO_JS = '''
return null; return null;
} }
// Component // Component or Island
if (hname.charAt(0) === "~") { if (hname.charAt(0) === "~") {
var comp = env[hname]; 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._component) return asyncRenderComponent(comp, expr.slice(1), env, ns);
if (comp && comp._macro) { if (comp && comp._macro) {
var expanded = trampoline(expandMacro(comp, expr.slice(1), env)); 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["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["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["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() { PRIMITIVES["concat"] = function() {
var out = []; var out = [];
for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]); 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 trackingContextAddDep(ctx, s) { if (ctx && ctx.deps.indexOf(s) < 0) ctx.deps.push(s); }
function trackingContextNotifyFn(ctx) { return ctx ? ctx.notifyFn : NIL; } 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 // JSON / dict helpers for island state serialization
function jsonSerialize(obj) { function jsonSerialize(obj) {
try { return JSON.stringify(obj); } catch(e) { return "{}"; } 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. // Render-expression detection — lets the evaluator delegate to the active adapter.
// Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements. // Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements.
function isRenderExpr(expr) { // Placeholder — overridden by transpiled version from render.sx
if (!Array.isArray(expr) || !expr.length) return false; function isRenderExpr(expr) { 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));
}
// Render dispatch — call the active adapter's render function. // Render dispatch — call the active adapter's render function.
// Set by each adapter when loaded; defaults to identity (no rendering). // Set by each adapter when loaded; defaults to identity (no rendering).
@@ -2522,7 +2531,10 @@ PLATFORM_JS_POST = '''
var range = PRIMITIVES["range"]; 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 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; } 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 // Additional primitive aliases used by adapter/engine transpiled code
var split = PRIMITIVES["split"]; var split = PRIMITIVES["split"];
@@ -2541,28 +2553,12 @@ PLATFORM_JS_POST = '''
function escapeAttr(s) { return escapeHtml(s); } function escapeAttr(s) { return escapeHtml(s); }
function rawHtmlContent(r) { return r.html; } function rawHtmlContent(r) { return r.html; }
function makeRawHtml(s) { return { _raw: true, html: s }; } function makeRawHtml(s) { return { _raw: true, html: s }; }
function sxExprSource(x) { return x && x.source ? x.source : String(x); }
// Serializer // Placeholders — overridden by transpiled spec from parser.sx / adapter-sx.sx
function serialize(val) { function serialize(val) { return String(val); }
if (isNil(val)) return "nil"; function isSpecialForm(n) { return false; }
if (typeof val === "boolean") return val ? "true" : "false"; function isHoForm(n) { return 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
}; }
// processBindings and evalCond — now specced in render.sx, bootstrapped above // processBindings and evalCond — now specced in render.sx, bootstrapped above
@@ -2888,8 +2884,12 @@ PLATFORM_DOM_JS = """
function domListen(el, name, handler) { function domListen(el, name, handler) {
if (!_hasDom || !el) return function() {}; if (!_hasDom || !el) return function() {};
el.addEventListener(name, handler); // Wrap SX lambdas from runtime-evaluated island code into native fns
return function() { el.removeEventListener(name, handler); }; var wrapped = isLambda(handler)
? function(e) { invoke(handler, e); }
: handler;
el.addEventListener(name, wrapped);
return function() { el.removeEventListener(name, wrapped); };
} }
function eventDetail(e) { function eventDetail(e) {
@@ -2930,79 +2930,8 @@ PLATFORM_DOM_JS = """
try { return JSON.parse(s); } catch(e) { return {}; } try { return JSON.parse(s); } catch(e) { return {}; }
} }
// ========================================================================= // renderDomComponent and renderDomElement are transpiled from
// Performance overrides — replace transpiled spec with imperative JS // adapter-dom.sx — no imperative overrides needed.
// =========================================================================
// 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;
};
""" """
PLATFORM_ENGINE_PURE_JS = """ PLATFORM_ENGINE_PURE_JS = """
@@ -3124,8 +3053,14 @@ PLATFORM_ORCHESTRATION_JS = """
// --- Timers --- // --- Timers ---
function setTimeout_(fn, ms) { return setTimeout(fn, ms || 0); } function _wrapSxFn(fn) {
function setInterval_(fn, ms) { return setInterval(fn, ms || 1000); } 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 clearTimeout_(id) { clearTimeout(id); }
function clearInterval_(id) { clearInterval(id); } function clearInterval_(id) { clearInterval(id); }
function requestAnimationFrame_(fn) { function requestAnimationFrame_(fn) {
@@ -3651,6 +3586,8 @@ PLATFORM_ORCHESTRATION_JS = """
logInfo("sx:route server " + pathname); logInfo("sx:route server " + pathname);
executeRequest(link, { method: "GET", url: liveHref }).then(function() { executeRequest(link, { method: "GET", url: liveHref }).then(function() {
try { history.pushState({ sxUrl: liveHref, scrollY: window.scrollY }, "", liveHref); } catch (err) {} 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(''' lines.append('''
// Expose signal functions as primitives so runtime-evaluated SX code // Expose signal functions as primitives so runtime-evaluated SX code
// (e.g. island bodies from .sx files) can call them // (e.g. island bodies from .sx files) can call them
PRIMITIVES["signal"] = createSignal; PRIMITIVES["signal"] = signal;
PRIMITIVES["signal?"] = isSignal; PRIMITIVES["signal?"] = isSignal;
PRIMITIVES["deref"] = deref; PRIMITIVES["deref"] = deref;
PRIMITIVES["reset!"] = reset_b; PRIMITIVES["reset!"] = reset_b;
@@ -4058,7 +3995,9 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False):
PRIMITIVES["computed"] = computed; PRIMITIVES["computed"] = computed;
PRIMITIVES["effect"] = effect; PRIMITIVES["effect"] = effect;
PRIMITIVES["batch"] = batch; 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 // Reactive DOM helpers for island code
PRIMITIVES["reactive-text"] = reactiveText; PRIMITIVES["reactive-text"] = reactiveText;
PRIMITIVES["create-text-node"] = createTextNode; PRIMITIVES["create-text-node"] = createTextNode;

View File

@@ -276,6 +276,10 @@ class PyEmitter:
# adapter-sx.sx # adapter-sx.sx
"render-to-sx": "render_to_sx", "render-to-sx": "render_to_sx",
"aser": "aser", "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 # Primitives that need exact aliases
"contains?": "contains_p", "contains?": "contains_p",
"starts-with?": "starts_with_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 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): def json_serialize(obj):
import json import json
try: try:
@@ -1605,17 +1619,8 @@ def dict_delete(d, k):
def is_render_expr(expr): def is_render_expr(expr):
"""Check if expression is an HTML element, component, or fragment.""" """Placeholder — overridden by transpiled version from render.sx."""
if not isinstance(expr, list) or not expr: return False
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)))
# Render dispatch -- set by adapter # Render dispatch -- set by adapter
@@ -1657,6 +1662,10 @@ def make_raw_html(s):
return _RawHTML(s) return _RawHTML(s)
def sx_expr_source(x):
return x.source if isinstance(x, SxExpr) else str(x)
class EvalError(Exception): class EvalError(Exception):
pass pass
@@ -1696,7 +1705,12 @@ def escape_string(s):
def serialize(val): 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) t = type_of(val)
if t == "sx-expr": if t == "sx-expr":
return val.source return val.source
@@ -1730,179 +1744,26 @@ def serialize(val):
return "nil" return "nil"
return str(val) 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([ _SPECIAL_FORM_NAMES = frozenset() # Placeholder — overridden by transpiled adapter-sx.sx
"if", "when", "cond", "case", "and", "or", _HO_FORM_NAMES = frozenset()
"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",
])
def is_special_form(name): 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): 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): def aser_special(name, expr, env):
"""Evaluate a special/HO form in aser mode. """Placeholder — overridden by transpiled version from adapter-sx.sx."""
return trampoline(eval_expr(expr, env))
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)
''' '''
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -2193,6 +2054,7 @@ parse_int = PRIMITIVES["parse-int"]
upper = PRIMITIVES["upper"] upper = PRIMITIVES["upper"]
has_key_p = PRIMITIVES["has-key?"] has_key_p = PRIMITIVES["has-key?"]
dissoc = PRIMITIVES["dissoc"] 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' ' return list(c.css_classes) if hasattr(c, "css_classes") and c.css_classes else []\n'
'\n' '\n'
'def env_components(env):\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' ' return [k for k, v in env.items()\n'
' if isinstance(v, (Component, Macro))]\n' ' if isinstance(v, (Component, Macro))]\n'
'\n' '\n'
@@ -2287,7 +2149,11 @@ CONTINUATIONS_PY = '''
_RESET_RESUME = [] # stack of resume values; empty = not resuming _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): def sf_reset(args, env):
"""(reset body) -- establish a continuation delimiter.""" """(reset body) -- establish a continuation delimiter."""

View File

@@ -20,7 +20,6 @@
;; (component-deps c) → cached deps list (may be empty) ;; (component-deps c) → cached deps list (may be empty)
;; (component-set-deps! c d)→ cache deps on component ;; (component-set-deps! c d)→ cache deps on component
;; (component-css-classes c)→ pre-scanned CSS class list ;; (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 ;; (regex-find-all pat src) → list of capture group 1 matches
;; (scan-css-classes src) → list of CSS class strings from source ;; (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-set-io-refs! c r)→ cache IO refs on component
;; (component-affinity c) → "auto" | "client" | "server" ;; (component-affinity c) → "auto" | "client" | "server"
;; (macro-body m) → AST body of macro ;; (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 ;; (regex-find-all pat src) → list of capture group matches
;; (scan-css-classes src) → list of CSS class strings from source ;; (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))))

View File

@@ -305,6 +305,10 @@
"}"))) "}")))
;; Alias: adapters use (serialize val) — canonicalize to sx-serialize
(define serialize sx-serialize)
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Platform parser interface ;; Platform parser interface
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------

View File

@@ -190,6 +190,30 @@
local))) 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) ;; Platform interface (shared across adapters)
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -199,11 +223,6 @@
;; (escape-attr s) → attribute-value-escaped string ;; (escape-attr s) → attribute-value-escaped string
;; (raw-html-content r) → unwrap RawHTML marker to string ;; (raw-html-content r) → unwrap RawHTML marker to string
;; ;;
;; Serialization: ;; From parser.sx:
;; (serialize val) → SX source string representation of val ;; (sx-serialize val) → SX source string (aliased as serialize above)
;;
;; 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
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------

View File

@@ -25,6 +25,15 @@
;; (set-tracking-context! c) → void ;; (set-tracking-context! c) → void
;; (get-tracking-context) → context or nil ;; (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 ((ctx (make-tracking-context recompute)))
(let ((prev (get-tracking-context))) (let ((prev (get-tracking-context)))
(set-tracking-context! ctx) (set-tracking-context! ctx)
(let ((new-val (compute-fn))) (let ((new-val (invoke compute-fn)))
(set-tracking-context! prev) (set-tracking-context! prev)
;; Save discovered deps ;; Save discovered deps
(signal-set-deps! s (tracking-context-deps ctx)) (signal-set-deps! s (tracking-context-deps ctx))
@@ -144,7 +153,7 @@
(fn () (fn ()
(when (not disposed) (when (not disposed)
;; Run previous cleanup if any ;; Run previous cleanup if any
(when cleanup-fn (cleanup-fn)) (when cleanup-fn (invoke cleanup-fn))
;; Unsubscribe from old deps ;; Unsubscribe from old deps
(for-each (for-each
@@ -156,7 +165,7 @@
(let ((ctx (make-tracking-context run-effect))) (let ((ctx (make-tracking-context run-effect)))
(let ((prev (get-tracking-context))) (let ((prev (get-tracking-context)))
(set-tracking-context! ctx) (set-tracking-context! ctx)
(let ((result (effect-fn))) (let ((result (invoke effect-fn)))
(set-tracking-context! prev) (set-tracking-context! prev)
(set! deps (tracking-context-deps ctx)) (set! deps (tracking-context-deps ctx))
;; If effect returns a function, it's the cleanup ;; If effect returns a function, it's the cleanup
@@ -169,7 +178,7 @@
;; Return dispose function ;; Return dispose function
(fn () (fn ()
(set! disposed true) (set! disposed true)
(when cleanup-fn (cleanup-fn)) (when cleanup-fn (invoke cleanup-fn))
(for-each (for-each
(fn (dep) (signal-remove-sub! dep run-effect)) (fn (dep) (signal-remove-sub! dep run-effect))
deps) deps)
@@ -189,7 +198,7 @@
(define batch (define batch
(fn (thunk) (fn (thunk)
(set! *batch-depth* (+ *batch-depth* 1)) (set! *batch-depth* (+ *batch-depth* 1))
(thunk) (invoke thunk)
(set! *batch-depth* (- *batch-depth* 1)) (set! *batch-depth* (- *batch-depth* 1))
(when (= *batch-depth* 0) (when (= *batch-depth* 0)
(let ((queue *batch-queue*)) (let ((queue *batch-queue*))
@@ -308,7 +317,7 @@
(let ((registry *store-registry*)) (let ((registry *store-registry*))
;; Only create the store once — subsequent calls return existing ;; Only create the store once — subsequent calls return existing
(when (not (has-key? registry name)) (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)))) (get *store-registry* name))))
(define use-store (define use-store
@@ -367,7 +376,7 @@
(fn (e) (fn (e)
(let ((detail (event-detail e)) (let ((detail (event-detail e))
(new-val (if transform-fn (new-val (if transform-fn
(transform-fn detail) (invoke transform-fn detail)
detail))) detail)))
(reset! target-signal new-val)))))) (reset! target-signal new-val))))))
;; Return cleanup — removes listener on dispose/re-run ;; Return cleanup — removes listener on dispose/re-run

View File

@@ -1,4 +1,5 @@
# WARNING: special-forms.sx declares forms not in eval.sx: reset, shift # 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. 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 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): def json_serialize(obj):
import json import json
try: try:
@@ -467,17 +478,8 @@ def dict_delete(d, k):
def is_render_expr(expr): def is_render_expr(expr):
"""Check if expression is an HTML element, component, or fragment.""" """Placeholder — overridden by transpiled version from render.sx."""
if not isinstance(expr, list) or not expr: return False
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)))
# Render dispatch -- set by adapter # Render dispatch -- set by adapter
@@ -519,6 +521,10 @@ def make_raw_html(s):
return _RawHTML(s) return _RawHTML(s)
def sx_expr_source(x):
return x.source if isinstance(x, SxExpr) else str(x)
class EvalError(Exception): class EvalError(Exception):
pass pass
@@ -558,7 +564,12 @@ def escape_string(s):
def serialize(val): 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) t = type_of(val)
if t == "sx-expr": if t == "sx-expr":
return val.source return val.source
@@ -592,179 +603,26 @@ def serialize(val):
return "nil" return "nil"
return str(val) 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([ _SPECIAL_FORM_NAMES = frozenset() # Placeholder — overridden by transpiled adapter-sx.sx
"if", "when", "cond", "case", "and", "or", _HO_FORM_NAMES = frozenset()
"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",
])
def is_special_form(name): 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): 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): def aser_special(name, expr, env):
"""Evaluate a special/HO form in aser mode. """Placeholder — overridden by transpiled version from adapter-sx.sx."""
return trampoline(eval_expr(expr, env))
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)
# ========================================================================= # =========================================================================
@@ -1021,6 +879,7 @@ parse_int = PRIMITIVES["parse-int"]
upper = PRIMITIVES["upper"] upper = PRIMITIVES["upper"]
has_key_p = PRIMITIVES["has-key?"] has_key_p = PRIMITIVES["has-key?"]
dissoc = PRIMITIVES["dissoc"] 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 [] return list(c.css_classes) if hasattr(c, "css_classes") and c.css_classes else []
def env_components(env): 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() return [k for k, v in env.items()
if isinstance(v, (Component, Macro))] 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
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)) 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 === # === 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)) 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) === # === Transpiled from deps (component dependency analysis) ===
# scan-refs # scan-refs
@@ -1413,6 +1320,9 @@ render_target = lambda name, env, io_names: (lambda key: (lambda val: ('server'
# page-render-plan # 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)) 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) === # === 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 : ( 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)), for_each(lambda dep: signal_remove_sub(dep, recompute), signal_deps(s)),
signal_set_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)) )[-1])))(NIL))([]))(make_signal(NIL))
# effect # effect
@@ -1441,11 +1351,11 @@ def effect(effect_fn):
_cells['deps'] = [] _cells['deps'] = []
_cells['disposed'] = False _cells['disposed'] = False
_cells['cleanup_fn'] = NIL _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() run_effect()
return _sx_fn(lambda : ( return _sx_fn(lambda : (
_sx_cell_set(_cells, 'disposed', True), _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']), for_each(lambda dep: signal_remove_sub(dep, run_effect), _cells['deps']),
_sx_cell_set(_cells, 'deps', []) _sx_cell_set(_cells, 'deps', [])
)[-1]) )[-1])
@@ -1459,7 +1369,7 @@ _batch_queue = []
# batch # batch
def batch(thunk): def batch(thunk):
_batch_depth = (_batch_depth + 1) _batch_depth = (_batch_depth + 1)
thunk() invoke(thunk)
_batch_depth = (_batch_depth - 1) _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) 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): def def_store(name, init_fn):
registry = _store_registry registry = _store_registry
if sx_truthy((not sx_truthy(has_key_p(registry, name)))): 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) return get(_store_registry, name)
# use-store # 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) on_event = lambda el, event_name, handler: dom_listen(el, event_name, handler)
# bridge-event # 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 # Public API
# ========================================================================= # =========================================================================
# Wrap aser outputs to return SxExpr
_wrap_aser_outputs()
# Set HTML as default adapter # Set HTML as default adapter
_setup_html_adapter() _setup_html_adapter()