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

@@ -792,6 +792,162 @@ def _sx_literal(v: object) -> str:
_cached_shell_static: dict[str, Any] | None = None
_cached_shell_comp_hash: str | None = None
def invalidate_shell_cache():
"""Call on component hot-reload to recompute shell statics."""
global _cached_shell_static, _cached_shell_comp_hash
_cached_shell_static = None
_cached_shell_comp_hash = None
def _get_shell_static() -> dict[str, Any]:
"""Compute and cache all shell kwargs that don't change per-request.
This is the expensive part: component dep scanning, serialization,
CSS class scanning, rule lookup, pages registry. All stable until
components are hot-reloaded.
"""
global _cached_shell_static, _cached_shell_comp_hash
from .jinja_bridge import components_for_page, css_classes_for_page, _component_env_hash
from .css_registry import lookup_rules, get_preamble, registry_loaded, store_css_hash
current_hash = _component_env_hash()
if _cached_shell_static is not None and _cached_shell_comp_hash == current_hash:
return _cached_shell_static
import time
t0 = time.monotonic()
from quart import current_app as _ca
from .jinja_bridge import client_components_tag, _COMPONENT_ENV, _CLIENT_LIBRARY_SOURCES
from .jinja_bridge import _component_env_hash
from .parser import serialize as _serialize
# Send ALL component definitions — the hash is stable per env so the
# browser caches them across all pages. Server-side expansion handles
# the per-page subset; the client needs the full set for client-side
# routing to any page.
parts = []
for key, val in _COMPONENT_ENV.items():
from .types import Island, Component, Macro
if isinstance(val, Island):
ps = ["&key"] + list(val.params)
if val.has_children: ps.extend(["&rest", "children"])
parts.append(f"(defisland ~{val.name} ({' '.join(ps)}) {_serialize(val.body, pretty=True)})")
elif isinstance(val, Component):
ps = ["&key"] + list(val.params)
if val.has_children: ps.extend(["&rest", "children"])
parts.append(f"(defcomp ~{val.name} ({' '.join(ps)}) {_serialize(val.body, pretty=True)})")
elif isinstance(val, Macro):
ps = list(val.params)
if val.rest_param: ps.extend(["&rest", val.rest_param])
parts.append(f"(defmacro {val.name} ({' '.join(ps)}) {_serialize(val.body, pretty=True)})")
all_parts = list(_CLIENT_LIBRARY_SOURCES) + parts
component_defs = "\n".join(all_parts)
component_hash = _component_env_hash()
# CSS: scan ALL components (not per-page) for the static cache
sx_css = ""
sx_css_classes = ""
if registry_loaded():
classes: set[str] = set()
from .types import Island as _I, Component as _C
for val in _COMPONENT_ENV.values():
if isinstance(val, (_I, _C)) and val.css_classes:
classes.update(val.css_classes)
classes.update(["bg-stone-50", "text-stone-900"])
rules = lookup_rules(classes)
sx_css = get_preamble() + rules
sx_css_classes = store_css_hash(classes)
pages_sx = _build_pages_sx(_ca.name)
_shell_cfg = _ca.config.get("SX_SHELL", {})
static = dict(
component_hash=component_hash,
component_defs=component_defs,
pages_sx=pages_sx,
sx_css=sx_css,
sx_css_classes=sx_css_classes,
sx_js_hash=_script_hash("sx-browser.js"),
body_js_hash=_script_hash("body.js"),
asset_url=_ca.config.get("ASSET_URL", "/static"),
head_scripts=_shell_cfg.get("head_scripts"),
inline_css=_shell_cfg.get("inline_css"),
inline_head_js=_shell_cfg.get("inline_head_js"),
init_sx=_shell_cfg.get("init_sx"),
body_scripts=_shell_cfg.get("body_scripts"),
)
t1 = time.monotonic()
import logging
logging.getLogger("sx.pages").info(
"[shell-static] computed in %.3fs, comp_defs=%d css=%d pages=%d",
t1 - t0, len(component_defs), len(sx_css), len(pages_sx))
_cached_shell_static = static
_cached_shell_comp_hash = current_hash
return static
async def _build_shell_kwargs(ctx: dict, page_sx: str, *,
meta_html: str = "",
head_scripts: list[str] | None = None,
inline_css: str | None = None,
inline_head_js: str | None = None,
init_sx: str | None = None,
body_scripts: list[str] | None = None) -> dict[str, Any]:
"""Compute all shell kwargs for sx-page-shell.
Static parts (components, CSS, pages) are cached. Only per-request
values (title, csrf) are computed fresh.
"""
static = _get_shell_static()
asset_url = get_asset_url(ctx) or static["asset_url"]
title = ctx.get("base_title", "Rose Ash")
csrf = _get_csrf_token()
kwargs: dict[str, Any] = dict(static)
kwargs.update(
title=_html_escape(title),
asset_url=asset_url,
meta_html=meta_html,
csrf=_html_escape(csrf),
)
# Per-page CSS: scan THIS page's classes and add to cached CSS
from .css_registry import scan_classes_from_sx, lookup_rules, registry_loaded
if registry_loaded() and page_sx:
page_classes = scan_classes_from_sx(page_sx)
if page_classes:
extra_rules = lookup_rules(page_classes)
if extra_rules:
kwargs["sx_css"] = static["sx_css"] + extra_rules
# Cookie-based component caching
client_hash = _get_sx_comp_cookie()
if not _is_dev_mode() and client_hash and client_hash == static["component_hash"]:
kwargs["component_defs"] = ""
# Per-call overrides
if head_scripts is not None:
kwargs["head_scripts"] = head_scripts
if inline_css is not None:
kwargs["inline_css"] = inline_css
if inline_head_js is not None:
kwargs["inline_head_js"] = inline_head_js
if init_sx is not None:
kwargs["init_sx"] = init_sx
if body_scripts is not None:
kwargs["body_scripts"] = body_scripts
return kwargs
async def sx_page(ctx: dict, page_sx: str, *,
meta_html: str = "",
head_scripts: list[str] | None = None,
@@ -799,109 +955,18 @@ async def sx_page(ctx: dict, page_sx: str, *,
inline_head_js: str | None = None,
init_sx: str | None = None,
body_scripts: list[str] | None = None) -> str:
"""Return a minimal HTML shell that boots the page from sx source.
The browser loads component definitions and page sx, then sx.js
renders everything client-side. CSS rules are scanned from the sx
source and component defs, then injected as a <style> block.
The shell is rendered from the ~shared:shell/sx-page-shell SX component
(shared/sx/templates/shell.sx).
"""
from .jinja_bridge import components_for_page, css_classes_for_page
from .css_registry import lookup_rules, get_preamble, registry_loaded, store_css_hash
# Per-page component bundle: this page's deps + all :data page deps
from quart import current_app as _ca
component_defs, component_hash = components_for_page(page_sx, service=_ca.name)
# Check if client already has this version cached (via cookie)
# In dev mode, always send full source so edits are visible immediately
client_hash = _get_sx_comp_cookie()
if not _is_dev_mode() and client_hash and client_hash == component_hash:
# Client has current components cached — send empty source
component_defs = ""
# Scan for CSS classes — only from components this page uses + page source
sx_css = ""
sx_css_classes = ""
sx_css_hash = ""
if registry_loaded():
classes = css_classes_for_page(page_sx, service=_ca.name)
# Always include body classes
classes.update(["bg-stone-50", "text-stone-900"])
rules = lookup_rules(classes)
sx_css = get_preamble() + rules
sx_css_hash = store_css_hash(classes)
sx_css_classes = sx_css_hash
asset_url = get_asset_url(ctx)
title = ctx.get("base_title", "Rose Ash")
csrf = _get_csrf_token()
# Dev mode: pretty-print page sx for readable View Source
if _is_dev_mode() and page_sx and page_sx.startswith("("):
from .parser import parse as _parse, serialize as _serialize
try:
page_sx = _serialize(_parse(page_sx), pretty=True)
except Exception as e:
import logging
logging.getLogger("sx").warning("Pretty-print page_sx failed: %s", e)
# Page registry for client-side routing
import logging
_plog = logging.getLogger("sx.pages")
from quart import current_app
pages_sx = _build_pages_sx(current_app.name)
_plog.debug("sx_page: pages_sx %d bytes for service %s", len(pages_sx), current_app.name)
if pages_sx:
_plog.debug("sx_page: pages_sx first 200 chars: %s", pages_sx[:200])
# Ensure page_sx is a plain str, not SxExpr — _build_component_ast
# parses SxExpr back into AST, which _arender then evaluates as HTML
# instead of passing through as raw content for the script tag.
"""Return a minimal HTML shell that boots the page from sx source."""
# Ensure page_sx is a plain str
if isinstance(page_sx, SxExpr):
page_sx = "".join([page_sx])
# Per-app shell config: check explicit args, then app config, then defaults
from quart import current_app as _app
_shell_cfg = _app.config.get("SX_SHELL", {})
if head_scripts is None:
head_scripts = _shell_cfg.get("head_scripts")
if inline_css is None:
inline_css = _shell_cfg.get("inline_css")
if inline_head_js is None:
inline_head_js = _shell_cfg.get("inline_head_js")
if init_sx is None:
init_sx = _shell_cfg.get("init_sx")
if body_scripts is None:
body_scripts = _shell_cfg.get("body_scripts")
shell_kwargs: dict[str, Any] = dict(
title=_html_escape(title),
asset_url=asset_url,
meta_html=meta_html,
csrf=_html_escape(csrf),
component_hash=component_hash,
component_defs=component_defs,
pages_sx=pages_sx,
page_sx=page_sx,
sx_css=sx_css,
sx_css_classes=sx_css_classes,
sx_js_hash=_script_hash("sx-browser.js"),
body_js_hash=_script_hash("body.js"),
)
if head_scripts is not None:
shell_kwargs["head_scripts"] = head_scripts
if inline_css is not None:
shell_kwargs["inline_css"] = inline_css
if inline_head_js is not None:
shell_kwargs["inline_head_js"] = inline_head_js
if init_sx is not None:
shell_kwargs["init_sx"] = init_sx
if body_scripts is not None:
shell_kwargs["body_scripts"] = body_scripts
return await render_to_html("shared:shell/sx-page-shell", **shell_kwargs)
kwargs = await _build_shell_kwargs(
ctx, page_sx, meta_html=meta_html,
head_scripts=head_scripts, inline_css=inline_css,
inline_head_js=inline_head_js, init_sx=init_sx,
body_scripts=body_scripts)
kwargs["page_sx"] = page_sx
return await render_to_html("shared:shell/sx-page-shell", **kwargs)
_SX_STREAMING_RESOLVE = """\