VM aser-slot → sx-page-full: single-call page render, 0.55s warm

Compiler fixes:
- Upvalue re-lookup returns own position (uv-index), not parent slot
- Spec: cek-call uses (make-env) not (dict) — OCaml Dict≠Env
- Bootstrap post-processes transpiler Dict→Env for cek_call

VM runtime fixes:
- compile_adapter evaluates constant defines (SPECIAL_FORM_NAMES etc.)
  via execute_module instead of wrapping as NativeFn closures
- Native primitives: map-indexed, some, every?
- Nil-safe HO forms: map/filter/for-each/some/every? accept nil as empty
- expand-components? set in kernel env (not just VM globals)
- unwrap_env diagnostic: reports actual type received

sx-page-full command:
- Single OCaml call: aser-slot body + render-to-html shell
- Eliminates two pipe round-trips (was: aser-slot→Python→shell render)
- Shell statics (component_defs, CSS, pages_sx) cached in Python,
  injected into kernel once, referenced by symbol in per-request command
- Large blobs use placeholder tokens — Python splices post-render,
  pipe transfers ~51KB instead of 2MB

Performance (warm):
- Server total: 0.55s (was ~2s)
- aser-slot VM: 0.3s, shell render: 0.01s, pipe: 0.06s
- kwargs computation: 0.000s (cached)

SX_STANDALONE mode for sx_docs dev (skips fragment fetches).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 11:06:04 +00:00
parent 8dd3eaa1d9
commit ae0e87fbf8
13 changed files with 477 additions and 149 deletions

View File

@@ -182,11 +182,11 @@ def create_app() -> "Quart":
from quart import request, make_response
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.jinja_bridge import get_component_env, _get_request_context
from shared.sx.async_eval import async_eval_slot_to_sx
from shared.sx.types import Symbol, Keyword
from shared.sx.helpers import full_page_sx, oob_page_sx, sx_response
from shared.sx.pages import get_page_helpers
from shared.sx.page import get_template_context
import os
path = request.path
content_ast = [
@@ -199,7 +199,15 @@ def create_app() -> "Quart":
ctx = _get_request_context()
try:
content_sx = await async_eval_slot_to_sx(content_ast, env, ctx)
if os.environ.get("SX_USE_OCAML") == "1":
from shared.sx.ocaml_bridge import get_bridge
from shared.sx.parser import serialize
bridge = await get_bridge()
sx_text = serialize(content_ast)
content_sx = await bridge.aser_slot(sx_text, ctx={"_helper_service": "sx"})
else:
from shared.sx.async_eval import async_eval_slot_to_sx
content_sx = await async_eval_slot_to_sx(content_ast, env, ctx)
except Exception:
from shared.browser.app.errors import _sx_error_page
html = _sx_error_page("404", "NOT FOUND",

View File

@@ -212,16 +212,27 @@ async def eval_sx_url(raw_path: str) -> Any:
serialize(oob_ast), ctx=ocaml_ctx))
return sx_response(content_sx)
else:
# Full page: single-pass — layout + content in ONE aser_slot
# Full page: single OCaml call — aser-slot + shell render
full_ast = [
Symbol("~shared:layout/app-body"),
Keyword("content"), wrapped_ast,
]
body_sx = SxExpr(await bridge.aser_slot(
serialize(full_ast), ctx=ocaml_ctx))
page_source = serialize(full_ast)
# Pre-compute shell kwargs in Python
import time as _time
_t0 = _time.monotonic()
from shared.sx.helpers import _build_shell_kwargs
tctx = await get_template_context()
return await make_response(
await sx_page(tctx, body_sx), 200)
_t1 = _time.monotonic()
shell_kwargs = await _build_shell_kwargs(tctx, page_source)
_t2 = _time.monotonic()
html = await bridge.sx_page_full(
page_source, shell_kwargs, ctx=ocaml_ctx)
_t3 = _time.monotonic()
logger.info("[sx-page-full-py] ctx=%.3fs kwargs=%.3fs ocaml=%.3fs total=%.3fs",
_t1-_t0, _t2-_t1, _t3-_t2, _t3-_t0)
return await make_response(html, 200)
else:
content_sx = await _eval_slot(wrapped_ast, env, ctx)
except Exception as e: