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>
302 lines
12 KiB
Python
302 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Bootstrap compiler: js.sx (self-hosting SX-to-JS translator) → sx-browser.js.
|
|
|
|
This is the canonical JS bootstrapper. js.sx is loaded into the Python evaluator,
|
|
which uses it to translate the .sx spec files into JavaScript. Platform code
|
|
(types, primitives, DOM interface) comes from platform_js.py.
|
|
|
|
Usage:
|
|
python run_js_sx.py # stdout
|
|
python run_js_sx.py -o shared/static/scripts/sx-browser.js # file
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", ".."))
|
|
if _PROJECT not in sys.path:
|
|
sys.path.insert(0, _PROJECT)
|
|
|
|
import tempfile
|
|
from shared.sx.parser import serialize
|
|
from hosts.javascript.platform import (
|
|
extract_defines,
|
|
ADAPTER_FILES, ADAPTER_DEPS, SPEC_MODULES, SPEC_MODULE_ORDER, EXTENSION_NAMES,
|
|
PREAMBLE, PLATFORM_JS_PRE, PLATFORM_JS_POST,
|
|
PRIMITIVES_JS_MODULES, _ALL_JS_MODULES, _assemble_primitives_js,
|
|
PLATFORM_DEPS_JS, PLATFORM_PARSER_JS, PLATFORM_DOM_JS,
|
|
PLATFORM_ENGINE_PURE_JS, PLATFORM_ORCHESTRATION_JS, PLATFORM_BOOT_JS,
|
|
PLATFORM_CEK_JS, CEK_FIXUPS_JS,
|
|
CONTINUATIONS_JS, ASYNC_IO_JS,
|
|
fixups_js, public_api_js, EPILOGUE,
|
|
)
|
|
|
|
|
|
_bridge = None # cached OcamlSync instance
|
|
|
|
|
|
def _get_bridge():
|
|
"""Get or create the OCaml sync bridge with transpiler loaded."""
|
|
global _bridge
|
|
if _bridge is not None:
|
|
return _bridge
|
|
from shared.sx.ocaml_sync import OcamlSync
|
|
_bridge = OcamlSync()
|
|
_bridge.load(os.path.join(_HERE, "transpiler.sx"))
|
|
return _bridge
|
|
|
|
|
|
def load_js_sx():
|
|
"""Load js.sx transpiler into the OCaml kernel. Returns the bridge."""
|
|
return _get_bridge()
|
|
|
|
|
|
def compile_ref_to_js(
|
|
adapters: list[str] | None = None,
|
|
modules: list[str] | None = None,
|
|
extensions: list[str] | None = None,
|
|
spec_modules: list[str] | None = None,
|
|
) -> str:
|
|
"""Compile SX spec files to JavaScript using js.sx.
|
|
|
|
Args:
|
|
adapters: List of adapter names to include. None = all.
|
|
modules: List of primitive module names. None = all.
|
|
extensions: List of extensions (continuations). None = none.
|
|
spec_modules: List of spec modules (deps, router, signals). None = auto.
|
|
"""
|
|
from datetime import datetime, timezone
|
|
|
|
# Source directories: core spec and web framework
|
|
_source_dirs = [
|
|
os.path.join(_PROJECT, "spec"), # Core spec
|
|
os.path.join(_PROJECT, "web"), # Web framework
|
|
]
|
|
bridge = _get_bridge()
|
|
|
|
# Resolve adapter set
|
|
if adapters is None:
|
|
adapter_set = set(ADAPTER_FILES.keys())
|
|
else:
|
|
adapter_set = set()
|
|
for a in adapters:
|
|
if a not in ADAPTER_FILES:
|
|
raise ValueError(f"Unknown adapter: {a!r}. Valid: {', '.join(ADAPTER_FILES)}")
|
|
adapter_set.add(a)
|
|
for dep in ADAPTER_DEPS.get(a, []):
|
|
adapter_set.add(dep)
|
|
|
|
# Resolve spec modules
|
|
spec_mod_set = set()
|
|
if spec_modules:
|
|
for sm in spec_modules:
|
|
if sm not in SPEC_MODULES:
|
|
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
|
|
spec_mod_set.add(sm)
|
|
if "dom" in adapter_set and "signals" in SPEC_MODULES:
|
|
spec_mod_set.add("signals")
|
|
if "boot" in adapter_set:
|
|
spec_mod_set.add("router")
|
|
spec_mod_set.add("deps")
|
|
if "page-helpers" in SPEC_MODULES:
|
|
spec_mod_set.add("page-helpers")
|
|
# CEK is always included (part of evaluator.sx core file)
|
|
has_cek = True
|
|
has_deps = "deps" in spec_mod_set
|
|
has_router = "router" in spec_mod_set
|
|
has_page_helpers = "page-helpers" in spec_mod_set
|
|
|
|
# Resolve extensions
|
|
ext_set = set()
|
|
if extensions:
|
|
for e in extensions:
|
|
if e not in EXTENSION_NAMES:
|
|
raise ValueError(f"Unknown extension: {e!r}. Valid: {', '.join(EXTENSION_NAMES)}")
|
|
ext_set.add(e)
|
|
has_continuations = "continuations" in ext_set
|
|
|
|
# Build file list: core evaluator + adapters + spec modules
|
|
# evaluator.sx = merged frames + eval utilities + CEK machine
|
|
sx_files = [
|
|
("evaluator.sx", "evaluator (frames + eval + CEK)"),
|
|
# stdlib.sx is loaded at runtime via eval, not transpiled —
|
|
# transpiling it would shadow native PRIMITIVES in module scope.
|
|
("freeze.sx", "freeze (serializable state boundaries)"),
|
|
("content.sx", "content (content-addressed computation)"),
|
|
("render.sx", "render (core)"),
|
|
("web-forms.sx", "web-forms (defstyle, deftype, defeffect, defrelation)"),
|
|
]
|
|
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"):
|
|
if name in adapter_set:
|
|
sx_files.append(ADAPTER_FILES[name])
|
|
# Use explicit ordering for spec modules (respects dependencies)
|
|
for name in SPEC_MODULE_ORDER:
|
|
if name in spec_mod_set:
|
|
sx_files.append(SPEC_MODULES[name])
|
|
# Any spec modules not in the order list (future-proofing)
|
|
for name in sorted(spec_mod_set):
|
|
if name not in SPEC_MODULE_ORDER:
|
|
sx_files.append(SPEC_MODULES[name])
|
|
|
|
has_html = "html" in adapter_set
|
|
has_sx = "sx" in adapter_set
|
|
has_dom = "dom" in adapter_set
|
|
has_engine = "engine" in adapter_set
|
|
has_orch = "orchestration" in adapter_set
|
|
has_boot = "boot" in adapter_set
|
|
has_parser = "parser" in adapter_set
|
|
has_signals = "signals" in spec_mod_set
|
|
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
|
|
|
|
# Platform JS blocks keyed by adapter name
|
|
adapter_platform = {
|
|
"parser": PLATFORM_PARSER_JS,
|
|
"dom": PLATFORM_DOM_JS,
|
|
"engine": PLATFORM_ENGINE_PURE_JS,
|
|
"orchestration": PLATFORM_ORCHESTRATION_JS,
|
|
"boot": PLATFORM_BOOT_JS,
|
|
}
|
|
|
|
# Determine primitive modules
|
|
prim_modules = None
|
|
if modules is not None:
|
|
prim_modules = [m for m in _ALL_JS_MODULES if m.startswith("core.")]
|
|
for m in modules:
|
|
if m not in prim_modules:
|
|
if m not in PRIMITIVES_JS_MODULES:
|
|
raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_JS_MODULES)}")
|
|
prim_modules.append(m)
|
|
|
|
# Build output
|
|
parts = []
|
|
parts.append(PREAMBLE)
|
|
parts.append(PLATFORM_JS_PRE)
|
|
parts.append('\n // =========================================================================')
|
|
parts.append(' // Primitives')
|
|
parts.append(' // =========================================================================\n')
|
|
parts.append(' var PRIMITIVES = {};')
|
|
parts.append(_assemble_primitives_js(prim_modules))
|
|
parts.append(PLATFORM_JS_POST)
|
|
|
|
if has_deps:
|
|
parts.append(PLATFORM_DEPS_JS)
|
|
|
|
if has_parser:
|
|
parts.append(adapter_platform["parser"])
|
|
|
|
# CEK platform aliases must come before transpiled cek.sx (which uses them)
|
|
if has_cek:
|
|
parts.append(PLATFORM_CEK_JS)
|
|
|
|
# Translate each spec file using js.sx
|
|
def _find_sx(filename):
|
|
for d in _source_dirs:
|
|
p = os.path.join(d, filename)
|
|
if os.path.exists(p):
|
|
return p
|
|
return None
|
|
|
|
for filename, label in sx_files:
|
|
filepath = _find_sx(filename)
|
|
if not filepath:
|
|
continue
|
|
with open(filepath) as f:
|
|
src = f.read()
|
|
defines = extract_defines(src)
|
|
|
|
sx_defines = [[name, expr] for name, expr in defines]
|
|
|
|
parts.append(f"\n // === Transpiled from {label} ===\n")
|
|
# Serialize defines to SX, write to temp file, load into OCaml kernel
|
|
defines_sx = serialize(sx_defines)
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".sx", delete=False) as tmp:
|
|
tmp.write(f"(define _defines \'{defines_sx})\n")
|
|
tmp_path = tmp.name
|
|
try:
|
|
bridge.load(tmp_path)
|
|
finally:
|
|
os.unlink(tmp_path)
|
|
result = bridge.eval("(js-translate-file _defines)")
|
|
parts.append(result)
|
|
|
|
# Platform JS for selected adapters
|
|
if not has_dom:
|
|
parts.append("\n var _hasDom = false;\n")
|
|
|
|
# CEK fixups + general fixups BEFORE boot (boot hydrates islands that need these)
|
|
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers))
|
|
if has_cek:
|
|
parts.append(CEK_FIXUPS_JS)
|
|
|
|
# Load stdlib.sx via eval (NOT transpiled) so defines go into the eval
|
|
# env, not the module scope. This prevents stdlib functions from
|
|
# shadowing native PRIMITIVES aliases used by transpiled evaluator code.
|
|
stdlib_path = _find_sx("stdlib.sx")
|
|
if stdlib_path:
|
|
with open(stdlib_path) as f:
|
|
stdlib_src = f.read()
|
|
# Escape for JS string literal
|
|
stdlib_escaped = stdlib_src.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
|
parts.append(f'\n // === stdlib.sx (eval\'d at runtime, not transpiled) ===')
|
|
parts.append(f' (function() {{')
|
|
parts.append(f' var src = "{stdlib_escaped}";')
|
|
parts.append(f' var forms = sxParse(src);')
|
|
parts.append(f' var tmpEnv = merge({{}}, PRIMITIVES);')
|
|
parts.append(f' for (var i = 0; i < forms.length; i++) {{')
|
|
parts.append(f' trampoline(evalExpr(forms[i], tmpEnv));')
|
|
parts.append(f' }}')
|
|
parts.append(f' for (var k in tmpEnv) {{')
|
|
parts.append(f' if (!PRIMITIVES[k]) PRIMITIVES[k] = tmpEnv[k];')
|
|
parts.append(f' }}')
|
|
parts.append(f' }})();\n')
|
|
|
|
for name in ("dom", "engine", "orchestration", "boot"):
|
|
if name in adapter_set and name in adapter_platform:
|
|
parts.append(adapter_platform[name])
|
|
# CONTINUATIONS_JS is the tree-walk shift/reset extension.
|
|
# With CEK as sole evaluator, continuations are handled natively by
|
|
# cek.sx (step-sf-reset, step-sf-shift). Skip the tree-walk extension.
|
|
# if has_continuations:
|
|
# parts.append(CONTINUATIONS_JS)
|
|
if has_dom:
|
|
parts.append(ASYNC_IO_JS)
|
|
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals, has_page_helpers, has_cek))
|
|
parts.append(EPILOGUE)
|
|
|
|
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
return "\n".join(parts).replace("BUILD_TIMESTAMP", build_ts)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
p = argparse.ArgumentParser(description="Bootstrap-compile SX reference spec to JavaScript via js.sx")
|
|
p.add_argument("--adapters", "-a",
|
|
help="Comma-separated adapter list (html,sx,dom,engine). Default: all")
|
|
p.add_argument("--modules", "-m",
|
|
help="Comma-separated primitive modules (core.* always included). Default: all")
|
|
p.add_argument("--extensions",
|
|
help="Comma-separated extensions (continuations). Default: none.")
|
|
p.add_argument("--spec-modules",
|
|
help="Comma-separated spec modules (deps). Default: none.")
|
|
default_output = os.path.join(_HERE, "..", "..", "static", "scripts", "sx-browser.js")
|
|
p.add_argument("--output", "-o", default=default_output,
|
|
help="Output file (default: shared/static/scripts/sx-browser.js)")
|
|
args = p.parse_args()
|
|
|
|
adapters = args.adapters.split(",") if args.adapters else None
|
|
modules = args.modules.split(",") if args.modules else None
|
|
extensions = args.extensions.split(",") if args.extensions else None
|
|
spec_modules = args.spec_modules.split(",") if args.spec_modules else None
|
|
js = compile_ref_to_js(adapters, modules, extensions, spec_modules)
|
|
|
|
with open(args.output, "w") as f:
|
|
f.write(js)
|
|
included = ", ".join(adapters) if adapters else "all"
|
|
mods = ", ".join(modules) if modules else "all"
|
|
ext_label = ", ".join(extensions) if extensions else "none"
|
|
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included}, modules: {mods}, extensions: {ext_label})",
|
|
file=sys.stderr)
|