diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js
index 4f4a886..617df9a 100644
--- a/shared/static/scripts/sx-browser.js
+++ b/shared/static/scripts/sx-browser.js
@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
- var SX_VERSION = "2026-03-11T14:54:55Z";
+ var SX_VERSION = "2026-03-11T16:35:21Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -204,7 +204,7 @@
// JSON / dict helpers for island state serialization
function jsonSerialize(obj) {
- try { return JSON.stringify(obj); } catch(e) { return "{}"; }
+ return JSON.stringify(obj);
}
function isEmptyDict(d) {
if (!d || typeof d !== "object") return true;
@@ -214,11 +214,34 @@
function envHas(env, name) { return name in env; }
function envGet(env, name) { return env[name]; }
- function envSet(env, name, val) { env[name] = val; }
+ function envSet(env, name, val) {
+ // Walk prototype chain to find where the variable is defined (for set!)
+ var obj = env;
+ while (obj !== null && obj !== Object.prototype) {
+ if (obj.hasOwnProperty(name)) { obj[name] = val; return; }
+ obj = Object.getPrototypeOf(obj);
+ }
+ // Not found in any parent scope — set on the immediate env
+ env[name] = val;
+ }
function envExtend(env) { return Object.create(env); }
function envMerge(base, overlay) {
+ // Same env or overlay is descendant of base — just extend, no copy.
+ // This prevents set! inside lambdas from modifying shadow copies.
+ if (base === overlay) return Object.create(base);
+ var p = overlay;
+ for (var d = 0; p && p !== Object.prototype && d < 100; d++) {
+ if (p === base) return Object.create(base);
+ p = Object.getPrototypeOf(p);
+ }
+ // General case: extend base, copy ONLY overlay properties that don't
+ // exist in the base chain (avoids shadowing closure bindings).
var child = Object.create(base);
- if (overlay) for (var k in overlay) if (overlay.hasOwnProperty(k)) child[k] = overlay[k];
+ if (overlay) {
+ for (var k in overlay) {
+ if (overlay.hasOwnProperty(k) && !(k in base)) child[k] = overlay[k];
+ }
+ }
return child;
}
@@ -732,9 +755,9 @@
var kwargs = first(parsed);
var children = nth(parsed, 1);
var local = envMerge(componentClosure(comp), env);
- { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = sxOr(dictGet(kwargs, p), NIL); } }
+ { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, sxOr(dictGet(kwargs, p), NIL)); } }
if (isSxTruthy(componentHasChildren(comp))) {
- local["children"] = children;
+ envSet(local, "children", children);
}
return makeThunk(componentBody(comp), local);
})(); };
@@ -841,7 +864,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var loopBody = (isSxTruthy((len(body) == 1)) ? first(body) : cons(makeSymbol("begin"), body));
var loopFn = makeLambda(params, loopBody, env);
loopFn.name = loopName;
- lambdaClosure(loopFn)[loopName] = loopFn;
+ envSet(lambdaClosure(loopFn), loopName, loopFn);
return (function() {
var initVals = map(function(e) { return trampoline(evalExpr(e, env)); }, inits);
return callLambda(loopFn, initVals, env);
@@ -865,7 +888,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
if (isSxTruthy((isSxTruthy(isLambda(value)) && isNil(lambdaName(value))))) {
value.name = symbolName(nameSym);
}
- env[symbolName(nameSym)] = value;
+ envSet(env, symbolName(nameSym), value);
return value;
})(); };
@@ -881,7 +904,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var affinity = defcompKwarg(args, "affinity", "auto");
return (function() {
var comp = makeComponent(compName, params, hasChildren, body, env, affinity);
- env[symbolName(nameSym)] = comp;
+ envSet(env, symbolName(nameSym), comp);
return comp;
})();
})(); };
@@ -924,7 +947,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var hasChildren = nth(parsed, 1);
return (function() {
var island = makeIsland(compName, params, hasChildren, body, env);
- env[symbolName(nameSym)] = island;
+ envSet(env, symbolName(nameSym), island);
return island;
})();
})(); };
@@ -939,7 +962,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var restParam = nth(parsed, 1);
return (function() {
var mac = makeMacro(params, restParam, body, env, symbolName(nameSym));
- env[symbolName(nameSym)] = mac;
+ envSet(env, symbolName(nameSym), mac);
return mac;
})();
})(); };
@@ -956,7 +979,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var sfDefstyle = function(args, env) { return (function() {
var nameSym = first(args);
var value = trampoline(evalExpr(nth(args, 1), env));
- env[symbolName(nameSym)] = value;
+ envSet(env, symbolName(nameSym), value);
return value;
})(); };
@@ -996,7 +1019,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var sfSetBang = function(args, env) { return (function() {
var name = symbolName(first(args));
var value = trampoline(evalExpr(nth(args, 1), env));
- env[name] = value;
+ envSet(env, name, value);
return value;
})(); };
@@ -1021,7 +1044,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
})(); }, NIL, range(0, (len(bindings) / 2))));
(function() {
var values = map(function(e) { return trampoline(evalExpr(e, local)); }, valExprs);
- { var _c = zip(names, values); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = nth(pair, 1); } }
+ { var _c = zip(names, values); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; envSet(local, first(pair), nth(pair, 1)); } }
return forEach(function(val) { return (isSxTruthy(isLambda(val)) ? forEach(function(n) { return envSet(lambdaClosure(val), n, envGet(local, n)); }, names) : NIL); }, values);
})();
{ var _c = slice(body, 0, (len(body) - 1)); for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; trampoline(evalExpr(e, local)); } }
@@ -1046,9 +1069,9 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
// expand-macro
var expandMacro = function(mac, rawArgs, env) { return (function() {
var local = envMerge(macroClosure(mac), env);
- { var _c = mapIndexed(function(i, p) { return [p, i]; }, macroParams(mac)); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = (isSxTruthy((nth(pair, 1) < len(rawArgs))) ? nth(rawArgs, nth(pair, 1)) : NIL); } }
+ { var _c = mapIndexed(function(i, p) { return [p, i]; }, macroParams(mac)); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; envSet(local, first(pair), (isSxTruthy((nth(pair, 1) < len(rawArgs))) ? nth(rawArgs, nth(pair, 1)) : NIL)); } }
if (isSxTruthy(macroRestParam(mac))) {
- local[macroRestParam(mac)] = slice(rawArgs, len(macroParams(mac)));
+ envSet(local, macroRestParam(mac), slice(rawArgs, len(macroParams(mac))));
}
return trampoline(evalExpr(macroBody(mac), local));
})(); };
@@ -1162,7 +1185,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
// process-bindings
var processBindings = function(bindings, env) { return (function() {
- var local = merge(env);
+ var local = envExtend(env);
{ var _c = bindings; for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(pair) == "list")) && (len(pair) >= 2)))) {
(function() {
var name = (isSxTruthy((typeOf(first(pair)) == "symbol")) ? symbolName(first(pair)) : (String(first(pair))));
@@ -1392,9 +1415,9 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
})(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() {
var local = envMerge(componentClosure(comp), env);
- { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
+ { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(comp))) {
- local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)));
+ envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))));
}
return renderToHtml(componentBody(comp), local);
})();
@@ -1458,20 +1481,20 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
return (function() {
var local = envMerge(componentClosure(island), env);
var islandName = componentName(island);
- { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
+ { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(island))) {
- local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children)));
+ envSet(local, "children", makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))));
}
return (function() {
var bodyHtml = renderToHtml(componentBody(island), local);
- var stateJson = serializeIslandState(kwargs);
- return (String("") + String(bodyHtml) + String(""));
+ var stateSx = serializeIslandState(kwargs);
+ return (String("") + String(bodyHtml) + String(""));
})();
})();
})(); };
// serialize-island-state
- var serializeIslandState = function(kwargs) { return (isSxTruthy(isEmptyDict(kwargs)) ? NIL : jsonSerialize(kwargs)); };
+ var serializeIslandState = function(kwargs) { return (isSxTruthy(isEmptyDict(kwargs)) ? NIL : sxSerialize(kwargs)); };
// === Transpiled from adapter-sx ===
@@ -1586,7 +1609,7 @@ return result; }, args);
var coll = trampoline(evalExpr(nth(args, 1), env));
return map(function(item) { return (isSxTruthy(isLambda(f)) ? (function() {
var local = envMerge(lambdaClosure(f), env);
- local[first(lambdaParams(f))] = item;
+ envSet(local, first(lambdaParams(f)), item);
return aser(lambdaBody(f), local);
})() : invoke(f, item)); }, coll);
})() : (isSxTruthy((name == "map-indexed")) ? (function() {
@@ -1594,8 +1617,8 @@ return result; }, args);
var coll = trampoline(evalExpr(nth(args, 1), env));
return mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? (function() {
var local = envMerge(lambdaClosure(f), env);
- local[first(lambdaParams(f))] = i;
- local[nth(lambdaParams(f), 1)] = item;
+ envSet(local, first(lambdaParams(f)), i);
+ envSet(local, nth(lambdaParams(f), 1), item);
return aser(lambdaBody(f), local);
})() : invoke(f, i, item)); }, coll);
})() : (isSxTruthy((name == "for-each")) ? (function() {
@@ -1604,7 +1627,7 @@ return result; }, args);
var results = [];
{ var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var item = _c[_i]; (isSxTruthy(isLambda(f)) ? (function() {
var local = envMerge(lambdaClosure(f), env);
- local[first(lambdaParams(f))] = item;
+ envSet(local, first(lambdaParams(f)), item);
return append_b(results, aser(lambdaBody(f), local));
})() : invoke(f, item)); } }
return (isSxTruthy(isEmpty(results)) ? NIL : results);
@@ -1662,7 +1685,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
var attrExpr = nth(args, (get(state, "i") + 1));
(isSxTruthy(startsWith(attrName, "on-")) ? (function() {
var attrVal = trampoline(evalExpr(attrExpr, env));
- return (isSxTruthy(isCallable(attrVal)) ? domListen(el, slice(attrName, 3), (isSxTruthy((isSxTruthy(isLambda(attrVal)) && (len(lambdaParams(attrVal)) == 0))) ? function(e) { return callLambda(attrVal, [], lambdaClosure(attrVal)); } : attrVal)) : NIL);
+ return (isSxTruthy(isCallable(attrVal)) ? domListen(el, slice(attrName, 3), attrVal) : NIL);
})() : (isSxTruthy((attrName == "bind")) ? (function() {
var attrVal = trampoline(evalExpr(attrExpr, env));
return (isSxTruthy(isSignal(attrVal)) ? bindInput(el, attrVal) : NIL);
@@ -1696,7 +1719,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
})(); }, {["i"]: 0, ["skip"]: false}, args);
return (function() {
var local = envMerge(componentClosure(comp), env);
- { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
+ { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(comp))) {
(function() {
var childFrag = createFragment();
@@ -1887,7 +1910,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return createFragme
return (function() {
var local = envMerge(componentClosure(island), env);
var islandName = componentName(island);
- { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
+ { var _c = componentParams(island); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
if (isSxTruthy(componentHasChildren(island))) {
(function() {
var childFrag = createFragment();
@@ -3002,9 +3025,9 @@ return postSwap(target); }))) : NIL);
var exprs = sxParse(body);
return domListen(el, eventName, function(e) { return (function() {
var handlerEnv = envExtend({});
- handlerEnv["event"] = e;
- handlerEnv["this"] = el;
- handlerEnv["detail"] = eventDetail(e);
+ envSet(handlerEnv, "event", e);
+ envSet(handlerEnv, "this", el);
+ envSet(handlerEnv, "detail", eventDetail(e));
return forEach(function(expr) { return evalExpr(expr, handlerEnv); }, exprs);
})(); });
})()) : NIL);
@@ -3233,17 +3256,17 @@ callExpr.push(dictGet(kwargs, k)); } }
// hydrate-island
var hydrateIsland = function(el) { return (function() {
var name = domGetAttr(el, "data-sx-island");
- var stateJson = sxOr(domGetAttr(el, "data-sx-state"), "{}");
+ var stateSx = sxOr(domGetAttr(el, "data-sx-state"), "{}");
return (function() {
var compName = (String("~") + String(name));
var env = getRenderEnv(NIL);
return (function() {
var comp = envGet(env, compName);
return (isSxTruthy(!isSxTruthy(sxOr(isComponent(comp), isIsland(comp)))) ? logWarn((String("hydrate-island: unknown island ") + String(compName))) : (function() {
- var kwargs = jsonParse(stateJson);
+ var kwargs = sxOr(first(sxParse(stateSx)), {});
var disposers = [];
var local = envMerge(componentClosure(comp), env);
- { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } }
+ { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envSet(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
return (function() {
var bodyDom = withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); });
domSetTextContent(el, "");
@@ -3976,8 +3999,11 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function domListen(el, name, handler) {
if (!_hasDom || !el) return function() {};
// Wrap SX lambdas from runtime-evaluated island code into native fns
+ // If lambda takes 0 params, call without event arg (convenience for on-click handlers)
var wrapped = isLambda(handler)
- ? function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
+ ? (lambdaParams(handler).length === 0
+ ? function(e) { try { invoke(handler); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
+ : function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
: handler;
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
el.addEventListener(name, wrapped);
diff --git a/shared/sx/html.py b/shared/sx/html.py
index f7fc9d5..694a03f 100644
--- a/shared/sx/html.py
+++ b/shared/sx/html.py
@@ -414,10 +414,10 @@ def _render_component(comp: Component, args: list, env: dict[str, Any]) -> str:
def _render_island(island: Island, args: list, env: dict[str, Any]) -> str:
"""Render an island as static HTML with hydration attributes.
- Produces: body HTML
- The client hydrates this into a reactive island.
+ Produces: body HTML
+ The client hydrates this into a reactive island via sx-parse (not JSON).
"""
- import json as _json
+ from .parser import serialize as _sx_serialize
kwargs: dict[str, Any] = {}
children: list[Any] = []
@@ -443,26 +443,13 @@ def _render_island(island: Island, args: list, env: dict[str, Any]) -> str:
body_html = _render(island.body, local)
- # Serialize state for hydration — only keyword args
- state = {}
- for k, v in kwargs.items():
- if isinstance(v, (str, int, float, bool)):
- state[k] = v
- elif v is NIL or v is None:
- state[k] = None
- elif isinstance(v, list):
- state[k] = v
- elif isinstance(v, dict):
- state[k] = v
- else:
- state[k] = str(v)
-
- state_json = _escape_attr(_json.dumps(state, separators=(",", ":"))) if state else ""
+ # Serialize state for hydration — SX format (not JSON)
+ state_sx = _escape_attr(_sx_serialize(kwargs)) if kwargs else ""
island_name = _escape_attr(island.name)
parts = [f'")
parts.append(body_html)
parts.append("")
diff --git a/shared/sx/ref/adapter-async.sx b/shared/sx/ref/adapter-async.sx
index b620090..c2f093d 100644
--- a/shared/sx/ref/adapter-async.sx
+++ b/shared/sx/ref/adapter-async.sx
@@ -450,7 +450,9 @@
(define-async async-process-bindings
(fn (bindings env ctx)
- (let ((local (merge env)))
+ ;; env-extend (not merge) — Env is not a dict subclass, so merge()
+ ;; returns an empty dict, losing all parent scope bindings.
+ (let ((local (env-extend env)))
(if (and (= (type-of bindings) "list") (not (empty? bindings)))
(if (= (type-of (first bindings)) "list")
;; Scheme-style: ((name val) ...)
@@ -669,7 +671,10 @@
(evaled-args (async-eval-args args env ctx)))
(cond
(and (callable? f) (not (lambda? f)) (not (component? f)))
- (async-invoke f evaled-args)
+ ;; apply directly — async-invoke takes &rest so passing a list
+ ;; would wrap it in another list
+ (let ((r (apply f evaled-args)))
+ (if (async-coroutine? r) (async-await! r) r))
(lambda? f)
(let ((local (env-merge (lambda-closure f) env)))
(for-each-indexed
@@ -1166,19 +1171,20 @@
(define-async async-eval-slot-inner
(fn (expr env ctx)
- (let ((result
- (if (and (list? expr) (not (empty? expr)))
- (let ((head (first expr)))
- (if (and (= (type-of head) "symbol")
- (starts-with? (symbol-name head) "~"))
- (let ((name (symbol-name head))
- (val (if (env-has? env name) (env-get env name) nil)))
- (if (component? val)
- (async-aser-component val (rest expr) env ctx)
- ;; Islands and unknown components — fall through to aser
- (async-maybe-expand-result (async-aser expr env ctx) env ctx)))
- (async-maybe-expand-result (async-aser expr env ctx) env ctx)))
- (async-maybe-expand-result (async-aser expr env ctx) env ctx))))
+ ;; NOTE: Uses statement-form let + set! to avoid expression-context
+ ;; let (IIFE lambdas) which can't contain await in Python.
+ (let ((result nil))
+ (if (and (list? expr) (not (empty? expr)))
+ (let ((head (first expr)))
+ (if (and (= (type-of head) "symbol")
+ (starts-with? (symbol-name head) "~"))
+ (let ((name (symbol-name head))
+ (val (if (env-has? env name) (env-get env name) nil)))
+ (if (component? val)
+ (set! result (async-aser-component val (rest expr) env ctx))
+ (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx))))
+ (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx))))
+ (set! result (async-maybe-expand-result (async-aser expr env ctx) env ctx)))
;; Normalize result to SxExpr
(if (sx-expr? result)
result
diff --git a/shared/sx/ref/adapter-dom.sx b/shared/sx/ref/adapter-dom.sx
index 3e84f4f..e659b99 100644
--- a/shared/sx/ref/adapter-dom.sx
+++ b/shared/sx/ref/adapter-dom.sx
@@ -186,15 +186,10 @@
(attr-expr (nth args (inc (get state "i")))))
(cond
;; Event handler: evaluate eagerly, bind listener
- ;; If handler is a 0-arity lambda, wrap to ignore the event arg
(starts-with? attr-name "on-")
(let ((attr-val (trampoline (eval-expr attr-expr env))))
(when (callable? attr-val)
- (dom-listen el (slice attr-name 3)
- (if (and (lambda? attr-val)
- (= (len (lambda-params attr-val)) 0))
- (fn (e) (call-lambda attr-val (list) (lambda-closure attr-val)))
- attr-val))))
+ (dom-listen el (slice attr-name 3) attr-val)))
;; Two-way input binding: :bind signal
(= attr-name "bind")
(let ((attr-val (trampoline (eval-expr attr-expr env))))
diff --git a/shared/sx/ref/adapter-html.sx b/shared/sx/ref/adapter-html.sx
index 039090e..f4719e2 100644
--- a/shared/sx/ref/adapter-html.sx
+++ b/shared/sx/ref/adapter-html.sx
@@ -433,11 +433,11 @@
;; Render the island body as HTML
(let ((body-html (render-to-html (component-body island) local))
- (state-json (serialize-island-state kwargs)))
+ (state-sx (serialize-island-state kwargs)))
;; Wrap in container with hydration attributes
(str ""
body-html
@@ -445,17 +445,17 @@
;; --------------------------------------------------------------------------
-;; serialize-island-state — serialize kwargs to JSON for hydration
+;; serialize-island-state — serialize kwargs to SX for hydration
;; --------------------------------------------------------------------------
;;
-;; Only serializes simple values (numbers, strings, booleans, nil, lists, dicts).
-;; Functions, components, and other non-serializable values are skipped.
+;; Uses the SX serializer (not JSON) so the client can parse with sx-parse.
+;; Handles all SX types natively: numbers, strings, booleans, nil, lists, dicts.
(define serialize-island-state
(fn (kwargs)
(if (empty-dict? kwargs)
nil
- (json-serialize kwargs))))
+ (sx-serialize kwargs))))
;; --------------------------------------------------------------------------
@@ -476,8 +476,8 @@
;; Raw HTML construction:
;; (make-raw-html s) → wrap string as raw HTML (not double-escaped)
;;
-;; JSON serialization (for island state):
-;; (json-serialize dict) → JSON string
+;; Island state serialization:
+;; (sx-serialize val) → SX source string (from parser.sx)
;; (empty-dict? d) → boolean
;; (escape-attr s) → HTML attribute escape
;;
diff --git a/shared/sx/ref/async_eval_ref.py b/shared/sx/ref/async_eval_ref.py
index eebe2b8..96a79d5 100644
--- a/shared/sx/ref/async_eval_ref.py
+++ b/shared/sx/ref/async_eval_ref.py
@@ -1,1022 +1,22 @@
-"""Async evaluation wrapper for the transpiled reference evaluator.
+"""Async evaluation — thin re-export from bootstrapped sx_ref.py.
-Wraps the sync sx_ref.py evaluator with async I/O support, mirroring
-the hand-written async_eval.py. Provides the same public API:
+The async adapter (adapter-async.sx) is now bootstrapped directly into
+sx_ref.py alongside the sync evaluator. This file re-exports the public
+API so existing imports keep working.
- async_eval() — evaluate with I/O primitives
- async_render() — render to HTML with I/O
- async_eval_to_sx() — evaluate to SX wire format with I/O
- async_eval_slot_to_sx() — expand components server-side, then serialize
+All async rendering, serialization, and evaluation logic lives in the spec:
+ - shared/sx/ref/adapter-async.sx (canonical SX source)
+ - shared/sx/ref/sx_ref.py (bootstrapped Python)
-The sync transpiled evaluator handles all control flow, special forms,
-and lambda/component dispatch. This wrapper adds:
-
- - RequestContext threading
- - I/O primitive interception (query, service, request-arg, etc.)
- - Async trampoline for thunks
- - SxExpr wrapping for wire format output
-
-DO NOT EDIT by hand — this is a thin wrapper; the actual eval logic
-lives in sx_ref.py (generated) and the I/O primitives in primitives_io.py.
+Platform async primitives (I/O dispatch, context vars, RequestContext)
+are in shared/sx/ref/platform_py.py → PLATFORM_ASYNC_PY.
"""
-from __future__ import annotations
-
-import contextvars
-import inspect
-from typing import Any
-
-from ..types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
-from ..parser import SxExpr, serialize
-from ..primitives_io import IO_PRIMITIVES, RequestContext, execute_io
-from ..html import (
- HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,
- escape_text, escape_attr, _RawHTML, css_class_collector, _svg_context,
-)
-
from . import sx_ref
-# Re-export EvalError from sx_ref
+# Re-export the public API used by handlers.py, helpers.py, pages.py, etc.
EvalError = sx_ref.EvalError
-
-# When True, _aser expands known components server-side
-_expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar(
- "_expand_components_ref", default=False
-)
-
-
-# ---------------------------------------------------------------------------
-# Async TCO
-# ---------------------------------------------------------------------------
-
-class _AsyncThunk:
- __slots__ = ("expr", "env", "ctx")
- def __init__(self, expr, env, ctx):
- self.expr = expr
- self.env = env
- self.ctx = ctx
-
-
-async def _async_trampoline(val):
- while isinstance(val, _AsyncThunk):
- val = await _async_eval(val.expr, val.env, val.ctx)
- return val
-
-
-# ---------------------------------------------------------------------------
-# Async evaluate — wraps transpiled sync eval with I/O support
-# ---------------------------------------------------------------------------
-
-async def async_eval(expr, env, ctx=None):
- """Public entry point: evaluate with I/O primitives."""
- if ctx is None:
- ctx = RequestContext()
- result = await _async_eval(expr, env, ctx)
- while isinstance(result, _AsyncThunk):
- result = await _async_eval(result.expr, result.env, result.ctx)
- return result
-
-
-async def _async_eval(expr, env, ctx):
- """Internal async evaluator. Intercepts I/O primitives,
- delegates everything else to the sync transpiled evaluator."""
- # Intercept I/O primitive calls
- if isinstance(expr, list) and expr:
- head = expr[0]
- if isinstance(head, Symbol) and head.name in IO_PRIMITIVES:
- args, kwargs = await _parse_io_args(expr[1:], env, ctx)
- return await execute_io(head.name, args, kwargs, ctx)
-
- # Check if this is a render expression (HTML tag, component, fragment)
- # so we can wrap the result in _RawHTML to prevent double-escaping.
- # The sync evaluator returns plain strings from render_list_to_html;
- # the async renderer would HTML-escape those without this wrapper.
- is_render = isinstance(expr, list) and sx_ref.is_render_expr(expr)
-
- # For everything else, use the sync transpiled evaluator
- result = sx_ref.eval_expr(expr, env)
- result = sx_ref.trampoline(result)
-
- if is_render and isinstance(result, str):
- return _RawHTML(result)
- return result
-
-
-async def _parse_io_args(exprs, env, ctx):
- """Parse and evaluate I/O node args (keyword + positional)."""
- args = []
- kwargs = {}
- i = 0
- while i < len(exprs):
- item = exprs[i]
- if isinstance(item, Keyword) and i + 1 < len(exprs):
- kwargs[item.name] = await async_eval(exprs[i + 1], env, ctx)
- i += 2
- else:
- args.append(await async_eval(item, env, ctx))
- i += 1
- return args, kwargs
-
-
-# ---------------------------------------------------------------------------
-# Async HTML renderer
-# ---------------------------------------------------------------------------
-
-async def async_render(expr, env, ctx=None):
- """Render to HTML, awaiting I/O primitives inline."""
- if ctx is None:
- ctx = RequestContext()
- return await _arender(expr, env, ctx)
-
-
-async def _arender(expr, env, ctx):
- if expr is None or expr is NIL or expr is False or expr is True:
- return ""
- if isinstance(expr, _RawHTML):
- return expr.html
- # Also handle sx_ref._RawHTML from the sync evaluator
- if isinstance(expr, sx_ref._RawHTML):
- return expr.html
- if isinstance(expr, str):
- return escape_text(expr)
- if isinstance(expr, (int, float)):
- return escape_text(str(expr))
- if isinstance(expr, Symbol):
- val = await async_eval(expr, env, ctx)
- return await _arender(val, env, ctx)
- if isinstance(expr, Keyword):
- return escape_text(expr.name)
- if isinstance(expr, list):
- if not expr:
- return ""
- return await _arender_list(expr, env, ctx)
- if isinstance(expr, dict):
- return ""
- return escape_text(str(expr))
-
-
-async def _arender_list(expr, env, ctx):
- head = expr[0]
- if isinstance(head, Symbol):
- name = head.name
-
- # I/O primitive
- if name in IO_PRIMITIVES:
- result = await async_eval(expr, env, ctx)
- return await _arender(result, env, ctx)
-
- # raw!
- if name == "raw!":
- parts = []
- for arg in expr[1:]:
- val = await async_eval(arg, env, ctx)
- if isinstance(val, _RawHTML):
- parts.append(val.html)
- elif isinstance(val, str):
- parts.append(val)
- elif val is not None and val is not NIL:
- parts.append(str(val))
- return "".join(parts)
-
- # Fragment
- if name == "<>":
- parts = [await _arender(c, env, ctx) for c in expr[1:]]
- return "".join(parts)
-
- # html: prefix
- if name.startswith("html:"):
- return await _arender_element(name[5:], expr[1:], env, ctx)
-
- # Render-aware special forms
- arsf = _ASYNC_RENDER_FORMS.get(name)
- if arsf is not None:
- if name in HTML_TAGS and (
- (len(expr) > 1 and isinstance(expr[1], Keyword))
- or _svg_context.get(False)
- ):
- return await _arender_element(name, expr[1:], env, ctx)
- return await arsf(expr, env, ctx)
-
- # Macro expansion
- if name in env:
- val = env[name]
- if isinstance(val, Macro):
- expanded = sx_ref.trampoline(
- sx_ref.expand_macro(val, expr[1:], env)
- )
- return await _arender(expanded, env, ctx)
-
- # HTML tag
- if name in HTML_TAGS:
- return await _arender_element(name, expr[1:], env, ctx)
-
- # Component / Island
- if name.startswith("~"):
- val = env.get(name)
- if isinstance(val, Island):
- return sx_ref.render_html_island(val, expr[1:], env)
- if isinstance(val, Component):
- return await _arender_component(val, expr[1:], env, ctx)
-
- # Custom element
- if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword):
- return await _arender_element(name, expr[1:], env, ctx)
-
- # SVG context
- if _svg_context.get(False):
- return await _arender_element(name, expr[1:], env, ctx)
-
- # Fallback — evaluate then render
- result = await async_eval(expr, env, ctx)
- return await _arender(result, env, ctx)
-
- if isinstance(head, (Lambda, list)):
- result = await async_eval(expr, env, ctx)
- return await _arender(result, env, ctx)
-
- # Data list
- parts = [await _arender(item, env, ctx) for item in expr]
- return "".join(parts)
-
-
-async def _arender_element(tag, args, env, ctx):
- attrs = {}
- children = []
- i = 0
- while i < len(args):
- arg = args[i]
- if isinstance(arg, Keyword) and i + 1 < len(args):
- attrs[arg.name] = await async_eval(args[i + 1], env, ctx)
- i += 2
- else:
- children.append(arg)
- i += 1
-
- class_val = attrs.get("class")
- if class_val is not None and class_val is not NIL and class_val is not False:
- collector = css_class_collector.get(None)
- if collector is not None:
- collector.update(str(class_val).split())
-
- parts = [f"<{tag}"]
- for attr_name, attr_val in attrs.items():
- if attr_val is None or attr_val is NIL or attr_val is False:
- continue
- if attr_name in BOOLEAN_ATTRS:
- if attr_val:
- parts.append(f" {attr_name}")
- elif attr_val is True:
- parts.append(f" {attr_name}")
- else:
- parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"')
- parts.append(">")
- opening = "".join(parts)
-
- if tag in VOID_ELEMENTS:
- return opening
-
- token = None
- if tag in ("svg", "math"):
- token = _svg_context.set(True)
- try:
- child_parts = [await _arender(c, env, ctx) for c in children]
- finally:
- if token is not None:
- _svg_context.reset(token)
-
- return f"{opening}{''.join(child_parts)}{tag}>"
-
-
-async def _arender_component(comp, args, env, ctx):
- kwargs = {}
- children = []
- i = 0
- while i < len(args):
- arg = args[i]
- if isinstance(arg, Keyword) and i + 1 < len(args):
- kwargs[arg.name] = await async_eval(args[i + 1], env, ctx)
- i += 2
- else:
- children.append(arg)
- i += 1
- local = dict(comp.closure)
- local.update(env)
- for p in comp.params:
- local[p] = kwargs.get(p, NIL)
- if comp.has_children:
- child_html = [await _arender(c, env, ctx) for c in children]
- local["children"] = _RawHTML("".join(child_html))
- return await _arender(comp.body, local, ctx)
-
-
-async def _arender_lambda(fn, args, env, ctx):
- local = dict(fn.closure)
- local.update(env)
- for p, v in zip(fn.params, args):
- local[p] = v
- return await _arender(fn.body, local, ctx)
-
-
-# ---------------------------------------------------------------------------
-# Render-aware special forms
-# ---------------------------------------------------------------------------
-
-async def _arsf_if(expr, env, ctx):
- cond = await async_eval(expr[1], env, ctx)
- if cond and cond is not NIL:
- return await _arender(expr[2], env, ctx)
- return await _arender(expr[3], env, ctx) if len(expr) > 3 else ""
-
-
-async def _arsf_when(expr, env, ctx):
- cond = await async_eval(expr[1], env, ctx)
- if cond and cond is not NIL:
- return "".join([await _arender(b, env, ctx) for b in expr[2:]])
- return ""
-
-
-async def _arsf_cond(expr, env, ctx):
- clauses = expr[1:]
- if not clauses:
- return ""
- if isinstance(clauses[0], list) and len(clauses[0]) == 2:
- for clause in clauses:
- test = clause[0]
- if isinstance(test, Symbol) and test.name in ("else", ":else"):
- return await _arender(clause[1], env, ctx)
- if isinstance(test, Keyword) and test.name == "else":
- return await _arender(clause[1], env, ctx)
- if await async_eval(test, env, ctx):
- return await _arender(clause[1], env, ctx)
- else:
- i = 0
- while i < len(clauses) - 1:
- test, result = clauses[i], clauses[i + 1]
- if isinstance(test, Keyword) and test.name == "else":
- return await _arender(result, env, ctx)
- if isinstance(test, Symbol) and test.name in (":else", "else"):
- return await _arender(result, env, ctx)
- if await async_eval(test, env, ctx):
- return await _arender(result, env, ctx)
- i += 2
- return ""
-
-
-async def _arsf_let(expr, env, ctx):
- bindings = expr[1]
- local = dict(env)
- if isinstance(bindings, list):
- if bindings and isinstance(bindings[0], list):
- for b in bindings:
- var = b[0]
- vname = var.name if isinstance(var, Symbol) else var
- local[vname] = await async_eval(b[1], local, ctx)
- elif len(bindings) % 2 == 0:
- for i in range(0, len(bindings), 2):
- var = bindings[i]
- vname = var.name if isinstance(var, Symbol) else var
- local[vname] = await async_eval(bindings[i + 1], local, ctx)
- return "".join([await _arender(b, local, ctx) for b in expr[2:]])
-
-
-async def _arsf_begin(expr, env, ctx):
- return "".join([await _arender(sub, env, ctx) for sub in expr[1:]])
-
-
-async def _arsf_define(expr, env, ctx):
- await async_eval(expr, env, ctx)
- return ""
-
-
-async def _arsf_map(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- parts = []
- for item in coll:
- if isinstance(fn, Lambda):
- parts.append(await _arender_lambda(fn, (item,), env, ctx))
- elif callable(fn):
- r = fn(item)
- if inspect.iscoroutine(r):
- r = await r
- parts.append(await _arender(r, env, ctx))
- else:
- parts.append(await _arender(item, env, ctx))
- return "".join(parts)
-
-
-async def _arsf_map_indexed(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- parts = []
- for i, item in enumerate(coll):
- if isinstance(fn, Lambda):
- parts.append(await _arender_lambda(fn, (i, item), env, ctx))
- elif callable(fn):
- r = fn(i, item)
- if inspect.iscoroutine(r):
- r = await r
- parts.append(await _arender(r, env, ctx))
- else:
- parts.append(await _arender(item, env, ctx))
- return "".join(parts)
-
-
-async def _arsf_filter(expr, env, ctx):
- result = await async_eval(expr, env, ctx)
- return await _arender(result, env, ctx)
-
-
-async def _arsf_for_each(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- parts = []
- for item in coll:
- if isinstance(fn, Lambda):
- parts.append(await _arender_lambda(fn, (item,), env, ctx))
- elif callable(fn):
- r = fn(item)
- if inspect.iscoroutine(r):
- r = await r
- parts.append(await _arender(r, env, ctx))
- else:
- parts.append(await _arender(item, env, ctx))
- return "".join(parts)
-
-
-_ASYNC_RENDER_FORMS = {
- "if": _arsf_if,
- "when": _arsf_when,
- "cond": _arsf_cond,
- "let": _arsf_let,
- "let*": _arsf_let,
- "begin": _arsf_begin,
- "do": _arsf_begin,
- "define": _arsf_define,
- "defstyle": _arsf_define,
- "defcomp": _arsf_define,
- "defmacro": _arsf_define,
- "defhandler": _arsf_define,
- "defisland": _arsf_define,
- "map": _arsf_map,
- "map-indexed": _arsf_map_indexed,
- "filter": _arsf_filter,
- "for-each": _arsf_for_each,
-}
-
-
-# ---------------------------------------------------------------------------
-# Async SX wire format (aser)
-# ---------------------------------------------------------------------------
-
-async def async_eval_to_sx(expr, env, ctx=None):
- """Evaluate and produce SX source string (wire format)."""
- if ctx is None:
- ctx = RequestContext()
- result = await _aser(expr, env, ctx)
- if isinstance(result, SxExpr):
- return result
- if result is None or result is NIL:
- return SxExpr("")
- if isinstance(result, str):
- return SxExpr(result)
- return SxExpr(serialize(result))
-
-
-async def async_eval_slot_to_sx(expr, env, ctx=None):
- """Like async_eval_to_sx but expands component calls server-side."""
- if ctx is None:
- ctx = RequestContext()
- token = _expand_components.set(True)
- try:
- return await _eval_slot_inner(expr, env, ctx)
- finally:
- _expand_components.reset(token)
-
-
-async def _eval_slot_inner(expr, env, ctx):
- if isinstance(expr, list) and expr:
- head = expr[0]
- if isinstance(head, Symbol) and head.name.startswith("~"):
- comp = env.get(head.name)
- if isinstance(comp, Component):
- result = await _aser_component(comp, expr[1:], env, ctx)
- if isinstance(result, SxExpr):
- return result
- if result is None or result is NIL:
- return SxExpr("")
- if isinstance(result, str):
- return SxExpr(result)
- return SxExpr(serialize(result))
- elif isinstance(comp, Island):
- pass # Islands serialize as SX for client hydration
- result = await _aser(expr, env, ctx)
- result = await _maybe_expand_component_result(result, env, ctx)
- if isinstance(result, SxExpr):
- return result
- if result is None or result is NIL:
- return SxExpr("")
- if isinstance(result, str):
- return SxExpr(result)
- return SxExpr(serialize(result))
-
-
-async def _maybe_expand_component_result(result, env, ctx):
- raw = None
- if isinstance(result, SxExpr):
- raw = str(result).strip()
- elif isinstance(result, str):
- raw = result.strip()
- if raw and raw.startswith("(~"):
- from ..parser import parse_all
- parsed = parse_all(raw)
- if parsed:
- return await async_eval_slot_to_sx(parsed[0], env, ctx)
- return result
-
-
-_aser_stack: list[str] = [] # diagnostic: track expression context
-
-
-async def _aser(expr, env, ctx):
- """Evaluate for SX wire format — serialize rendering forms, evaluate control flow."""
- if isinstance(expr, (int, float, bool)):
- return expr
- if isinstance(expr, SxExpr):
- return expr
- if isinstance(expr, str):
- return expr
- if expr is None or expr is NIL:
- return NIL
-
- if isinstance(expr, Symbol):
- name = expr.name
- if name in env:
- return env[name]
- if sx_ref.is_primitive(name):
- return sx_ref.get_primitive(name)
- if name == "true":
- return True
- if name == "false":
- return False
- if name == "nil":
- return NIL
- ctx_info = " → ".join(_aser_stack[-5:]) if _aser_stack else "(top)"
- raise EvalError(f"Undefined symbol: {name} [aser context: {ctx_info}]")
-
- if isinstance(expr, Keyword):
- return expr.name
-
- if isinstance(expr, dict):
- return {k: await _aser(v, env, ctx) for k, v in expr.items()}
-
- if not isinstance(expr, list):
- return expr
- if not expr:
- return []
-
- head = expr[0]
- if not isinstance(head, (Symbol, Lambda, list)):
- return [await _aser(x, env, ctx) for x in expr]
-
- if isinstance(head, Symbol):
- name = head.name
-
- # I/O primitives
- if name in IO_PRIMITIVES:
- args, kwargs = await _parse_io_args(expr[1:], env, ctx)
- return await execute_io(name, args, kwargs, ctx)
-
- # Fragment
- if name == "<>":
- return await _aser_fragment(expr[1:], env, ctx)
-
- # raw!
- if name == "raw!":
- return await _aser_call("raw!", expr[1:], env, ctx)
-
- # html: prefix
- if name.startswith("html:"):
- return await _aser_call(name[5:], expr[1:], env, ctx)
-
- # Component / Island call
- if name.startswith("~"):
- val = env.get(name)
- if isinstance(val, Macro):
- expanded = sx_ref.trampoline(
- sx_ref.expand_macro(val, expr[1:], env)
- )
- return await _aser(expanded, env, ctx)
- if isinstance(val, Component) and (
- _expand_components.get()
- or getattr(val, "render_target", None) == "server"
- ):
- return await _aser_component(val, expr[1:], env, ctx)
- return await _aser_call(name, expr[1:], env, ctx)
-
- # Serialize-mode special/HO forms
- sf = _ASER_FORMS.get(name)
- if sf is not None:
- if name in HTML_TAGS and (
- (len(expr) > 1 and isinstance(expr[1], Keyword))
- or _svg_context.get(False)
- ):
- return await _aser_call(name, expr[1:], env, ctx)
- return await sf(expr, env, ctx)
-
- # HTML tag
- if name in HTML_TAGS:
- return await _aser_call(name, expr[1:], env, ctx)
-
- # Macro
- if name in env:
- val = env[name]
- if isinstance(val, Macro):
- expanded = sx_ref.trampoline(
- sx_ref.expand_macro(val, expr[1:], env)
- )
- return await _aser(expanded, env, ctx)
-
- # Custom element
- if "-" in name and len(expr) > 1 and isinstance(expr[1], Keyword):
- return await _aser_call(name, expr[1:], env, ctx)
-
- # SVG context
- if _svg_context.get(False):
- return await _aser_call(name, expr[1:], env, ctx)
-
- # Function/lambda call — fallback: evaluate head as callable
- fn = await async_eval(head, env, ctx)
- args = [await async_eval(a, env, ctx) for a in expr[1:]]
-
- if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
- result = fn(*args)
- if inspect.iscoroutine(result):
- return await result
- return result
- if isinstance(fn, Lambda):
- local = dict(fn.closure)
- local.update(env)
- for p, v in zip(fn.params, args):
- local[p] = v
- return await _aser(fn.body, local, ctx)
- if isinstance(fn, Component):
- return await _aser_call(f"~{fn.name}", expr[1:], env, ctx)
- if isinstance(fn, Island):
- return await _aser_call(f"~{fn.name}", expr[1:], env, ctx)
- raise EvalError(f"Not callable in aser: {fn!r} (expr head: {head!r})")
-
-
-async def _aser_fragment(children, env, ctx):
- parts = []
- for child in children:
- result = await _aser(child, env, ctx)
- if isinstance(result, list):
- for item in result:
- if item is not NIL and item is not None:
- parts.append(serialize(item))
- elif result is not NIL and result is not None:
- parts.append(serialize(result))
- if not parts:
- return SxExpr("")
- return SxExpr("(<> " + " ".join(parts) + ")")
-
-
-async def _aser_component(comp, args, env, ctx):
- _aser_stack.append(f"~{comp.name}")
- try:
- kwargs = {}
- children = []
- i = 0
- while i < len(args):
- arg = args[i]
- if isinstance(arg, Keyword) and i + 1 < len(args):
- kwargs[arg.name] = await _aser(args[i + 1], env, ctx)
- i += 2
- else:
- children.append(arg)
- i += 1
- local = dict(comp.closure)
- local.update(env)
- for p in comp.params:
- local[p] = kwargs.get(p, NIL)
- if comp.has_children:
- child_parts = []
- for c in children:
- result = await _aser(c, env, ctx)
- if isinstance(result, list):
- for item in result:
- if item is not NIL and item is not None:
- child_parts.append(serialize(item))
- elif result is not NIL and result is not None:
- child_parts.append(serialize(result))
- local["children"] = SxExpr("(<> " + " ".join(child_parts) + ")")
- return await _aser(comp.body, local, ctx)
- finally:
- _aser_stack.pop()
-
-
-async def _aser_call(name, args, env, ctx):
- _aser_stack.append(name)
- token = None
- if name in ("svg", "math"):
- token = _svg_context.set(True)
- try:
- parts = [name]
- extra_class = None
- i = 0
- while i < len(args):
- arg = args[i]
- if isinstance(arg, Keyword) and i + 1 < len(args):
- val = await _aser(args[i + 1], env, ctx)
- if val is not NIL and val is not None:
- parts.append(f":{arg.name}")
- if isinstance(val, list):
- live = [v for v in val if v is not NIL and v is not None]
- items = [serialize(v) for v in live]
- if not items:
- parts.append("nil")
- elif any(isinstance(v, SxExpr) for v in live):
- parts.append("(<> " + " ".join(items) + ")")
- else:
- parts.append("(list " + " ".join(items) + ")")
- else:
- parts.append(serialize(val))
- i += 2
- else:
- result = await _aser(arg, env, ctx)
- if result is not NIL and result is not None:
- if isinstance(result, list):
- for item in result:
- if item is not NIL and item is not None:
- parts.append(serialize(item))
- else:
- parts.append(serialize(result))
- i += 1
- if extra_class:
- _merge_class_into_parts(parts, extra_class)
- return SxExpr("(" + " ".join(parts) + ")")
- finally:
- _aser_stack.pop()
- if token is not None:
- _svg_context.reset(token)
-
-
-def _merge_class_into_parts(parts, class_name):
- for i, p in enumerate(parts):
- if p == ":class" and i + 1 < len(parts):
- existing = parts[i + 1]
- if existing.startswith('"') and existing.endswith('"'):
- parts[i + 1] = existing[:-1] + " " + class_name + '"'
- else:
- parts[i + 1] = f'(str {existing} " {class_name}")'
- return
- parts.insert(1, f'"{class_name}"')
- parts.insert(1, ":class")
-
-
-# ---------------------------------------------------------------------------
-# Aser-mode special forms
-# ---------------------------------------------------------------------------
-
-async def _assf_if(expr, env, ctx):
- cond = await async_eval(expr[1], env, ctx)
- if cond and cond is not NIL:
- return await _aser(expr[2], env, ctx)
- return await _aser(expr[3], env, ctx) if len(expr) > 3 else NIL
-
-
-async def _assf_when(expr, env, ctx):
- cond = await async_eval(expr[1], env, ctx)
- if cond and cond is not NIL:
- result = NIL
- for body_expr in expr[2:]:
- result = await _aser(body_expr, env, ctx)
- return result
- return NIL
-
-
-async def _assf_let(expr, env, ctx):
- bindings = expr[1]
- local = dict(env)
- if isinstance(bindings, list):
- if bindings and isinstance(bindings[0], list):
- for b in bindings:
- var = b[0]
- vname = var.name if isinstance(var, Symbol) else var
- local[vname] = await _aser(b[1], local, ctx)
- elif len(bindings) % 2 == 0:
- for i in range(0, len(bindings), 2):
- var = bindings[i]
- vname = var.name if isinstance(var, Symbol) else var
- local[vname] = await _aser(bindings[i + 1], local, ctx)
- result = NIL
- for body_expr in expr[2:]:
- result = await _aser(body_expr, local, ctx)
- return result
-
-
-async def _assf_cond(expr, env, ctx):
- clauses = expr[1:]
- if not clauses:
- return NIL
- if isinstance(clauses[0], list) and len(clauses[0]) == 2:
- for clause in clauses:
- test = clause[0]
- if isinstance(test, Symbol) and test.name in ("else", ":else"):
- return await _aser(clause[1], env, ctx)
- if isinstance(test, Keyword) and test.name == "else":
- return await _aser(clause[1], env, ctx)
- if await async_eval(test, env, ctx):
- return await _aser(clause[1], env, ctx)
- else:
- i = 0
- while i < len(clauses) - 1:
- test, result = clauses[i], clauses[i + 1]
- if isinstance(test, Keyword) and test.name == "else":
- return await _aser(result, env, ctx)
- if isinstance(test, Symbol) and test.name in (":else", "else"):
- return await _aser(result, env, ctx)
- if await async_eval(test, env, ctx):
- return await _aser(result, env, ctx)
- i += 2
- return NIL
-
-
-async def _assf_case(expr, env, ctx):
- match_val = await async_eval(expr[1], env, ctx)
- clauses = expr[2:]
- i = 0
- while i < len(clauses) - 1:
- test, result = clauses[i], clauses[i + 1]
- if isinstance(test, Keyword) and test.name == "else":
- return await _aser(result, env, ctx)
- if isinstance(test, Symbol) and test.name in (":else", "else"):
- return await _aser(result, env, ctx)
- if match_val == await async_eval(test, env, ctx):
- return await _aser(result, env, ctx)
- i += 2
- return NIL
-
-
-async def _assf_begin(expr, env, ctx):
- result = NIL
- for sub in expr[1:]:
- result = await _aser(sub, env, ctx)
- return result
-
-
-async def _assf_define(expr, env, ctx):
- await async_eval(expr, env, ctx)
- return NIL
-
-
-async def _assf_and(expr, env, ctx):
- result = True
- for arg in expr[1:]:
- result = await async_eval(arg, env, ctx)
- if not result:
- return result
- return result
-
-
-async def _assf_or(expr, env, ctx):
- result = False
- for arg in expr[1:]:
- result = await async_eval(arg, env, ctx)
- if result:
- return result
- return result
-
-
-async def _assf_lambda(expr, env, ctx):
- params_expr = expr[1]
- param_names = []
- for p in params_expr:
- if isinstance(p, Symbol):
- param_names.append(p.name)
- elif isinstance(p, str):
- param_names.append(p)
- return Lambda(param_names, expr[2], dict(env))
-
-
-async def _assf_quote(expr, env, ctx):
- return expr[1] if len(expr) > 1 else NIL
-
-
-async def _assf_thread_first(expr, env, ctx):
- result = await async_eval(expr[1], env, ctx)
- for form in expr[2:]:
- if isinstance(form, list):
- fn = await async_eval(form[0], env, ctx)
- fn_args = [result] + [await async_eval(a, env, ctx) for a in form[1:]]
- else:
- fn = await async_eval(form, env, ctx)
- fn_args = [result]
- if callable(fn) and not isinstance(fn, (Lambda, Component, Island)):
- result = fn(*fn_args)
- if inspect.iscoroutine(result):
- result = await result
- elif isinstance(fn, Lambda):
- local = dict(fn.closure)
- local.update(env)
- for p, v in zip(fn.params, fn_args):
- local[p] = v
- result = await async_eval(fn.body, local, ctx)
- else:
- raise EvalError(f"-> form not callable: {fn!r}")
- return result
-
-
-async def _assf_set_bang(expr, env, ctx):
- value = await async_eval(expr[2], env, ctx)
- env[expr[1].name] = value
- return value
-
-
-# Aser-mode HO forms
-
-async def _asho_map(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- results = []
- for item in coll:
- if isinstance(fn, Lambda):
- local = dict(fn.closure)
- local.update(env)
- local[fn.params[0]] = item
- results.append(await _aser(fn.body, local, ctx))
- elif callable(fn):
- r = fn(item)
- results.append(await r if inspect.iscoroutine(r) else r)
- else:
- raise EvalError(f"map requires callable, got {type(fn).__name__}")
- return results
-
-
-async def _asho_map_indexed(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- results = []
- for i, item in enumerate(coll):
- if isinstance(fn, Lambda):
- local = dict(fn.closure)
- local.update(env)
- local[fn.params[0]] = i
- local[fn.params[1]] = item
- results.append(await _aser(fn.body, local, ctx))
- elif callable(fn):
- r = fn(i, item)
- results.append(await r if inspect.iscoroutine(r) else r)
- else:
- raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}")
- return results
-
-
-async def _asho_filter(expr, env, ctx):
- return await async_eval(expr, env, ctx)
-
-
-async def _asho_for_each(expr, env, ctx):
- fn = await async_eval(expr[1], env, ctx)
- coll = await async_eval(expr[2], env, ctx)
- results = []
- for item in coll:
- if isinstance(fn, Lambda):
- local = dict(fn.closure)
- local.update(env)
- local[fn.params[0]] = item
- results.append(await _aser(fn.body, local, ctx))
- elif callable(fn):
- r = fn(item)
- results.append(await r if inspect.iscoroutine(r) else r)
- return results
-
-
-_ASER_FORMS = {
- "if": _assf_if,
- "when": _assf_when,
- "cond": _assf_cond,
- "case": _assf_case,
- "and": _assf_and,
- "or": _assf_or,
- "let": _assf_let,
- "let*": _assf_let,
- "lambda": _assf_lambda,
- "fn": _assf_lambda,
- "define": _assf_define,
- "defstyle": _assf_define,
- "defcomp": _assf_define,
- "defmacro": _assf_define,
- "defhandler": _assf_define,
- "defisland": _assf_define,
- "begin": _assf_begin,
- "do": _assf_begin,
- "quote": _assf_quote,
- "->": _assf_thread_first,
- "set!": _assf_set_bang,
- "map": _asho_map,
- "map-indexed": _asho_map_indexed,
- "filter": _asho_filter,
- "for-each": _asho_for_each,
-}
+async_eval = sx_ref.async_eval
+async_render = sx_ref.async_render
+async_eval_to_sx = sx_ref.async_eval_to_sx
+async_eval_slot_to_sx = sx_ref.async_eval_slot_to_sx
diff --git a/shared/sx/ref/boot.sx b/shared/sx/ref/boot.sx
index afb9d55..30a2e63 100644
--- a/shared/sx/ref/boot.sx
+++ b/shared/sx/ref/boot.sx
@@ -344,15 +344,15 @@
(define hydrate-island
(fn (el)
(let ((name (dom-get-attr el "data-sx-island"))
- (state-json (or (dom-get-attr el "data-sx-state") "{}")))
+ (state-sx (or (dom-get-attr el "data-sx-state") "{}")))
(let ((comp-name (str "~" name))
(env (get-render-env nil)))
(let ((comp (env-get env comp-name)))
(if (not (or (component? comp) (island? comp)))
(log-warn (str "hydrate-island: unknown island " comp-name))
- ;; Parse state and build keyword args
- (let ((kwargs (json-parse state-json))
+ ;; Parse state and build keyword args — SX format, not JSON
+ (let ((kwargs (or (first (sx-parse state-sx)) {}))
(disposers (list))
(local (env-merge (component-closure comp) env)))
@@ -494,8 +494,8 @@
;; (log-info msg) → void (console.log with prefix)
;; (log-parse-error label text err) → void (diagnostic parse error)
;;
-;; === JSON ===
-;; (json-parse str) → dict/list/value (JSON.parse)
+;; === Parsing (island state) ===
+;; (sx-parse str) → list of AST expressions (from parser.sx)
;;
;; === Processing markers ===
;; (mark-processed! el key) → void
diff --git a/shared/sx/ref/bootstrap_py.py b/shared/sx/ref/bootstrap_py.py
index 875b381..2a16806 100644
--- a/shared/sx/ref/bootstrap_py.py
+++ b/shared/sx/ref/bootstrap_py.py
@@ -49,6 +49,8 @@ class PyEmitter:
def __init__(self):
self.indent = 0
+ self._async_names: set[str] = set() # SX names of define-async functions
+ self._in_async: bool = False # Currently emitting async def body?
def emit(self, expr) -> str:
"""Emit a Python expression from an SX AST node."""
@@ -80,6 +82,8 @@ class PyEmitter:
name = head.name
if name == "define":
return self._emit_define(expr, indent)
+ if name == "define-async":
+ return self._emit_define_async(expr, indent)
if name == "set!":
return f"{pad}{self._mangle(expr[1].name)} = {self.emit(expr[2])}"
if name == "when":
@@ -275,6 +279,19 @@ class PyEmitter:
"sf-defisland": "sf_defisland",
# adapter-sx.sx
"render-to-sx": "render_to_sx",
+ # adapter-async.sx platform primitives
+ "svg-context-set!": "svg_context_set",
+ "svg-context-reset!": "svg_context_reset",
+ "css-class-collect!": "css_class_collect",
+ "is-raw-html?": "is_raw_html",
+ "async-coroutine?": "is_async_coroutine",
+ "async-await!": "async_await",
+ "is-sx-expr?": "is_sx_expr",
+ "sx-expr?": "is_sx_expr",
+ "io-primitive?": "io_primitive_p",
+ "expand-components?": "expand_components_p",
+ "svg-context?": "svg_context_p",
+ "make-sx-expr": "make_sx_expr",
"aser": "aser",
"eval-case-aser": "eval_case_aser",
"sx-serialize": "sx_serialize",
@@ -417,6 +434,8 @@ class PyEmitter:
# Regular function call
fn_name = self._mangle(name)
args = ", ".join(self.emit(x) for x in expr[1:])
+ if self._in_async and name in self._async_names:
+ return f"(await {fn_name}({args}))"
return f"{fn_name}({args})"
# --- Special form emitters ---
@@ -513,7 +532,7 @@ class PyEmitter:
body_parts = expr[2:]
lines = [f"{pad}if sx_truthy({cond}):"]
for b in body_parts:
- lines.append(self.emit_statement(b, indent + 1))
+ self._emit_stmt_recursive(b, lines, indent + 1)
return "\n".join(lines)
def _emit_cond(self, expr) -> str:
@@ -642,6 +661,16 @@ class PyEmitter:
val = self.emit(val_expr)
return f"{pad}{self._mangle(name)} = {val}"
+ def _emit_define_async(self, expr, indent: int = 0) -> str:
+ """Emit a define-async form as an async def statement."""
+ name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
+ val_expr = expr[2]
+ if (isinstance(val_expr, list) and val_expr and
+ isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")):
+ return self._emit_define_as_def(name, val_expr, indent, is_async=True)
+ # Shouldn't happen — define-async should always wrap fn/lambda
+ return self._emit_define(expr, indent)
+
def _body_uses_set(self, fn_expr) -> bool:
"""Check if a fn expression's body (recursively) uses set!."""
def _has_set(node):
@@ -654,12 +683,16 @@ class PyEmitter:
body = fn_expr[2:]
return any(_has_set(b) for b in body)
- def _emit_define_as_def(self, name: str, fn_expr, indent: int = 0) -> str:
+ def _emit_define_as_def(self, name: str, fn_expr, indent: int = 0,
+ is_async: bool = False) -> str:
"""Emit a define with fn value as a proper def statement.
This is used for functions that contain set! — Python closures can't
rebind outer lambda params, so we need proper def + local variables.
Variables mutated by set! from nested lambdas use a _cells dict.
+
+ When is_async=True, emits 'async def' and sets _in_async so that
+ calls to other async functions receive 'await'.
"""
pad = " " * indent
params = fn_expr[1]
@@ -686,14 +719,19 @@ class PyEmitter:
py_name = self._mangle(name)
# Find set! target variables that are used from nested lambda scopes
nested_set_vars = self._find_nested_set_vars(body)
- lines = [f"{pad}def {py_name}({params_str}):"]
+ def_kw = "async def" if is_async else "def"
+ lines = [f"{pad}{def_kw} {py_name}({params_str}):"]
if nested_set_vars:
lines.append(f"{pad} _cells = {{}}")
- # Emit body with cell var tracking
+ # Emit body with cell var tracking (and async context if needed)
old_cells = getattr(self, '_current_cell_vars', set())
+ old_async = self._in_async
self._current_cell_vars = nested_set_vars
+ if is_async:
+ self._in_async = True
self._emit_body_stmts(body, lines, indent + 1)
self._current_cell_vars = old_cells
+ self._in_async = old_async
return "\n".join(lines)
def _find_nested_set_vars(self, body) -> set[str]:
@@ -750,7 +788,7 @@ class PyEmitter:
if is_last:
self._emit_return_expr(expr, lines, indent)
else:
- lines.append(self.emit_statement(expr, indent))
+ self._emit_stmt_recursive(expr, lines, indent)
def _emit_return_expr(self, expr, lines: list, indent: int) -> None:
"""Emit an expression in return position, flattening control flow."""
@@ -775,6 +813,11 @@ class PyEmitter:
if name in ("do", "begin"):
self._emit_body_stmts(expr[1:], lines, indent)
return
+ if name == "for-each":
+ # for-each in return position: emit as statement, return NIL
+ lines.append(self._emit_for_each_stmt(expr, indent))
+ lines.append(f"{pad}return NIL")
+ return
lines.append(f"{pad}return {self.emit(expr)}")
def _emit_if_return(self, expr, lines: list, indent: int) -> None:
@@ -1034,12 +1077,15 @@ class PyEmitter:
# ---------------------------------------------------------------------------
def extract_defines(source: str) -> list[tuple[str, list]]:
- """Parse .sx source, return list of (name, define-expr) for top-level defines."""
+ """Parse .sx source, return list of (name, define-expr) for top-level defines.
+
+ Extracts both (define ...) and (define-async ...) forms.
+ """
exprs = parse_all(source)
defines = []
for expr in exprs:
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
- if expr[0].name == "define":
+ if expr[0].name in ("define", "define-async"):
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
defines.append((name, expr))
return defines
@@ -1212,6 +1258,28 @@ def compile_ref_to_py(
for name in sorted(spec_mod_set):
sx_files.append(SPEC_MODULES[name])
+ # Pre-scan define-async names (needed before transpilation so emitter
+ # knows which calls require 'await')
+ has_async = "async" in adapter_set
+ if has_async:
+ async_filename = ADAPTER_FILES["async"][0]
+ async_filepath = os.path.join(ref_dir, async_filename)
+ if os.path.exists(async_filepath):
+ with open(async_filepath) as f:
+ async_src = f.read()
+ for aexpr in parse_all(async_src):
+ if (isinstance(aexpr, list) and aexpr
+ and isinstance(aexpr[0], Symbol)
+ and aexpr[0].name == "define-async"):
+ aname = aexpr[1].name if isinstance(aexpr[1], Symbol) else str(aexpr[1])
+ emitter._async_names.add(aname)
+ # Platform async primitives (provided by host, also need await)
+ emitter._async_names.update({
+ "async-eval", "execute-io", "async-await!",
+ })
+ # Async adapter is transpiled last (after sync adapters)
+ sx_files.append(ADAPTER_FILES["async"])
+
all_sections = []
for filename, label in sx_files:
filepath = os.path.join(ref_dir, filename)
@@ -1248,6 +1316,9 @@ def compile_ref_to_py(
if has_deps:
parts.append(PLATFORM_DEPS_PY)
+ if has_async:
+ parts.append(PLATFORM_ASYNC_PY)
+
for label, defines in all_sections:
parts.append(f"\n# === Transpiled from {label} ===\n")
for name, expr in defines:
@@ -1258,7 +1329,7 @@ def compile_ref_to_py(
parts.append(FIXUPS_PY)
if has_continuations:
parts.append(CONTINUATIONS_PY)
- parts.append(public_api_py(has_html, has_sx, has_deps))
+ parts.append(public_api_py(has_html, has_sx, has_deps, has_async))
return "\n".join(parts)
diff --git a/shared/sx/ref/js.sx b/shared/sx/ref/js.sx
index cea6379..3c35b73 100644
--- a/shared/sx/ref/js.sx
+++ b/shared/sx/ref/js.sx
@@ -1290,8 +1290,9 @@
(= name "append!")
(str (js-expr (nth expr 1)) ".push(" (js-expr (nth expr 2)) ");")
(= name "env-set!")
- (str (js-expr (nth expr 1)) "[" (js-expr (nth expr 2))
- "] = " (js-expr (nth expr 3)) ";")
+ (str "envSet(" (js-expr (nth expr 1))
+ ", " (js-expr (nth expr 2))
+ ", " (js-expr (nth expr 3)) ");")
(= name "set-lambda-name!")
(str (js-expr (nth expr 1)) ".name = " (js-expr (nth expr 2)) ";")
:else
diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py
index 9dd2881..ad5e759 100644
--- a/shared/sx/ref/platform_js.py
+++ b/shared/sx/ref/platform_js.py
@@ -1194,7 +1194,7 @@ PLATFORM_JS_PRE = '''
// JSON / dict helpers for island state serialization
function jsonSerialize(obj) {
- try { return JSON.stringify(obj); } catch(e) { return "{}"; }
+ return JSON.stringify(obj);
}
function isEmptyDict(d) {
if (!d || typeof d !== "object") return true;
@@ -1204,11 +1204,34 @@ PLATFORM_JS_PRE = '''
function envHas(env, name) { return name in env; }
function envGet(env, name) { return env[name]; }
- function envSet(env, name, val) { env[name] = val; }
+ function envSet(env, name, val) {
+ // Walk prototype chain to find where the variable is defined (for set!)
+ var obj = env;
+ while (obj !== null && obj !== Object.prototype) {
+ if (obj.hasOwnProperty(name)) { obj[name] = val; return; }
+ obj = Object.getPrototypeOf(obj);
+ }
+ // Not found in any parent scope — set on the immediate env
+ env[name] = val;
+ }
function envExtend(env) { return Object.create(env); }
function envMerge(base, overlay) {
+ // Same env or overlay is descendant of base — just extend, no copy.
+ // This prevents set! inside lambdas from modifying shadow copies.
+ if (base === overlay) return Object.create(base);
+ var p = overlay;
+ for (var d = 0; p && p !== Object.prototype && d < 100; d++) {
+ if (p === base) return Object.create(base);
+ p = Object.getPrototypeOf(p);
+ }
+ // General case: extend base, copy ONLY overlay properties that don't
+ // exist in the base chain (avoids shadowing closure bindings).
var child = Object.create(base);
- if (overlay) for (var k in overlay) if (overlay.hasOwnProperty(k)) child[k] = overlay[k];
+ if (overlay) {
+ for (var k in overlay) {
+ if (overlay.hasOwnProperty(k) && !(k in base)) child[k] = overlay[k];
+ }
+ }
return child;
}
@@ -1649,8 +1672,11 @@ PLATFORM_DOM_JS = """
function domListen(el, name, handler) {
if (!_hasDom || !el) return function() {};
// Wrap SX lambdas from runtime-evaluated island code into native fns
+ // If lambda takes 0 params, call without event arg (convenience for on-click handlers)
var wrapped = isLambda(handler)
- ? function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
+ ? (lambdaParams(handler).length === 0
+ ? function(e) { try { invoke(handler); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
+ : function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
: handler;
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
el.addEventListener(name, wrapped);
diff --git a/shared/sx/ref/platform_py.py b/shared/sx/ref/platform_py.py
index de9794f..345bb67 100644
--- a/shared/sx/ref/platform_py.py
+++ b/shared/sx/ref/platform_py.py
@@ -462,10 +462,7 @@ def invoke(f, *args):
def json_serialize(obj):
import json
- try:
- return json.dumps(obj)
- except (TypeError, ValueError):
- return "{}"
+ return json.dumps(obj)
def is_empty_dict(d):
@@ -1067,10 +1064,19 @@ import inspect
from shared.sx.primitives_io import (
IO_PRIMITIVES, RequestContext, execute_io,
- css_class_collector as _css_class_collector_cv,
- _svg_context as _svg_context_cv,
)
+# Lazy imports to avoid circular dependency (html.py imports sx_ref.py)
+_css_class_collector_cv = None
+_svg_context_cv = None
+
+def _ensure_html_imports():
+ global _css_class_collector_cv, _svg_context_cv
+ if _css_class_collector_cv is None:
+ from shared.sx.html import css_class_collector, _svg_context
+ _css_class_collector_cv = css_class_collector
+ _svg_context_cv = _svg_context
+
# When True, async_aser expands known components server-side
_expand_components_cv: contextvars.ContextVar[bool] = contextvars.ContextVar(
"_expand_components_ref", default=False
@@ -1094,18 +1100,22 @@ def expand_components_p():
def svg_context_p():
+ _ensure_html_imports()
return _svg_context_cv.get(False)
def svg_context_set(val):
+ _ensure_html_imports()
return _svg_context_cv.set(val)
def svg_context_reset(token):
+ _ensure_html_imports()
_svg_context_cv.reset(token)
def css_class_collect(val):
+ _ensure_html_imports()
collector = _css_class_collector_cv.get(None)
if collector is not None:
collector.update(str(val).split())
@@ -1123,6 +1133,25 @@ def is_sx_expr(x):
return isinstance(x, SxExpr)
+# Predicate helpers used by adapter-async (these are in PRIMITIVES but
+# the bootstrapped code calls them as plain functions)
+def string_p(x):
+ return isinstance(x, str)
+
+
+def list_p(x):
+ return isinstance(x, _b_list)
+
+
+def number_p(x):
+ return isinstance(x, (int, float)) and not isinstance(x, bool)
+
+
+def sx_parse(src):
+ from shared.sx.parser import parse_all
+ return parse_all(src)
+
+
def is_async_coroutine(x):
return inspect.iscoroutine(x)
@@ -1199,48 +1228,16 @@ async def async_eval_slot_to_sx(expr, env, ctx=None):
ctx = RequestContext()
token = _expand_components_cv.set(True)
try:
- return await _eval_slot_inner(expr, env, ctx)
+ result = await async_eval_slot_inner(expr, env, ctx)
+ if isinstance(result, SxExpr):
+ return result
+ if result is None or result is NIL:
+ return SxExpr("")
+ if isinstance(result, str):
+ return SxExpr(result)
+ return SxExpr(sx_serialize(result))
finally:
_expand_components_cv.reset(token)
-
-
-async def _eval_slot_inner(expr, env, ctx):
- if isinstance(expr, list) and expr:
- head = expr[0]
- if isinstance(head, Symbol) and head.name.startswith("~"):
- comp = env.get(head.name)
- if isinstance(comp, Component):
- result = await async_aser_component(comp, expr[1:], env, ctx)
- if isinstance(result, SxExpr):
- return result
- if result is None or result is NIL:
- return SxExpr("")
- if isinstance(result, str):
- return SxExpr(result)
- return SxExpr(sx_serialize(result))
- result = await async_aser(expr, env, ctx)
- result = await _maybe_expand_component_result(result, env, ctx)
- if isinstance(result, SxExpr):
- return result
- if result is None or result is NIL:
- return SxExpr("")
- if isinstance(result, str):
- return SxExpr(result)
- return SxExpr(sx_serialize(result))
-
-
-async def _maybe_expand_component_result(result, env, ctx):
- raw = None
- if isinstance(result, SxExpr):
- raw = str(result).strip()
- elif isinstance(result, str):
- raw = result.strip()
- if raw and raw.startswith("(~"):
- from shared.sx.parser import parse_all as _pa
- parsed = _pa(raw)
- if parsed:
- return await async_eval_slot_to_sx(parsed[0], env, ctx)
- return result
'''
# ---------------------------------------------------------------------------
@@ -1366,7 +1363,8 @@ aser_special = _aser_special_with_continuations
# Public API generator
# ---------------------------------------------------------------------------
-def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str:
+def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False,
+ has_async: bool = False) -> str:
lines = [
'',
'# =========================================================================',
@@ -1419,8 +1417,9 @@ def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str:
# ---------------------------------------------------------------------------
ADAPTER_FILES = {
- "html": ("adapter-html.sx", "adapter-html"),
- "sx": ("adapter-sx.sx", "adapter-sx"),
+ "html": ("adapter-html.sx", "adapter-html"),
+ "sx": ("adapter-sx.sx", "adapter-sx"),
+ "async": ("adapter-async.sx", "adapter-async"),
}
SPEC_MODULES = {
diff --git a/shared/sx/ref/render.sx b/shared/sx/ref/render.sx
index 66384c5..e6793ca 100644
--- a/shared/sx/ref/render.sx
+++ b/shared/sx/ref/render.sx
@@ -178,7 +178,9 @@
;; bindings = ((name1 expr1) (name2 expr2) ...)
(define process-bindings
(fn (bindings env)
- (let ((local (merge env)))
+ ;; env-extend (not merge) — Env is not a dict subclass, so merge()
+ ;; returns an empty dict, losing all parent scope bindings.
+ (let ((local (env-extend env)))
(for-each
(fn (pair)
(when (and (= (type-of pair) "list") (>= (len pair) 2))
diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py
index 7512091..f7bc12a 100644
--- a/shared/sx/ref/sx_ref.py
+++ b/shared/sx/ref/sx_ref.py
@@ -421,10 +421,7 @@ def invoke(f, *args):
def json_serialize(obj):
import json
- try:
- return json.dumps(obj)
- except (TypeError, ValueError):
- return "{}"
+ return json.dumps(obj)
def is_empty_dict(d):
@@ -968,6 +965,191 @@ def component_set_io_refs(c, refs):
c.io_refs = set(refs) if not isinstance(refs, set) else refs
+# =========================================================================
+# Platform interface -- Async adapter
+# =========================================================================
+
+import contextvars
+import inspect
+
+from shared.sx.primitives_io import (
+ IO_PRIMITIVES, RequestContext, execute_io,
+)
+
+# Lazy imports to avoid circular dependency (html.py imports sx_ref.py)
+_css_class_collector_cv = None
+_svg_context_cv = None
+
+def _ensure_html_imports():
+ global _css_class_collector_cv, _svg_context_cv
+ if _css_class_collector_cv is None:
+ from shared.sx.html import css_class_collector, _svg_context
+ _css_class_collector_cv = css_class_collector
+ _svg_context_cv = _svg_context
+
+# When True, async_aser expands known components server-side
+_expand_components_cv: contextvars.ContextVar[bool] = contextvars.ContextVar(
+ "_expand_components_ref", default=False
+)
+
+
+class _AsyncThunk:
+ __slots__ = ("expr", "env", "ctx")
+ def __init__(self, expr, env, ctx):
+ self.expr = expr
+ self.env = env
+ self.ctx = ctx
+
+
+def io_primitive_p(name):
+ return name in IO_PRIMITIVES
+
+
+def expand_components_p():
+ return _expand_components_cv.get()
+
+
+def svg_context_p():
+ _ensure_html_imports()
+ return _svg_context_cv.get(False)
+
+
+def svg_context_set(val):
+ _ensure_html_imports()
+ return _svg_context_cv.set(val)
+
+
+def svg_context_reset(token):
+ _ensure_html_imports()
+ _svg_context_cv.reset(token)
+
+
+def css_class_collect(val):
+ _ensure_html_imports()
+ collector = _css_class_collector_cv.get(None)
+ if collector is not None:
+ collector.update(str(val).split())
+
+
+def is_raw_html(x):
+ return isinstance(x, _RawHTML)
+
+
+def make_sx_expr(s):
+ return SxExpr(s)
+
+
+def is_sx_expr(x):
+ return isinstance(x, SxExpr)
+
+
+# Predicate helpers used by adapter-async (these are in PRIMITIVES but
+# the bootstrapped code calls them as plain functions)
+def string_p(x):
+ return isinstance(x, str)
+
+
+def list_p(x):
+ return isinstance(x, _b_list)
+
+
+def number_p(x):
+ return isinstance(x, (int, float)) and not isinstance(x, bool)
+
+
+def sx_parse(src):
+ from shared.sx.parser import parse_all
+ return parse_all(src)
+
+
+def is_async_coroutine(x):
+ return inspect.iscoroutine(x)
+
+
+async def async_await(x):
+ return await x
+
+
+async def _async_trampoline(val):
+ while isinstance(val, _AsyncThunk):
+ val = await async_eval(val.expr, val.env, val.ctx)
+ return val
+
+
+async def async_eval(expr, env, ctx=None):
+ """Evaluate with I/O primitives. Entry point for async evaluation."""
+ if ctx is None:
+ ctx = RequestContext()
+ result = await _async_eval_inner(expr, env, ctx)
+ while isinstance(result, _AsyncThunk):
+ result = await _async_eval_inner(result.expr, result.env, result.ctx)
+ return result
+
+
+async def _async_eval_inner(expr, env, ctx):
+ """Intercept I/O primitives, delegate everything else to sync eval."""
+ if isinstance(expr, list) and expr:
+ head = expr[0]
+ if isinstance(head, Symbol) and head.name in IO_PRIMITIVES:
+ args_list, kwargs = await _parse_io_args(expr[1:], env, ctx)
+ return await execute_io(head.name, args_list, kwargs, ctx)
+ is_render = isinstance(expr, list) and is_render_expr(expr)
+ result = eval_expr(expr, env)
+ result = trampoline(result)
+ if is_render and isinstance(result, str):
+ return _RawHTML(result)
+ return result
+
+
+async def _parse_io_args(exprs, env, ctx):
+ """Parse and evaluate I/O node args (keyword + positional)."""
+ from shared.sx.types import Keyword as _Kw
+ args_list = []
+ kwargs = {}
+ i = 0
+ while i < len(exprs):
+ item = exprs[i]
+ if isinstance(item, _Kw) and i + 1 < len(exprs):
+ kwargs[item.name] = await async_eval(exprs[i + 1], env, ctx)
+ i += 2
+ else:
+ args_list.append(await async_eval(item, env, ctx))
+ i += 1
+ return args_list, kwargs
+
+
+async def async_eval_to_sx(expr, env, ctx=None):
+ """Evaluate and produce SX source string (wire format)."""
+ if ctx is None:
+ ctx = RequestContext()
+ result = await async_aser(expr, env, ctx)
+ if isinstance(result, SxExpr):
+ return result
+ if result is None or result is NIL:
+ return SxExpr("")
+ if isinstance(result, str):
+ return SxExpr(result)
+ return SxExpr(sx_serialize(result))
+
+
+async def async_eval_slot_to_sx(expr, env, ctx=None):
+ """Like async_eval_to_sx but expands component calls server-side."""
+ if ctx is None:
+ ctx = RequestContext()
+ token = _expand_components_cv.set(True)
+ try:
+ result = await async_eval_slot_inner(expr, env, ctx)
+ if isinstance(result, SxExpr):
+ return result
+ if result is None or result is NIL:
+ return SxExpr("")
+ if isinstance(result, str):
+ return SxExpr(result)
+ return SxExpr(sx_serialize(result))
+ finally:
+ _expand_components_cv.reset(token)
+
+
# === Transpiled from eval ===
# trampoline
@@ -1267,7 +1449,13 @@ def sf_let(args, env):
bindings = first(args)
body = rest(args)
local = env_extend(env)
- (for_each(lambda binding: (lambda vname: _sx_dict_set(local, vname, trampoline(eval_expr(nth(binding, 1), local))))((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else (lambda i: reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_dict_set(local, vname, trampoline(eval_expr(val_expr, local))))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2))))(0))
+ if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))):
+ for binding in bindings:
+ vname = (symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))
+ local[vname] = trampoline(eval_expr(nth(binding, 1), local))
+ else:
+ i = 0
+ reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_dict_set(local, vname, trampoline(eval_expr(val_expr, local))))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2)))
for e in slice(body, 0, (len(body) - 1)):
trampoline(eval_expr(e, local))
return make_thunk(last(body), local)
@@ -1279,10 +1467,12 @@ def sf_named_let(args, env):
body = slice(args, 2)
params = []
inits = []
- (for_each(_sx_fn(lambda binding: (
- _sx_append(params, (symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))),
- _sx_append(inits, nth(binding, 1))
-)[-1]), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else reduce(lambda acc, pair_idx: _sx_begin(_sx_append(params, (symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), _sx_append(inits, nth(bindings, ((pair_idx * 2) + 1)))), NIL, range(0, (len(bindings) / 2))))
+ if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))):
+ for binding in bindings:
+ params.append((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding)))
+ inits.append(nth(binding, 1))
+ else:
+ reduce(lambda acc, pair_idx: _sx_begin(_sx_append(params, (symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), _sx_append(inits, nth(bindings, ((pair_idx * 2) + 1)))), NIL, range(0, (len(bindings) / 2)))
loop_body = (first(body) if sx_truthy((len(body) == 1)) else cons(make_symbol('begin'), body))
loop_fn = make_lambda(params, loop_body, env)
loop_fn.name = loop_name
@@ -1448,7 +1638,14 @@ def sf_letrec(args, env):
local = env_extend(env)
names = []
val_exprs = []
- (for_each(lambda binding: (lambda vname: _sx_begin(_sx_append(names, vname), _sx_append(val_exprs, nth(binding, 1)), _sx_dict_set(local, vname, NIL)))((symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))), bindings) if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))) else reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_begin(_sx_append(names, vname), _sx_append(val_exprs, val_expr), _sx_dict_set(local, vname, NIL)))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2))))
+ if sx_truthy(((type_of(first(bindings)) == 'list') if not sx_truthy((type_of(first(bindings)) == 'list')) else (len(first(bindings)) == 2))):
+ for binding in bindings:
+ vname = (symbol_name(first(binding)) if sx_truthy((type_of(first(binding)) == 'symbol')) else first(binding))
+ names.append(vname)
+ val_exprs.append(nth(binding, 1))
+ local[vname] = NIL
+ else:
+ reduce(lambda acc, pair_idx: (lambda vname: (lambda val_expr: _sx_begin(_sx_append(names, vname), _sx_append(val_exprs, val_expr), _sx_dict_set(local, vname, NIL)))(nth(bindings, ((pair_idx * 2) + 1))))((symbol_name(nth(bindings, (pair_idx * 2))) if sx_truthy((type_of(nth(bindings, (pair_idx * 2))) == 'symbol')) else nth(bindings, (pair_idx * 2)))), NIL, range(0, (len(bindings) / 2)))
values = map(lambda e: trampoline(eval_expr(e, local)), val_exprs)
for pair in zip(names, values):
local[first(pair)] = nth(pair, 1)
@@ -1531,7 +1728,9 @@ def ho_every(args, env):
def ho_for_each(args, env):
f = trampoline(eval_expr(first(args), env))
coll = trampoline(eval_expr(nth(args, 1), env))
- return for_each(lambda item: call_fn(f, [item], env), coll)
+ for item in coll:
+ call_fn(f, [item], env)
+ return NIL
# === Transpiled from forms (server definition forms) ===
@@ -1698,7 +1897,7 @@ def eval_cond_clojure(clauses, env):
# process-bindings
def process_bindings(bindings, env):
- local = merge(env)
+ local = env_extend(env)
for pair in bindings:
if sx_truthy(((type_of(pair) == 'list') if not sx_truthy((type_of(pair) == 'list')) else (len(pair) >= 2))):
name = (symbol_name(first(pair)) if sx_truthy((type_of(first(pair)) == 'symbol')) else sx_str(first(pair)))
@@ -1920,15 +2119,15 @@ def render_html_island(island, args, env):
if sx_truthy(component_has_children(island)):
local['children'] = make_raw_html(join('', map(lambda c: render_to_html(c, env), children)))
body_html = render_to_html(component_body(island), local)
- state_json = serialize_island_state(kwargs)
- return sx_str('', body_html, '')
+ state_sx = serialize_island_state(kwargs)
+ return sx_str('', body_html, '')
# serialize-island-state
def serialize_island_state(kwargs):
if sx_truthy(is_empty_dict(kwargs)):
return NIL
else:
- return json_serialize(kwargs)
+ return sx_serialize(kwargs)
# === Transpiled from adapter-sx ===
@@ -2195,9 +2394,13 @@ def scan_refs_walk(node, refs):
return NIL
return NIL
elif sx_truthy((type_of(node) == 'list')):
- return for_each(lambda item: scan_refs_walk(item, refs), node)
+ for item in node:
+ scan_refs_walk(item, refs)
+ return NIL
elif sx_truthy((type_of(node) == 'dict')):
- return for_each(lambda key: scan_refs_walk(dict_get(node, key), refs), keys(node))
+ for key in keys(node):
+ scan_refs_walk(dict_get(node, key), refs)
+ return NIL
else:
return NIL
@@ -2207,9 +2410,13 @@ def transitive_deps_walk(n, seen, env):
seen.append(n)
val = env_get(env, n)
if sx_truthy((type_of(val) == 'component')):
- return for_each(lambda ref: transitive_deps_walk(ref, seen, env), scan_refs(component_body(val)))
+ for ref in scan_refs(component_body(val)):
+ transitive_deps_walk(ref, seen, env)
+ return NIL
elif sx_truthy((type_of(val) == 'macro')):
- return for_each(lambda ref: transitive_deps_walk(ref, seen, env), scan_refs(macro_body(val)))
+ for ref in scan_refs(macro_body(val)):
+ transitive_deps_walk(ref, seen, env)
+ return NIL
else:
return NIL
return NIL
@@ -2223,7 +2430,11 @@ def transitive_deps(name, env):
# compute-all-deps
def compute_all_deps(env):
- return for_each(lambda name: (lambda val: (component_set_deps(val, transitive_deps(name, env)) if sx_truthy((type_of(val) == 'component')) else NIL))(env_get(env, name)), env_components(env))
+ for name in env_components(env):
+ val = env_get(env, name)
+ if sx_truthy((type_of(val) == 'component')):
+ component_set_deps(val, transitive_deps(name, env))
+ return NIL
# scan-components-from-source
def scan_components_from_source(source):
@@ -2273,9 +2484,13 @@ def scan_io_refs_walk(node, io_names, refs):
return NIL
return NIL
elif sx_truthy((type_of(node) == 'list')):
- return for_each(lambda item: scan_io_refs_walk(item, io_names, refs), node)
+ for item in node:
+ scan_io_refs_walk(item, io_names, refs)
+ return NIL
elif sx_truthy((type_of(node) == 'dict')):
- return for_each(lambda key: scan_io_refs_walk(dict_get(node, key), io_names, refs), keys(node))
+ for key in keys(node):
+ scan_io_refs_walk(dict_get(node, key), io_names, refs)
+ return NIL
else:
return NIL
@@ -2294,12 +2509,16 @@ def transitive_io_refs_walk(n, seen, all_refs, env, io_names):
for ref in scan_io_refs(component_body(val), io_names):
if sx_truthy((not sx_truthy(contains_p(all_refs, ref)))):
all_refs.append(ref)
- return for_each(lambda dep: transitive_io_refs_walk(dep, seen, all_refs, env, io_names), scan_refs(component_body(val)))
+ for dep in scan_refs(component_body(val)):
+ transitive_io_refs_walk(dep, seen, all_refs, env, io_names)
+ return NIL
elif sx_truthy((type_of(val) == 'macro')):
for ref in scan_io_refs(macro_body(val), io_names):
if sx_truthy((not sx_truthy(contains_p(all_refs, ref)))):
all_refs.append(ref)
- return for_each(lambda dep: transitive_io_refs_walk(dep, seen, all_refs, env, io_names), scan_refs(macro_body(val)))
+ for dep in scan_refs(macro_body(val)):
+ transitive_io_refs_walk(dep, seen, all_refs, env, io_names)
+ return NIL
else:
return NIL
return NIL
@@ -2314,7 +2533,11 @@ def transitive_io_refs(name, env, io_names):
# compute-all-io-refs
def compute_all_io_refs(env, io_names):
- return for_each(lambda name: (lambda val: (component_set_io_refs(val, transitive_io_refs(name, env, io_names)) if sx_truthy((type_of(val) == 'component')) else NIL))(env_get(env, name)), env_components(env))
+ for name in env_components(env):
+ val = env_get(env, name)
+ if sx_truthy((type_of(val) == 'component')):
+ component_set_io_refs(val, transitive_io_refs(name, env, io_names))
+ return NIL
# component-io-refs-cached
def component_io_refs_cached(name, env, io_names):
@@ -2608,7 +2831,9 @@ def batch(thunk):
if sx_truthy((not sx_truthy(contains_p(seen, sub)))):
seen.append(sub)
pending.append(sub)
- return for_each(lambda sub: sub(), pending)
+ for sub in pending:
+ sub()
+ return NIL
return NIL
# notify-subscribers
@@ -2622,7 +2847,9 @@ def notify_subscribers(s):
# flush-subscribers
def flush_subscribers(s):
- return for_each(lambda sub: sub(), signal_subscribers(s))
+ for sub in signal_subscribers(s):
+ sub()
+ return NIL
# dispose-computed
def dispose_computed(s):
@@ -2704,6 +2931,826 @@ def resource(fetch_fn):
return state
+# === Transpiled from adapter-async ===
+
+# async-render
+async def async_render(expr, env, ctx):
+ _match = type_of(expr)
+ if _match == 'nil':
+ return ''
+ elif _match == 'boolean':
+ return ''
+ elif _match == 'string':
+ return escape_html(expr)
+ elif _match == 'number':
+ return escape_html(sx_str(expr))
+ elif _match == 'raw-html':
+ return raw_html_content(expr)
+ elif _match == 'symbol':
+ val = (await async_eval(expr, env, ctx))
+ return (await async_render(val, env, ctx))
+ elif _match == 'keyword':
+ return escape_html(keyword_name(expr))
+ elif _match == 'list':
+ if sx_truthy(empty_p(expr)):
+ return ''
+ else:
+ return (await async_render_list(expr, env, ctx))
+ elif _match == 'dict':
+ return ''
+ else:
+ return escape_html(sx_str(expr))
+
+# async-render-list
+async def async_render_list(expr, env, ctx):
+ head = first(expr)
+ if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))):
+ if sx_truthy((is_lambda(head) if sx_truthy(is_lambda(head)) else (type_of(head) == 'list'))):
+ return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
+ else:
+ return join('', (await async_map_render(expr, env, ctx)))
+ else:
+ name = symbol_name(head)
+ args = rest(expr)
+ if sx_truthy(io_primitive_p(name)):
+ return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
+ elif sx_truthy((name == 'raw!')):
+ return (await async_render_raw(args, env, ctx))
+ elif sx_truthy((name == '<>')):
+ return join('', (await async_map_render(args, env, ctx)))
+ elif sx_truthy(starts_with_p(name, 'html:')):
+ return (await async_render_element(slice(name, 5), args, env, ctx))
+ elif sx_truthy(async_render_form_p(name)):
+ if sx_truthy((contains_p(HTML_TAGS, name) if not sx_truthy(contains_p(HTML_TAGS, name)) else (((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword')) if sx_truthy(((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword'))) else svg_context_p()))):
+ return (await async_render_element(name, args, env, ctx))
+ else:
+ return (await dispatch_async_render_form(name, expr, env, ctx))
+ elif sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))):
+ return (await async_render(trampoline(expand_macro(env_get(env, name), args, env)), env, ctx))
+ elif sx_truthy(contains_p(HTML_TAGS, name)):
+ return (await async_render_element(name, args, env, ctx))
+ elif sx_truthy((starts_with_p(name, '~') if not sx_truthy(starts_with_p(name, '~')) else (env_has(env, name) if not sx_truthy(env_has(env, name)) else is_island(env_get(env, name))))):
+ return (await async_render_island(env_get(env, name), args, env, ctx))
+ elif sx_truthy(starts_with_p(name, '~')):
+ val = (env_get(env, name) if sx_truthy(env_has(env, name)) else NIL)
+ if sx_truthy(is_component(val)):
+ return (await async_render_component(val, args, env, ctx))
+ elif sx_truthy(is_macro(val)):
+ return (await async_render(trampoline(expand_macro(val, args, env)), env, ctx))
+ else:
+ return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
+ elif sx_truthy(((index_of(name, '-') > 0) if not sx_truthy((index_of(name, '-') > 0)) else ((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword')))):
+ return (await async_render_element(name, args, env, ctx))
+ elif sx_truthy(svg_context_p()):
+ return (await async_render_element(name, args, env, ctx))
+ else:
+ return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
+
+# async-render-raw
+async def async_render_raw(args, env, ctx):
+ parts = []
+ for arg in args:
+ val = (await async_eval(arg, env, ctx))
+ if sx_truthy(is_raw_html(val)):
+ parts.append(raw_html_content(val))
+ elif sx_truthy((type_of(val) == 'string')):
+ parts.append(val)
+ elif sx_truthy(((not sx_truthy(is_nil(val))) if not sx_truthy((not sx_truthy(is_nil(val)))) else (not sx_truthy((val == False))))):
+ parts.append(sx_str(val))
+ return join('', parts)
+
+# async-render-element
+async def async_render_element(tag, args, env, ctx):
+ attrs = {}
+ children = []
+ (await async_parse_element_args(args, attrs, children, env, ctx))
+ class_val = dict_get(attrs, 'class')
+ if sx_truthy(((not sx_truthy(is_nil(class_val))) if not sx_truthy((not sx_truthy(is_nil(class_val)))) else (not sx_truthy((class_val == False))))):
+ css_class_collect(sx_str(class_val))
+ opening = sx_str('<', tag, render_attrs(attrs), '>')
+ if sx_truthy(contains_p(VOID_ELEMENTS, tag)):
+ return opening
+ else:
+ token = (svg_context_set(True) if sx_truthy(((tag == 'svg') if sx_truthy((tag == 'svg')) else (tag == 'math'))) else NIL)
+ child_html = join('', (await async_map_render(children, env, ctx)))
+ if sx_truthy(token):
+ svg_context_reset(token)
+ return sx_str(opening, child_html, '', tag, '>')
+
+# async-parse-element-args
+async def async_parse_element_args(args, attrs, children, env, ctx):
+ _cells = {}
+ _cells['skip'] = False
+ _cells['i'] = 0
+ for arg in args:
+ if sx_truthy(_cells['skip']):
+ _cells['skip'] = False
+ _cells['i'] = (_cells['i'] + 1)
+ else:
+ if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))):
+ val = (await async_eval(nth(args, (_cells['i'] + 1)), env, ctx))
+ attrs[keyword_name(arg)] = val
+ _cells['skip'] = True
+ _cells['i'] = (_cells['i'] + 1)
+ else:
+ children.append(arg)
+ _cells['i'] = (_cells['i'] + 1)
+ return NIL
+
+# async-render-component
+async def async_render_component(comp, args, env, ctx):
+ kwargs = {}
+ children = []
+ (await async_parse_kw_args(args, kwargs, children, env, ctx))
+ local = env_merge(component_closure(comp), env)
+ for p in component_params(comp):
+ local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL)
+ if sx_truthy(component_has_children(comp)):
+ local['children'] = make_raw_html(join('', (await async_map_render(children, env, ctx))))
+ return (await async_render(component_body(comp), local, ctx))
+
+# async-render-island
+async def async_render_island(island, args, env, ctx):
+ kwargs = {}
+ children = []
+ (await async_parse_kw_args(args, kwargs, children, env, ctx))
+ local = env_merge(component_closure(island), env)
+ island_name = component_name(island)
+ for p in component_params(island):
+ local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL)
+ if sx_truthy(component_has_children(island)):
+ local['children'] = make_raw_html(join('', (await async_map_render(children, env, ctx))))
+ body_html = (await async_render(component_body(island), local, ctx))
+ state_json = serialize_island_state(kwargs)
+ return sx_str('', body_html, '')
+
+# async-render-lambda
+async def async_render_lambda(f, args, env, ctx):
+ local = env_merge(lambda_closure(f), env)
+ for_each_indexed(lambda i, p: _sx_dict_set(local, p, nth(args, i)), lambda_params(f))
+ return (await async_render(lambda_body(f), local, ctx))
+
+# async-parse-kw-args
+async def async_parse_kw_args(args, kwargs, children, env, ctx):
+ _cells = {}
+ _cells['skip'] = False
+ _cells['i'] = 0
+ for arg in args:
+ if sx_truthy(_cells['skip']):
+ _cells['skip'] = False
+ _cells['i'] = (_cells['i'] + 1)
+ else:
+ if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))):
+ val = (await async_eval(nth(args, (_cells['i'] + 1)), env, ctx))
+ kwargs[keyword_name(arg)] = val
+ _cells['skip'] = True
+ _cells['i'] = (_cells['i'] + 1)
+ else:
+ children.append(arg)
+ _cells['i'] = (_cells['i'] + 1)
+ return NIL
+
+# async-map-render
+async def async_map_render(exprs, env, ctx):
+ results = []
+ for x in exprs:
+ results.append((await async_render(x, env, ctx)))
+ return results
+
+# ASYNC_RENDER_FORMS
+ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each']
+
+# async-render-form?
+def async_render_form_p(name):
+ return contains_p(ASYNC_RENDER_FORMS, name)
+
+# dispatch-async-render-form
+async def dispatch_async_render_form(name, expr, env, ctx):
+ if sx_truthy((name == 'if')):
+ cond_val = (await async_eval(nth(expr, 1), env, ctx))
+ if sx_truthy(cond_val):
+ return (await async_render(nth(expr, 2), env, ctx))
+ else:
+ if sx_truthy((len(expr) > 3)):
+ return (await async_render(nth(expr, 3), env, ctx))
+ else:
+ return ''
+ elif sx_truthy((name == 'when')):
+ if sx_truthy((not sx_truthy((await async_eval(nth(expr, 1), env, ctx))))):
+ return ''
+ else:
+ return join('', (await async_map_render(slice(expr, 2), env, ctx)))
+ elif sx_truthy((name == 'cond')):
+ clauses = rest(expr)
+ if sx_truthy(cond_scheme_p(clauses)):
+ return (await async_render_cond_scheme(clauses, env, ctx))
+ else:
+ return (await async_render_cond_clojure(clauses, env, ctx))
+ elif sx_truthy((name == 'case')):
+ return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
+ elif sx_truthy(((name == 'let') if sx_truthy((name == 'let')) else (name == 'let*'))):
+ local = (await async_process_bindings(nth(expr, 1), env, ctx))
+ return join('', (await async_map_render(slice(expr, 2), local, ctx)))
+ elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))):
+ return join('', (await async_map_render(rest(expr), env, ctx)))
+ elif sx_truthy(is_definition_form(name)):
+ (await async_eval(expr, env, ctx))
+ return ''
+ elif sx_truthy((name == 'map')):
+ f = (await async_eval(nth(expr, 1), env, ctx))
+ coll = (await async_eval(nth(expr, 2), env, ctx))
+ return join('', (await async_map_fn_render(f, coll, env, ctx)))
+ elif sx_truthy((name == 'map-indexed')):
+ f = (await async_eval(nth(expr, 1), env, ctx))
+ coll = (await async_eval(nth(expr, 2), env, ctx))
+ return join('', (await async_map_indexed_fn_render(f, coll, env, ctx)))
+ elif sx_truthy((name == 'filter')):
+ return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
+ elif sx_truthy((name == 'for-each')):
+ f = (await async_eval(nth(expr, 1), env, ctx))
+ coll = (await async_eval(nth(expr, 2), env, ctx))
+ return join('', (await async_map_fn_render(f, coll, env, ctx)))
+ else:
+ return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
+
+# async-render-cond-scheme
+async def async_render_cond_scheme(clauses, env, ctx):
+ if sx_truthy(empty_p(clauses)):
+ return ''
+ else:
+ clause = first(clauses)
+ test = first(clause)
+ body = nth(clause, 1)
+ if sx_truthy((((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))) if sx_truthy(((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else')))) else ((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')))):
+ return (await async_render(body, env, ctx))
+ else:
+ if sx_truthy((await async_eval(test, env, ctx))):
+ return (await async_render(body, env, ctx))
+ else:
+ return (await async_render_cond_scheme(rest(clauses), env, ctx))
+
+# async-render-cond-clojure
+async def async_render_cond_clojure(clauses, env, ctx):
+ if sx_truthy((len(clauses) < 2)):
+ return ''
+ else:
+ test = first(clauses)
+ body = nth(clauses, 1)
+ if sx_truthy((((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')) if sx_truthy(((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else'))) else ((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))))):
+ return (await async_render(body, env, ctx))
+ else:
+ if sx_truthy((await async_eval(test, env, ctx))):
+ return (await async_render(body, env, ctx))
+ else:
+ return (await async_render_cond_clojure(slice(clauses, 2), env, ctx))
+
+# async-process-bindings
+async def async_process_bindings(bindings, env, ctx):
+ local = env_extend(env)
+ if sx_truthy(((type_of(bindings) == 'list') if not sx_truthy((type_of(bindings) == 'list')) else (not sx_truthy(empty_p(bindings))))):
+ if sx_truthy((type_of(first(bindings)) == 'list')):
+ for pair in bindings:
+ if sx_truthy(((type_of(pair) == 'list') if not sx_truthy((type_of(pair) == 'list')) else (len(pair) >= 2))):
+ name = (symbol_name(first(pair)) if sx_truthy((type_of(first(pair)) == 'symbol')) else sx_str(first(pair)))
+ local[name] = (await async_eval(nth(pair, 1), local, ctx))
+ else:
+ (await async_process_bindings_flat(bindings, local, ctx))
+ return local
+
+# async-process-bindings-flat
+async def async_process_bindings_flat(bindings, local, ctx):
+ _cells = {}
+ _cells['skip'] = False
+ _cells['i'] = 0
+ for item in bindings:
+ if sx_truthy(_cells['skip']):
+ _cells['skip'] = False
+ _cells['i'] = (_cells['i'] + 1)
+ else:
+ name = (symbol_name(item) if sx_truthy((type_of(item) == 'symbol')) else sx_str(item))
+ if sx_truthy(((_cells['i'] + 1) < len(bindings))):
+ local[name] = (await async_eval(nth(bindings, (_cells['i'] + 1)), local, ctx))
+ _cells['skip'] = True
+ _cells['i'] = (_cells['i'] + 1)
+ return NIL
+
+# async-map-fn-render
+async def async_map_fn_render(f, coll, env, ctx):
+ results = []
+ for item in coll:
+ if sx_truthy(is_lambda(f)):
+ results.append((await async_render_lambda(f, [item], env, ctx)))
+ else:
+ r = (await async_invoke(f, item))
+ results.append((await async_render(r, env, ctx)))
+ return results
+
+# async-map-indexed-fn-render
+async def async_map_indexed_fn_render(f, coll, env, ctx):
+ _cells = {}
+ results = []
+ _cells['i'] = 0
+ for item in coll:
+ if sx_truthy(is_lambda(f)):
+ results.append((await async_render_lambda(f, [_cells['i'], item], env, ctx)))
+ else:
+ r = (await async_invoke(f, _cells['i'], item))
+ results.append((await async_render(r, env, ctx)))
+ _cells['i'] = (_cells['i'] + 1)
+ return results
+
+# async-invoke
+async def async_invoke(f, *args):
+ r = apply(f, args)
+ if sx_truthy(is_async_coroutine(r)):
+ return (await async_await(r))
+ else:
+ return r
+
+# async-aser
+async def async_aser(expr, env, ctx):
+ _match = type_of(expr)
+ if _match == 'number':
+ return expr
+ elif _match == 'string':
+ return expr
+ elif _match == 'boolean':
+ return expr
+ elif _match == 'nil':
+ return NIL
+ elif _match == 'symbol':
+ name = symbol_name(expr)
+ if sx_truthy(env_has(env, name)):
+ return env_get(env, name)
+ elif sx_truthy(is_primitive(name)):
+ return get_primitive(name)
+ elif sx_truthy((name == 'true')):
+ return True
+ elif sx_truthy((name == 'false')):
+ return False
+ elif sx_truthy((name == 'nil')):
+ return NIL
+ else:
+ return error(sx_str('Undefined symbol: ', name))
+ elif _match == 'keyword':
+ return keyword_name(expr)
+ elif _match == 'dict':
+ return (await async_aser_dict(expr, env, ctx))
+ elif _match == 'list':
+ if sx_truthy(empty_p(expr)):
+ return []
+ else:
+ return (await async_aser_list(expr, env, ctx))
+ else:
+ return expr
+
+# async-aser-dict
+async def async_aser_dict(expr, env, ctx):
+ result = {}
+ for key in keys(expr):
+ result[key] = (await async_aser(dict_get(expr, key), env, ctx))
+ return result
+
+# async-aser-list
+async def async_aser_list(expr, env, ctx):
+ head = first(expr)
+ args = rest(expr)
+ if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))):
+ if sx_truthy((is_lambda(head) if sx_truthy(is_lambda(head)) else (type_of(head) == 'list'))):
+ return (await async_aser_eval_call(head, args, env, ctx))
+ else:
+ return (await async_aser_map_list(expr, env, ctx))
+ else:
+ name = symbol_name(head)
+ if sx_truthy(io_primitive_p(name)):
+ return (await async_eval(expr, env, ctx))
+ elif sx_truthy((name == '<>')):
+ return (await async_aser_fragment(args, env, ctx))
+ elif sx_truthy((name == 'raw!')):
+ return (await async_aser_call('raw!', args, env, ctx))
+ elif sx_truthy(starts_with_p(name, 'html:')):
+ return (await async_aser_call(slice(name, 5), args, env, ctx))
+ elif sx_truthy(starts_with_p(name, '~')):
+ val = (env_get(env, name) if sx_truthy(env_has(env, name)) else NIL)
+ if sx_truthy(is_macro(val)):
+ return (await async_aser(trampoline(expand_macro(val, args, env)), env, ctx))
+ elif sx_truthy((is_component(val) if not sx_truthy(is_component(val)) else (expand_components_p() if sx_truthy(expand_components_p()) else (component_affinity(val) == 'server')))):
+ return (await async_aser_component(val, args, env, ctx))
+ else:
+ return (await async_aser_call(name, args, env, ctx))
+ elif sx_truthy(async_aser_form_p(name)):
+ if sx_truthy((contains_p(HTML_TAGS, name) if not sx_truthy(contains_p(HTML_TAGS, name)) else (((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword')) if sx_truthy(((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword'))) else svg_context_p()))):
+ return (await async_aser_call(name, args, env, ctx))
+ else:
+ return (await dispatch_async_aser_form(name, expr, env, ctx))
+ elif sx_truthy(contains_p(HTML_TAGS, name)):
+ return (await async_aser_call(name, args, env, ctx))
+ elif sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))):
+ return (await async_aser(trampoline(expand_macro(env_get(env, name), args, env)), env, ctx))
+ elif sx_truthy(((index_of(name, '-') > 0) if not sx_truthy((index_of(name, '-') > 0)) else ((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword')))):
+ return (await async_aser_call(name, args, env, ctx))
+ elif sx_truthy(svg_context_p()):
+ return (await async_aser_call(name, args, env, ctx))
+ else:
+ return (await async_aser_eval_call(head, args, env, ctx))
+
+# async-aser-eval-call
+async def async_aser_eval_call(head, args, env, ctx):
+ f = (await async_eval(head, env, ctx))
+ evaled_args = (await async_eval_args(args, env, ctx))
+ if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))):
+ r = apply(f, evaled_args)
+ if sx_truthy(is_async_coroutine(r)):
+ return (await async_await(r))
+ else:
+ return r
+ elif sx_truthy(is_lambda(f)):
+ local = env_merge(lambda_closure(f), env)
+ for_each_indexed(lambda i, p: _sx_dict_set(local, p, nth(evaled_args, i)), lambda_params(f))
+ return (await async_aser(lambda_body(f), local, ctx))
+ elif sx_truthy(is_component(f)):
+ return (await async_aser_call(sx_str('~', component_name(f)), args, env, ctx))
+ elif sx_truthy(is_island(f)):
+ return (await async_aser_call(sx_str('~', component_name(f)), args, env, ctx))
+ else:
+ return error(sx_str('Not callable: ', inspect(f)))
+
+# async-eval-args
+async def async_eval_args(args, env, ctx):
+ results = []
+ for a in args:
+ results.append((await async_eval(a, env, ctx)))
+ return results
+
+# async-aser-map-list
+async def async_aser_map_list(exprs, env, ctx):
+ results = []
+ for x in exprs:
+ results.append((await async_aser(x, env, ctx)))
+ return results
+
+# async-aser-fragment
+async def async_aser_fragment(children, env, ctx):
+ parts = []
+ for c in children:
+ result = (await async_aser(c, env, ctx))
+ if sx_truthy((type_of(result) == 'list')):
+ for item in result:
+ if sx_truthy((not sx_truthy(is_nil(item)))):
+ parts.append(serialize(item))
+ else:
+ if sx_truthy((not sx_truthy(is_nil(result)))):
+ parts.append(serialize(result))
+ if sx_truthy(empty_p(parts)):
+ return make_sx_expr('')
+ else:
+ return make_sx_expr(sx_str('(<> ', join(' ', parts), ')'))
+
+# async-aser-component
+async def async_aser_component(comp, args, env, ctx):
+ kwargs = {}
+ children = []
+ (await async_parse_aser_kw_args(args, kwargs, children, env, ctx))
+ local = env_merge(component_closure(comp), env)
+ for p in component_params(comp):
+ local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL)
+ if sx_truthy(component_has_children(comp)):
+ child_parts = []
+ for c in children:
+ result = (await async_aser(c, env, ctx))
+ if sx_truthy(list_p(result)):
+ for item in result:
+ if sx_truthy((not sx_truthy(is_nil(item)))):
+ child_parts.append(serialize(item))
+ else:
+ if sx_truthy((not sx_truthy(is_nil(result)))):
+ child_parts.append(serialize(result))
+ local['children'] = make_sx_expr(sx_str('(<> ', join(' ', child_parts), ')'))
+ return (await async_aser(component_body(comp), local, ctx))
+
+# async-parse-aser-kw-args
+async def async_parse_aser_kw_args(args, kwargs, children, env, ctx):
+ _cells = {}
+ _cells['skip'] = False
+ _cells['i'] = 0
+ for arg in args:
+ if sx_truthy(_cells['skip']):
+ _cells['skip'] = False
+ _cells['i'] = (_cells['i'] + 1)
+ else:
+ if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))):
+ val = (await async_aser(nth(args, (_cells['i'] + 1)), env, ctx))
+ kwargs[keyword_name(arg)] = val
+ _cells['skip'] = True
+ _cells['i'] = (_cells['i'] + 1)
+ else:
+ children.append(arg)
+ _cells['i'] = (_cells['i'] + 1)
+ return NIL
+
+# async-aser-call
+async def async_aser_call(name, args, env, ctx):
+ _cells = {}
+ token = (svg_context_set(True) if sx_truthy(((name == 'svg') if sx_truthy((name == 'svg')) else (name == 'math'))) else NIL)
+ parts = [name]
+ _cells['skip'] = False
+ _cells['i'] = 0
+ for arg in args:
+ if sx_truthy(_cells['skip']):
+ _cells['skip'] = False
+ _cells['i'] = (_cells['i'] + 1)
+ else:
+ if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((_cells['i'] + 1) < len(args)))):
+ val = (await async_aser(nth(args, (_cells['i'] + 1)), env, ctx))
+ if sx_truthy((not sx_truthy(is_nil(val)))):
+ parts.append(sx_str(':', keyword_name(arg)))
+ if sx_truthy((type_of(val) == 'list')):
+ live = filter(lambda v: (not sx_truthy(is_nil(v))), val)
+ if sx_truthy(empty_p(live)):
+ parts.append('nil')
+ else:
+ items = map(serialize, live)
+ if sx_truthy(some(lambda v: is_sx_expr(v), live)):
+ parts.append(sx_str('(<> ', join(' ', items), ')'))
+ else:
+ parts.append(sx_str('(list ', join(' ', items), ')'))
+ else:
+ parts.append(serialize(val))
+ _cells['skip'] = True
+ _cells['i'] = (_cells['i'] + 1)
+ else:
+ result = (await async_aser(arg, env, ctx))
+ if sx_truthy((not sx_truthy(is_nil(result)))):
+ if sx_truthy((type_of(result) == 'list')):
+ for item in result:
+ if sx_truthy((not sx_truthy(is_nil(item)))):
+ parts.append(serialize(item))
+ else:
+ parts.append(serialize(result))
+ _cells['i'] = (_cells['i'] + 1)
+ if sx_truthy(token):
+ svg_context_reset(token)
+ return make_sx_expr(sx_str('(', join(' ', parts), ')'))
+
+# ASYNC_ASER_FORM_NAMES
+ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland']
+
+# ASYNC_ASER_HO_NAMES
+ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each']
+
+# async-aser-form?
+def async_aser_form_p(name):
+ return (contains_p(ASYNC_ASER_FORM_NAMES, name) if sx_truthy(contains_p(ASYNC_ASER_FORM_NAMES, name)) else contains_p(ASYNC_ASER_HO_NAMES, name))
+
+# dispatch-async-aser-form
+async def dispatch_async_aser_form(name, expr, env, ctx):
+ _cells = {}
+ args = rest(expr)
+ if sx_truthy((name == 'if')):
+ cond_val = (await async_eval(first(args), env, ctx))
+ if sx_truthy(cond_val):
+ return (await async_aser(nth(args, 1), env, ctx))
+ else:
+ if sx_truthy((len(args) > 2)):
+ return (await async_aser(nth(args, 2), env, ctx))
+ else:
+ return NIL
+ elif sx_truthy((name == 'when')):
+ if sx_truthy((not sx_truthy((await async_eval(first(args), env, ctx))))):
+ return NIL
+ else:
+ _cells['result'] = NIL
+ for body in rest(args):
+ _cells['result'] = (await async_aser(body, env, ctx))
+ return _cells['result']
+ elif sx_truthy((name == 'cond')):
+ if sx_truthy(cond_scheme_p(args)):
+ return (await async_aser_cond_scheme(args, env, ctx))
+ else:
+ return (await async_aser_cond_clojure(args, env, ctx))
+ elif sx_truthy((name == 'case')):
+ match_val = (await async_eval(first(args), env, ctx))
+ return (await async_aser_case_loop(match_val, rest(args), env, ctx))
+ elif sx_truthy(((name == 'let') if sx_truthy((name == 'let')) else (name == 'let*'))):
+ local = (await async_process_bindings(first(args), env, ctx))
+ _cells['result'] = NIL
+ for body in rest(args):
+ _cells['result'] = (await async_aser(body, local, ctx))
+ return _cells['result']
+ elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))):
+ _cells['result'] = NIL
+ for body in args:
+ _cells['result'] = (await async_aser(body, env, ctx))
+ return _cells['result']
+ elif sx_truthy((name == 'and')):
+ _cells['result'] = True
+ _cells['stop'] = False
+ for arg in args:
+ if sx_truthy((not sx_truthy(_cells['stop']))):
+ _cells['result'] = (await async_eval(arg, env, ctx))
+ if sx_truthy((not sx_truthy(_cells['result']))):
+ _cells['stop'] = True
+ return _cells['result']
+ elif sx_truthy((name == 'or')):
+ _cells['result'] = False
+ _cells['stop'] = False
+ for arg in args:
+ if sx_truthy((not sx_truthy(_cells['stop']))):
+ _cells['result'] = (await async_eval(arg, env, ctx))
+ if sx_truthy(_cells['result']):
+ _cells['stop'] = True
+ return _cells['result']
+ elif sx_truthy(((name == 'lambda') if sx_truthy((name == 'lambda')) else (name == 'fn'))):
+ return sf_lambda(args, env)
+ elif sx_truthy((name == 'quote')):
+ if sx_truthy(empty_p(args)):
+ return NIL
+ else:
+ return first(args)
+ elif sx_truthy((name == '->')):
+ return (await async_aser_thread_first(args, env, ctx))
+ elif sx_truthy((name == 'set!')):
+ value = (await async_eval(nth(args, 1), env, ctx))
+ env[symbol_name(first(args))] = value
+ return value
+ elif sx_truthy((name == 'map')):
+ return (await async_aser_ho_map(args, env, ctx))
+ elif sx_truthy((name == 'map-indexed')):
+ return (await async_aser_ho_map_indexed(args, env, ctx))
+ elif sx_truthy((name == 'filter')):
+ return (await async_eval(expr, env, ctx))
+ elif sx_truthy((name == 'for-each')):
+ return (await async_aser_ho_for_each(args, env, ctx))
+ elif sx_truthy((name == 'defisland')):
+ (await async_eval(expr, env, ctx))
+ return serialize(expr)
+ elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else (name == 'defaction'))))))))):
+ (await async_eval(expr, env, ctx))
+ return NIL
+ else:
+ return (await async_eval(expr, env, ctx))
+
+# async-aser-cond-scheme
+async def async_aser_cond_scheme(clauses, env, ctx):
+ if sx_truthy(empty_p(clauses)):
+ return NIL
+ else:
+ clause = first(clauses)
+ test = first(clause)
+ body = nth(clause, 1)
+ if sx_truthy((((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))) if sx_truthy(((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else')))) else ((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')))):
+ return (await async_aser(body, env, ctx))
+ else:
+ if sx_truthy((await async_eval(test, env, ctx))):
+ return (await async_aser(body, env, ctx))
+ else:
+ return (await async_aser_cond_scheme(rest(clauses), env, ctx))
+
+# async-aser-cond-clojure
+async def async_aser_cond_clojure(clauses, env, ctx):
+ if sx_truthy((len(clauses) < 2)):
+ return NIL
+ else:
+ test = first(clauses)
+ body = nth(clauses, 1)
+ if sx_truthy((((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')) if sx_truthy(((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else'))) else ((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))))):
+ return (await async_aser(body, env, ctx))
+ else:
+ if sx_truthy((await async_eval(test, env, ctx))):
+ return (await async_aser(body, env, ctx))
+ else:
+ return (await async_aser_cond_clojure(slice(clauses, 2), env, ctx))
+
+# async-aser-case-loop
+async def async_aser_case_loop(match_val, clauses, env, ctx):
+ if sx_truthy((len(clauses) < 2)):
+ return NIL
+ else:
+ test = first(clauses)
+ body = nth(clauses, 1)
+ if sx_truthy((((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')) if sx_truthy(((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else'))) else ((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == ':else') if sx_truthy((symbol_name(test) == ':else')) else (symbol_name(test) == 'else'))))):
+ return (await async_aser(body, env, ctx))
+ else:
+ if sx_truthy((match_val == (await async_eval(test, env, ctx)))):
+ return (await async_aser(body, env, ctx))
+ else:
+ return (await async_aser_case_loop(match_val, slice(clauses, 2), env, ctx))
+
+# async-aser-thread-first
+async def async_aser_thread_first(args, env, ctx):
+ _cells = {}
+ _cells['result'] = (await async_eval(first(args), env, ctx))
+ for form in rest(args):
+ if sx_truthy((type_of(form) == 'list')):
+ f = (await async_eval(first(form), env, ctx))
+ fn_args = cons(_cells['result'], (await async_eval_args(rest(form), env, ctx)))
+ _cells['result'] = (await async_invoke_or_lambda(f, fn_args, env, ctx))
+ else:
+ f = (await async_eval(form, env, ctx))
+ _cells['result'] = (await async_invoke_or_lambda(f, [_cells['result']], env, ctx))
+ return _cells['result']
+
+# async-invoke-or-lambda
+async def async_invoke_or_lambda(f, args, env, ctx):
+ if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else (not sx_truthy(is_component(f)))))):
+ r = apply(f, args)
+ if sx_truthy(is_async_coroutine(r)):
+ return (await async_await(r))
+ else:
+ return r
+ elif sx_truthy(is_lambda(f)):
+ local = env_merge(lambda_closure(f), env)
+ for_each_indexed(lambda i, p: _sx_dict_set(local, p, nth(args, i)), lambda_params(f))
+ return (await async_eval(lambda_body(f), local, ctx))
+ else:
+ return error(sx_str('-> form not callable: ', inspect(f)))
+
+# async-aser-ho-map
+async def async_aser_ho_map(args, env, ctx):
+ f = (await async_eval(first(args), env, ctx))
+ coll = (await async_eval(nth(args, 1), env, ctx))
+ results = []
+ for item in coll:
+ if sx_truthy(is_lambda(f)):
+ local = env_merge(lambda_closure(f), env)
+ local[first(lambda_params(f))] = item
+ results.append((await async_aser(lambda_body(f), local, ctx)))
+ else:
+ results.append((await async_invoke(f, item)))
+ return results
+
+# async-aser-ho-map-indexed
+async def async_aser_ho_map_indexed(args, env, ctx):
+ _cells = {}
+ f = (await async_eval(first(args), env, ctx))
+ coll = (await async_eval(nth(args, 1), env, ctx))
+ results = []
+ _cells['i'] = 0
+ for item in coll:
+ if sx_truthy(is_lambda(f)):
+ local = env_merge(lambda_closure(f), env)
+ local[first(lambda_params(f))] = _cells['i']
+ local[nth(lambda_params(f), 1)] = item
+ results.append((await async_aser(lambda_body(f), local, ctx)))
+ else:
+ results.append((await async_invoke(f, _cells['i'], item)))
+ _cells['i'] = (_cells['i'] + 1)
+ return results
+
+# async-aser-ho-for-each
+async def async_aser_ho_for_each(args, env, ctx):
+ f = (await async_eval(first(args), env, ctx))
+ coll = (await async_eval(nth(args, 1), env, ctx))
+ results = []
+ for item in coll:
+ if sx_truthy(is_lambda(f)):
+ local = env_merge(lambda_closure(f), env)
+ local[first(lambda_params(f))] = item
+ results.append((await async_aser(lambda_body(f), local, ctx)))
+ else:
+ results.append((await async_invoke(f, item)))
+ return results
+
+# async-eval-slot-inner
+async def async_eval_slot_inner(expr, env, ctx):
+ result = NIL
+ if sx_truthy((list_p(expr) if not sx_truthy(list_p(expr)) else (not sx_truthy(empty_p(expr))))):
+ head = first(expr)
+ if sx_truthy(((type_of(head) == 'symbol') if not sx_truthy((type_of(head) == 'symbol')) else starts_with_p(symbol_name(head), '~'))):
+ name = symbol_name(head)
+ val = (env_get(env, name) if sx_truthy(env_has(env, name)) else NIL)
+ if sx_truthy(is_component(val)):
+ result = (await async_aser_component(val, rest(expr), env, ctx))
+ else:
+ result = (await async_maybe_expand_result((await async_aser(expr, env, ctx)), env, ctx))
+ else:
+ result = (await async_maybe_expand_result((await async_aser(expr, env, ctx)), env, ctx))
+ else:
+ result = (await async_maybe_expand_result((await async_aser(expr, env, ctx)), env, ctx))
+ if sx_truthy(is_sx_expr(result)):
+ return result
+ else:
+ if sx_truthy(is_nil(result)):
+ return make_sx_expr('')
+ else:
+ if sx_truthy(string_p(result)):
+ return make_sx_expr(result)
+ else:
+ return make_sx_expr(serialize(result))
+
+# async-maybe-expand-result
+async def async_maybe_expand_result(result, env, ctx):
+ raw = (trim(sx_str(result)) if sx_truthy(is_sx_expr(result)) else (trim(result) if sx_truthy(string_p(result)) else NIL))
+ if sx_truthy((raw if not sx_truthy(raw) else starts_with_p(raw, '(~'))):
+ parsed = sx_parse(raw)
+ if sx_truthy((parsed if not sx_truthy(parsed) else (not sx_truthy(empty_p(parsed))))):
+ return (await async_eval_slot_inner(first(parsed), env, ctx))
+ else:
+ return result
+ else:
+ return result
+
+
# =========================================================================
# Fixups -- wire up render adapter dispatch
# =========================================================================
@@ -2786,4 +3833,4 @@ def render(expr, env=None):
def make_env(**kwargs):
"""Create an environment with initial bindings."""
- return _Env(dict(kwargs))
+ return _Env(dict(kwargs))
\ No newline at end of file
diff --git a/shared/sx/ref/test-aser.sx b/shared/sx/ref/test-aser.sx
index 053386a..217b0a4 100644
--- a/shared/sx/ref/test-aser.sx
+++ b/shared/sx/ref/test-aser.sx
@@ -119,6 +119,19 @@
(assert-equal "(p \"hello\")"
(render-sx "(let ((x \"hello\")) (p x))")))
+ (deftest "let preserves outer scope bindings"
+ ;; Regression: process-bindings must preserve parent env scope chain.
+ ;; Using merge() instead of env-extend loses parent scope items.
+ (assert-equal "(p \"outer\")"
+ (render-sx "(do (define theme \"outer\") (let ((x 1)) (p theme)))")))
+
+ (deftest "nested let preserves outer scope"
+ (assert-equal "(div (span \"hello\") (span \"world\"))"
+ (render-sx "(do (define a \"hello\")
+ (define b \"world\")
+ (div (let ((x 1)) (span a))
+ (let ((y 2)) (span b))))")))
+
(deftest "begin serializes last"
(assert-equal "(p \"last\")"
(render-sx "(begin (p \"first\") (p \"last\"))"))))
@@ -213,6 +226,17 @@
(assert-equal "10"
(render-sx "(do (define double (fn (x) (* x 2))) (double 5))")))
+ (deftest "native callable with multiple args"
+ ;; Regression: async-aser-eval-call passed evaled-args list to
+ ;; async-invoke (&rest), wrapping it in another list. apply(f, [list])
+ ;; calls f(list) instead of f(*list).
+ (assert-equal "3"
+ (render-sx "(do (define my-add +) (my-add 1 2))")))
+
+ (deftest "native callable with two args via alias"
+ (assert-equal "hello world"
+ (render-sx "(do (define my-join str) (my-join \"hello\" \" world\"))")))
+
(deftest "higher-order: map returns list"
(let ((result (render-sx "(map (fn (x) (+ x 1)) (list 1 2 3))")))
;; map at top level returns a list, not serialized tags
diff --git a/shared/sx/ref/test-render.sx b/shared/sx/ref/test-render.sx
index 08f1d7f..ccca649 100644
--- a/shared/sx/ref/test-render.sx
+++ b/shared/sx/ref/test-render.sx
@@ -149,7 +149,20 @@
(deftest "let in render context"
(assert-equal "hello
"
- (render-html "(let ((x \"hello\")) (p x))"))))
+ (render-html "(let ((x \"hello\")) (p x))")))
+
+ (deftest "let preserves outer scope bindings"
+ ;; Regression: process-bindings must preserve parent env scope chain.
+ ;; Using merge() on Env objects returns empty dict (Env is not dict subclass).
+ (assert-equal "outer
"
+ (render-html "(do (define theme \"outer\") (let ((x 1)) (p theme)))")))
+
+ (deftest "nested let preserves outer scope"
+ (assert-equal "helloworld
"
+ (render-html "(do (define a \"hello\")
+ (define b \"world\")
+ (div (let ((x 1)) (span a))
+ (let ((y 2)) (span b))))"))))
;; --------------------------------------------------------------------------
diff --git a/shared/sx/tests/run.py b/shared/sx/tests/run.py
index 297ebac..80ae432 100644
--- a/shared/sx/tests/run.py
+++ b/shared/sx/tests/run.py
@@ -127,7 +127,8 @@ def render_html(sx_source):
except ImportError:
raise RuntimeError("render-to-html not available — sx_ref.py not built")
exprs = parse_all(sx_source)
- render_env = dict(env)
+ # Use Env (not flat dict) so tests exercise the real scope chain path.
+ render_env = _Env(dict(env))
result = ""
for expr in exprs:
result += _render_to_html(expr, render_env)
@@ -143,7 +144,9 @@ def render_sx(sx_source):
except ImportError:
raise RuntimeError("aser not available — sx_ref.py not built")
exprs = parse_all(sx_source)
- render_env = dict(env)
+ # Use Env (not flat dict) so tests exercise the real scope chain path.
+ # Using dict(env) hides bugs where merge() drops Env parent scopes.
+ render_env = _Env(dict(env))
result = ""
for expr in exprs:
val = _aser(expr, render_env)
diff --git a/sx/sx/page-helpers-demo.sx b/sx/sx/page-helpers-demo.sx
index f4adce1..2bf8e8c 100644
--- a/sx/sx/page-helpers-demo.sx
+++ b/sx/sx/page-helpers-demo.sx
@@ -42,7 +42,6 @@
(sf-ms (- (now-ms) t1))
(sf-cats {})
(sf-total 0)
-
;; 2. build-reference-data
(t2 (now-ms))
(ref-result (build-reference-data "attributes"