#!/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) from shared.sx.parser import parse_all from shared.sx.types import Symbol 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, ) _js_sx_env = None # cached def load_js_sx() -> dict: """Load js.sx into an evaluator environment and return it.""" global _js_sx_env if _js_sx_env is not None: return _js_sx_env js_sx_path = os.path.join(_HERE, "transpiler.sx") with open(js_sx_path) as f: source = f.read() exprs = parse_all(source) from shared.sx.ref.sx_ref import evaluate, make_env env = make_env() for expr in exprs: evaluate(expr, env) _js_sx_env = env return env 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 from shared.sx.ref.sx_ref import evaluate ref_dir = os.path.join(_PROJECT, "shared", "sx", "ref") # Source directories: core spec, web framework, and legacy ref (for bootstrapper tools) _source_dirs = [ os.path.join(_PROJECT, "spec"), # Core spec os.path.join(_PROJECT, "web"), # Web framework ref_dir, # Legacy location (fallback) ] env = load_js_sx() # 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", "stdlib (library functions from former primitives)"), ("freeze.sx", "freeze (serializable state boundaries)"), ("content.sx", "content (content-addressed computation)"), ("render.sx", "render (core)"), ] 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") env["_defines"] = sx_defines result = evaluate( [Symbol("js-translate-file"), Symbol("_defines")], env, ) 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) 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)