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)}" - - -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, '') + +# 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"