The irreducible primitive set drops from 79 to 33. Everything that can be expressed in SX is now a library function in stdlib.sx, loaded after evaluator.sx and before render.sx. Moved to stdlib.sx (pure SX, no host dependency): - Logic: not - Comparison: != <= >= eq? eqv? equal? - Predicates: nil? boolean? number? string? list? dict? continuation? empty? odd? even? zero? contains? - Arithmetic: inc dec abs ceil round min max clamp - Collections: first last rest nth cons append reverse flatten range chunk-every zip-pairs vals has-key? merge assoc dissoc into - Strings: upcase downcase string-length substring string-contains? starts-with? ends-with? split join replace - Text: pluralize escape assert parse-datetime Remaining irreducible primitives (33): + - * / mod floor pow sqrt = < > type-of symbol-name keyword-name str slice index-of upper lower trim char-from-code list dict concat get len keys dict-set! append! random-int json-encode format-date parse-int format-decimal strip-tags sx-parse error apply All hosts: JS 957+1080, Python 744, OCaml 952 — zero regressions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
282 lines
11 KiB
Python
282 lines
11 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)
|
|
|
|
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)
|