Aser server-affinity component expansion + readline buffer fix

adapter-sx.sx: aser-expand-component expands :affinity :server components
inline during SX wire format serialization. Binds keyword args via
eval-expr, children via aser (handles HTML tags), then asers the body.

ocaml_bridge.py: 10MB readline buffer for large spec responses.
nav-data.sx: evaluator.sx filename fix.

Page rendering stays on Python _eval_slot for now — full OCaml rendering
needs the page shell IO (headers, CSRF, CSS) migrated to OCaml IO bridge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 20:46:33 +00:00
parent 1c91680e63
commit 171c18d3be
4 changed files with 82 additions and 7 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-18T19:18:46Z";
var SX_VERSION = "2026-03-18T20:14:01Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -2673,7 +2673,10 @@ PRIMITIVES["aser"] = aser;
var args = rest(expr);
return (isSxTruthy(!isSxTruthy((typeOf(head) == "symbol"))) ? map(function(x) { return aser(x, env); }, expr) : (function() {
var name = symbolName(head);
return (isSxTruthy((name == "<>")) ? aserFragment(args, env) : (isSxTruthy(startsWith(name, "~")) ? aserCall(name, args, env) : (isSxTruthy((name == "lake")) ? aserCall(name, args, env) : (isSxTruthy((name == "marsh")) ? aserCall(name, args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? aserCall(name, args, env) : (isSxTruthy(sxOr(isSpecialForm(name), isHoForm(name))) ? aserSpecial(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? aser(expandMacro(envGet(env, name), args, env), env) : (function() {
return (isSxTruthy((name == "<>")) ? aserFragment(args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() {
var comp = (isSxTruthy(envHas(env, name)) ? envGet(env, name) : NIL);
return (isSxTruthy((isSxTruthy(comp) && isSxTruthy(isComponent(comp)) && (componentAffinity(comp) == "server"))) ? aserExpandComponent(comp, args, env) : aserCall(name, args, env));
})() : (isSxTruthy((name == "lake")) ? aserCall(name, args, env) : (isSxTruthy((name == "marsh")) ? aserCall(name, args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? aserCall(name, args, env) : (isSxTruthy(sxOr(isSpecialForm(name), isHoForm(name))) ? aserSpecial(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? aser(expandMacro(envGet(env, name), args, env), env) : (function() {
var f = trampoline(evalExpr(head, env));
var evaledArgs = map(function(a) { return trampoline(evalExpr(a, env)); }, args);
return (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isSxTruthy(isLambda(f))) && isSxTruthy(!isSxTruthy(isComponent(f))) && !isSxTruthy(isIsland(f)))) ? apply(f, evaledArgs) : (isSxTruthy(isLambda(f)) ? trampoline(callLambda(f, evaledArgs, env)) : (isSxTruthy(isComponent(f)) ? aserCall((String("~") + String(componentName(f))), args, env) : (isSxTruthy(isIsland(f)) ? aserCall((String("~") + String(componentName(f))), args, env) : error((String("Not callable: ") + String(inspect(f))))))));
@@ -2728,6 +2731,24 @@ PRIMITIVES["aser-fragment"] = aserFragment;
})(); };
PRIMITIVES["aser-call"] = aserCall;
// aser-expand-component
var aserExpandComponent = function(comp, args, env) { return (function() {
var params = componentParams(comp);
var local = envMerge(env, componentClosure(comp));
var i = 0;
var skip = false;
var children = [];
{ var _c = args; for (var _i = 0; _i < _c.length; _i++) { var arg = _c[_i]; (isSxTruthy(skip) ? ((skip = false), (i = (i + 1))) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((i + 1) < len(args)))) ? (envBind(local, keywordName(arg), trampoline(evalExpr(nth(args, (i + 1)), env))), (skip = true), (i = (i + 1))) : (append_b(children, arg), (i = (i + 1))))); } }
if (isSxTruthy(componentHasChildren(comp))) {
(function() {
var aseredChildren = map(function(c) { return aser(c, env); }, children);
return envBind(local, "children", (isSxTruthy((len(aseredChildren) == 1)) ? first(aseredChildren) : aseredChildren));
})();
}
return aser(componentBody(comp), local);
})(); };
PRIMITIVES["aser-expand-component"] = aserExpandComponent;
// SPECIAL_FORM_NAMES
var SPECIAL_FORM_NAMES = ["if", "when", "cond", "case", "and", "or", "let", "let*", "lambda", "fn", "define", "defcomp", "defmacro", "defstyle", "defhandler", "defpage", "defquery", "defaction", "defrelation", "begin", "do", "quote", "quasiquote", "->", "set!", "letrec", "dynamic-wind", "defisland", "deftype", "defeffect", "scope", "provide"];
PRIMITIVES["SPECIAL_FORM_NAMES"] = SPECIAL_FORM_NAMES;

View File

@@ -2257,7 +2257,10 @@
(env-bind! local "children" children))
(make-cek-state (component-body f) local kont))
:else (error (str "Not callable: " (inspect f))))))
:else (error (str "Not callable: " (inspect f)
(when raw-args
(str " in (" (inspect (first raw-args)) " ...)")))))))
;; --------------------------------------------------------------------------

View File

@@ -199,9 +199,9 @@ async def eval_sx_url(raw_path: str) -> Any:
logger.error("SX URL render failed for %s: %s", raw_path, e, exc_info=True)
return None
# Return response
# Return response — Python wraps in page shell (CSS, scripts, headers)
if is_htmx_request():
return sx_response(await oob_page_sx(content=content_sx))
return sx_response(content_sx)
else:
tctx = await get_template_context()
html = await full_page_sx(tctx, header_rows="", content=content_sx)

View File

@@ -71,9 +71,13 @@
(= name "<>")
(aser-fragment args env)
;; Component call — serialize WITHOUT expanding
;; Component call — expand server-affinity, serialize others
(starts-with? name "~")
(aser-call name args env)
(let ((comp (if (env-has? env name) (env-get env name) nil)))
(if (and comp (component? comp)
(= (component-affinity comp) "server"))
(aser-expand-component comp args env)
(aser-call name args env)))
;; Lake — serialize (server-morphable slot)
(= name "lake")
@@ -213,6 +217,53 @@
(str "(" (join " " parts) ")")))))
;; --------------------------------------------------------------------------
;; Server-affinity component expansion
;; --------------------------------------------------------------------------
;;
;; When a component has :affinity :server, the aser expands it inline:
;; bind keyword args + children, then aser the body.
;; This is the aser equivalent of render-to-html's component expansion.
(define aser-expand-component :effects [render]
(fn ((comp :as any) (args :as list) (env :as dict))
(let ((params (component-params comp))
(local (env-merge env (component-closure comp)))
(i 0)
(skip false)
(children (list)))
;; Parse keyword args and positional children from args
;; Keyword values are eval'd (they're data). Children are NOT eval'd
;; (they may contain HTML tags that only the aser can handle).
(for-each
(fn (arg)
(if skip
(do (set! skip false) (set! i (inc i)))
(if (and (= (type-of arg) "keyword")
(< (inc i) (len args)))
;; Keyword arg: bind name = eval'd next arg
(do
(env-bind! local (keyword-name arg)
(trampoline (eval-expr (nth args (inc i)) env)))
(set! skip true)
(set! i (inc i)))
;; Positional child: keep as unevaluated AST for aser
(do
(append! children arg)
(set! i (inc i))))))
args)
;; Bind &rest children — aser each child first, then bind the result
(when (component-has-children comp)
(let ((asered-children
(map (fn (c) (aser c env)) children)))
(env-bind! local "children"
(if (= (len asered-children) 1)
(first asered-children)
asered-children))))
;; Aser the body in the merged env
(aser (component-body comp) local))))
;; --------------------------------------------------------------------------
;; Form classification
;; --------------------------------------------------------------------------