Delete sx_ref.py — OCaml is the sole SX evaluator

Removes the 5993-line bootstrapped Python evaluator (sx_ref.py) and all
code that depended on it exclusively. Both bootstrappers (JS + OCaml)
now use a new synchronous OCaml bridge (ocaml_sync.py) to run the
transpiler. JS build produces identical output; OCaml bootstrap produces
byte-identical sx_ref.ml.

Key changes:
- New shared/sx/ocaml_sync.py: sync subprocess bridge to sx_server.exe
- hosts/javascript/bootstrap.py: serialize defines → temp file → OCaml eval
- hosts/ocaml/bootstrap.py: same pattern for OCaml transpiler
- shared/sx/{html,async_eval,resolver,jinja_bridge,handlers,pages,deps,helpers}:
  stub or remove sx_ref imports; runtime uses OCaml bridge (SX_USE_OCAML=1)
- sx/sxc/pages: parse defpage/defhandler from AST instead of Python eval
- hosts/ocaml/lib/sx_primitives.ml: append handles non-list 2nd arg per spec
- Deleted: sx_ref.py, async_eval_ref.py, 6 Python test runners, misc ref/ files

Test results: JS 1078/1078, OCaml 1114/1114.
sx_docs SSR has pre-existing rendering issues to investigate separately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 14:32:55 +00:00
parent 482bc0ca5e
commit d735e28b39
30 changed files with 400 additions and 9239 deletions

View File

@@ -32,7 +32,7 @@ logger = logging.getLogger("sx.pages")
def _eval_error_sx(e: EvalError, context: str) -> str:
"""Render an EvalError as SX content that's visible to the developer."""
from .ref.sx_ref import escape_html as _esc
from html import escape as _esc
msg = _esc(str(e))
ctx = _esc(context)
return (
@@ -141,29 +141,60 @@ def get_page_helpers(service: str) -> dict[str, Any]:
# Loading — parse .sx files and collect PageDef instances
# ---------------------------------------------------------------------------
def _parse_defpage(expr: list) -> PageDef | None:
"""Extract PageDef from a (defpage name :path ... :content ...) form."""
from .types import Keyword
if len(expr) < 3:
return None
name = expr[1].name if hasattr(expr[1], 'name') else str(expr[1])
kwargs: dict[str, Any] = {}
i = 2
while i < len(expr):
item = expr[i]
if isinstance(item, Keyword) and i + 1 < len(expr):
kwargs[item.name] = expr[i + 1]
i += 2
else:
i += 1
path = kwargs.get("path")
if not path or not isinstance(path, str):
return None
auth = kwargs.get("auth", "public")
if hasattr(auth, 'name'):
auth = auth.name
return PageDef(
name=name, path=path, auth=auth,
layout=kwargs.get("layout"),
cache=None,
data_expr=kwargs.get("data"),
content_expr=kwargs.get("content"),
filter_expr=kwargs.get("filter"),
aside_expr=kwargs.get("aside"),
menu_expr=kwargs.get("menu"),
)
def load_page_file(filepath: str, service_name: str) -> list[PageDef]:
"""Parse an .sx file, evaluate it, and register any PageDef values."""
"""Parse an .sx file and register any defpage definitions."""
from .parser import parse_all
from .ref.sx_ref import eval_expr as _raw_eval, trampoline as _trampoline
_eval = lambda expr, env: _trampoline(_raw_eval(expr, env))
from .jinja_bridge import get_component_env
with open(filepath, encoding="utf-8") as f:
source = f.read()
# Seed env with component definitions so pages can reference components
env = dict(get_component_env())
exprs = parse_all(source)
pages: list[PageDef] = []
for expr in exprs:
_eval(expr, env)
# Collect all PageDef values from the env
for key, val in env.items():
if isinstance(val, PageDef):
register_page(service_name, val)
pages.append(val)
if (isinstance(expr, list) and expr
and hasattr(expr[0], 'name') and expr[0].name == "defpage"):
pd = _parse_defpage(expr)
if pd:
register_page(service_name, pd)
pages.append(pd)
return pages
@@ -177,6 +208,50 @@ def load_page_dir(directory: str, service_name: str) -> list[PageDef]:
return pages
# ---------------------------------------------------------------------------
# URL → SX expression conversion (was in sx_ref.py, pure logic)
# ---------------------------------------------------------------------------
def prepare_url_expr(url_path: str, env: dict) -> list:
"""Convert a URL path to an SX expression, quoting unknown symbols."""
from .parser import parse_all
from .types import Symbol
if not url_path or url_path == "/":
return []
trimmed = url_path.lstrip("/")
sx_source = trimmed.replace(".", " ")
exprs = parse_all(sx_source)
if not exprs:
return []
expr = exprs[0]
if not isinstance(expr, list):
return expr
# Auto-quote unknown symbols (not in env, not keywords/components)
return _auto_quote(expr, env)
def _auto_quote(expr, env: dict):
from .types import Symbol
if not isinstance(expr, list) or not expr:
return expr
head = expr[0]
children = []
for child in expr[1:]:
if isinstance(child, list):
children.append(_auto_quote(child, env))
elif isinstance(child, Symbol):
name = child.name
if (name in env or name.startswith(":") or
name.startswith("~") or name.startswith("!")):
children.append(child)
else:
children.append(name) # quote as string
else:
children.append(child)
return [head] + children
# ---------------------------------------------------------------------------
# Page execution
# ---------------------------------------------------------------------------