"""Test bootstrapper transpilation: js.sx and py.sx.""" from __future__ import annotations import os import re import pytest from shared.sx.parser import parse, parse_all from shared.sx.ref.run_js_sx import compile_ref_to_js, load_js_sx from shared.sx.ref.platform_js import ( ADAPTER_FILES, SPEC_MODULES, extract_defines, ) from shared.sx.ref.bootstrap_py import PyEmitter from shared.sx.types import Symbol, Keyword class TestJsSxTranslation: """js.sx self-hosting bootstrapper handles all SX constructs.""" def _translate(self, sx_source: str) -> str: """Translate a single SX expression to JS using js.sx.""" from shared.sx.ref.sx_ref import evaluate env = load_js_sx() expr = parse(sx_source) env["_def_expr"] = expr return evaluate( [Symbol("js-expr"), Symbol("_def_expr")], env ) def test_simple_dict(self): result = self._translate('{"name" "hello"}') assert '"name"' in result assert '"hello"' in result def test_function_call_in_dict(self): result = self._translate('{"parsed" (parse-route-pattern (get page "path"))}') assert "parseRoutePattern" in result assert "Symbol" not in result class TestPyEmitterNativeDict: """Python bootstrapper must handle native Python dicts from {:key val} syntax.""" def test_simple_string_values(self): expr = parse('{"name" "hello"}') py = PyEmitter().emit(expr) assert py == "{'name': 'hello'}" def test_function_call_value(self): """Dict value containing a function call must emit the call, not raw AST.""" expr = parse('{"parsed" (parse-route-pattern (get page "path"))}') py = PyEmitter().emit(expr) assert "parse_route_pattern" in py assert "Symbol" not in py def test_multiple_keys(self): expr = parse('{"a" 1 "b" (+ x 2)}') py = PyEmitter().emit(expr) assert "'a': 1" in py assert "'b': (x + 2)" in py # --------------------------------------------------------------------------- # Platform mapping and PRIMITIVES validation # # Catches two classes of bugs: # 1. Spec defines missing from compiled JS: a function defined in an .sx # spec file doesn't appear in the compiled output (e.g. because the # spec module wasn't included). # 2. Missing PRIMITIVES registration: a function is declared in # primitives.sx but not registered in PRIMITIVES[...], so runtime- # evaluated SX (island bodies) gets "Undefined symbol" errors. # --------------------------------------------------------------------------- _REF_DIR = os.path.join(os.path.dirname(__file__), "..", "ref") class TestPlatformMapping: """Verify compiled JS output contains all spec-defined functions.""" def test_compiled_defines_present_in_js(self): """Every top-level define from spec files must appear in compiled JS output.""" js_output = compile_ref_to_js( spec_modules=list(SPEC_MODULES.keys()), ) # Collect all var/function definitions from the JS defined_in_js = set(re.findall(r'\bvar\s+(\w+)\s*=', js_output)) defined_in_js.update(re.findall(r'\bfunction\s+(\w+)\s*\(', js_output)) all_defs: set[str] = set() for filename, _label in ( [("eval.sx", "eval"), ("render.sx", "render")] + list(ADAPTER_FILES.values()) + list(SPEC_MODULES.values()) ): filepath = os.path.join(_REF_DIR, filename) if not os.path.exists(filepath): continue for name, _expr in extract_defines(open(filepath).read()): all_defs.add(name) # Use js.sx RENAMES to map SX names → JS names env = load_js_sx() renames = env.get("js-renames", {}) missing = [] for sx_name in sorted(all_defs): # Check if there's an explicit rename js_name = renames.get(sx_name) if js_name is None: # Auto-mangle: hyphens → camelCase, ! → _b, ? → _p js_name = _auto_mangle(sx_name) if js_name not in defined_in_js: missing.append(f"{sx_name} → {js_name}") if missing: pytest.fail( f"{len(missing)} spec definitions not found in compiled JS " f"(compile with all spec_modules):\n " + "\n ".join(missing) ) def _auto_mangle(name: str) -> str: """Approximate js.sx's auto-mangle for SX name → JS identifier.""" # Remove ~, replace ?, !, -, *, > n = name if n.startswith("~"): n = n[1:] n = n.replace("?", "_p").replace("!", "_b") n = n.replace("->", "_to_").replace(">=", "_gte").replace("<=", "_lte") n = n.replace(">", "_gt").replace("<", "_lt") n = n.replace("*", "_star_").replace("/", "_slash_") # camelCase from hyphens parts = n.split("-") if len(parts) > 1: n = parts[0] + "".join(p.capitalize() for p in parts[1:]) return n class TestPrimitivesRegistration: """Functions callable from runtime-evaluated SX must be in PRIMITIVES[...].""" def test_declared_primitives_registered(self): """Every primitive declared in primitives.sx must have a PRIMITIVES[...] entry.""" from shared.sx.ref.boundary_parser import parse_primitives_sx declared = parse_primitives_sx() js_output = compile_ref_to_js() registered = set(re.findall(r'PRIMITIVES\["([^"]+)"\]', js_output)) # Aliases aliases = { "downcase": "lower", "upcase": "upper", "eq?": "=", "eqv?": "=", "equal?": "=", } for alias, canonical in aliases.items(): if alias in declared and canonical in registered: declared = declared - {alias} # Extension-only primitives extension_only = {"continuation?"} declared = declared - extension_only missing = declared - registered if missing: pytest.fail( f"{len(missing)} primitives declared in primitives.sx but " f"not registered in PRIMITIVES[...]:\n " + "\n ".join(sorted(missing)) ) def test_signal_runtime_primitives_registered(self): """Signal/reactive functions used by island bodies must be in PRIMITIVES.""" required = { "signal", "signal?", "deref", "reset!", "swap!", "computed", "effect", "batch", "resource", "def-store", "use-store", "emit-event", "on-event", "bridge-event", "promise-delayed", "promise-then", "dom-focus", "dom-tag-name", "dom-get-prop", "stop-propagation", "error-message", "schedule-idle", "set-interval", "clear-interval", "reactive-text", "create-text-node", "dom-set-text-content", "dom-listen", "dom-dispatch", "event-detail", } js_output = compile_ref_to_js() registered = set(re.findall(r'PRIMITIVES\["([^"]+)"\]', js_output)) missing = required - registered if missing: pytest.fail( f"{len(missing)} signal/reactive primitives not registered " f"in PRIMITIVES[...]:\n " + "\n ".join(sorted(missing)) )