diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index daa4b4b..c904fec 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -413,6 +413,18 @@ let make_server_env () = bind "dom-get-data" (fun _args -> Nil); bind "event-detail" (fun _args -> Nil); bind "promise-then" (fun _args -> Nil); + bind "thunk?" (fun args -> match args with [Thunk _] -> Bool true | _ -> Bool false); + bind "thunk-expr" (fun args -> match args with [v] -> thunk_expr v | _ -> Nil); + bind "thunk-env" (fun args -> match args with [v] -> thunk_env v | _ -> Nil); + bind "schedule-idle" (fun _args -> Nil); + bind "dom-query" (fun _args -> Nil); + bind "dom-query-all" (fun _args -> List []); + bind "dom-set-prop" (fun _args -> Nil); + bind "dom-get-attr" (fun _args -> Nil); + bind "dom-set-attr" (fun _args -> Nil); + bind "dom-text-content" (fun _args -> String ""); + bind "dom-set-text-content" (fun _args -> Nil); + bind "dom-body" (fun _args -> Nil); (* Raw HTML — platform primitives for adapter-html.sx *) bind "make-raw-html" (fun args -> @@ -503,7 +515,12 @@ let make_server_env () = | _ -> raise (Eval_error "eval-expr: expected (expr env?)")); bind "trampoline" (fun args -> match args with - | [v] -> v (* CEK never produces thunks *) + | [v] -> + (* sf-letrec returns thunks — resolve them *) + let rec resolve v = match v with + | Thunk (expr, env) -> resolve (Sx_ref.eval_expr expr (Env env)) + | _ -> v + in resolve v | _ -> raise (Eval_error "trampoline: expected 1 arg")); bind "call-lambda" (fun args -> match args with diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 6297351..7fe53e3 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-23T13:21:57Z"; + var SX_VERSION = "2026-03-23T15:29:13Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -2456,11 +2456,11 @@ PRIMITIVES["serialize"] = serialize; // render-to-html var renderToHtml = function(expr, env) { setRenderActiveB(true); -return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); if (_m == "raw-html") return rawHtmlContent(expr); if (_m == "spread") return (sxEmit("element-attrs", spreadAttrs(expr)), ""); return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); }; +return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); if (_m == "raw-html") return rawHtmlContent(expr); if (_m == "spread") return (sxEmit("element-attrs", spreadAttrs(expr)), ""); if (_m == "thunk") return renderToHtml(thunkExpr(expr), thunkEnv(expr)); return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); }; PRIMITIVES["render-to-html"] = renderToHtml; // render-value-to-html - var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); if (_m == "spread") return (sxEmit("element-attrs", spreadAttrs(val)), ""); return escapeHtml((String(val))); })(); }; + var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); if (_m == "spread") return (sxEmit("element-attrs", spreadAttrs(val)), ""); if (_m == "thunk") return renderToHtml(thunkExpr(val), thunkEnv(val)); return escapeHtml((String(val))); })(); }; PRIMITIVES["render-value-to-html"] = renderValueToHtml; // RENDER_HTML_FORMS diff --git a/shared/sx/ocaml_bridge.py b/shared/sx/ocaml_bridge.py index 45eb572..bbc13d3 100644 --- a/shared/sx/ocaml_bridge.py +++ b/shared/sx/ocaml_bridge.py @@ -439,6 +439,15 @@ class OcamlBridge: skipped += 1 _logger.warning("OCaml load skipped %s: %s", filepath, e) + # SSR overrides: effect is a no-op on the server (prevents + # reactive loops during island SSR — effects are DOM side-effects) + try: + noop_dispose = '(fn () nil)' + await self._send(f'(load-source "(define effect (fn (f) {noop_dispose}))")') + await self._read_until_ok(ctx=None) + except OcamlBridgeError: + pass + # Register JIT hook — lambdas compile on first call try: await self._send('(vm-compile-adapter)') diff --git a/web/adapter-html.sx b/web/adapter-html.sx index 156b783..29125dd 100644 --- a/web/adapter-html.sx +++ b/web/adapter-html.sx @@ -32,6 +32,8 @@ "raw-html" (raw-html-content expr) ;; Spread — emit attrs to nearest element provider "spread" (do (emit! "element-attrs" (spread-attrs expr)) "") + ;; Thunk — unwrap and render the inner expression (from letrec TCO) + "thunk" (render-to-html (thunk-expr expr) (thunk-env expr)) ;; Everything else — evaluate first :else (render-value-to-html (trampoline (eval-expr expr env)) env)))) @@ -45,6 +47,7 @@ "list" (render-list-to-html val env) "raw-html" (raw-html-content val) "spread" (do (emit! "element-attrs" (spread-attrs val)) "") + "thunk" (render-to-html (thunk-expr val) (thunk-env val)) :else (escape-html (str val)))))