"""Test bootstrapper transpilation: JSEmitter and PyEmitter.""" from __future__ import annotations import os import re import pytest from shared.sx.parser import parse, parse_all from shared.sx.ref.bootstrap_js import ( JSEmitter, ADAPTER_FILES, SPEC_MODULES, extract_defines, compile_ref_to_js, ) from shared.sx.ref.bootstrap_py import PyEmitter from shared.sx.types import Symbol, Keyword class TestJSEmitterNativeDict: """JS bootstrapper must handle native Python dicts from {:key val} syntax.""" def test_simple_string_values(self): expr = parse('{"name" "hello"}') assert isinstance(expr, dict) js = JSEmitter().emit(expr) assert js == '{"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"))}') js = JSEmitter().emit(expr) assert "parseRoutePattern" in js assert "Symbol" not in js assert js == '{"parsed": parseRoutePattern(get(page, "path"))}' def test_multiple_keys(self): expr = parse('{"a" 1 "b" (+ x 2)}') js = JSEmitter().emit(expr) assert '"a": 1' in js assert '"b": (x + 2)' in js def test_nested_dict(self): expr = parse('{"outer" {"inner" 42}}') js = JSEmitter().emit(expr) assert '{"outer": {"inner": 42}}' == js def test_nil_value(self): expr = parse('{"key" nil}') js = JSEmitter().emit(expr) assert '"key": NIL' in js 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. Catches: spec modules not included, _mangle producing wrong names for defines, transpilation silently dropping definitions. """ 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) emitter = JSEmitter() missing = [] for sx_name in sorted(all_defs): js_name = emitter._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 test_renames_values_are_unique(self): """RENAMES should not map different SX names to the same JS name. Duplicate JS names would cause one definition to silently shadow another. """ renames = JSEmitter.RENAMES seen: dict[str, str] = {} dupes = [] for sx_name, js_name in sorted(renames.items()): if js_name in seen: # Allow intentional aliases (e.g. has-key? and dict-has? # both → dictHas) dupes.append( f" {sx_name} → {js_name} (same as {seen[js_name]})" ) else: seen[js_name] = sx_name # Intentional aliases — these are expected duplicates # (e.g. has-key? and dict-has? both map to dictHas) # Don't fail for these, just document them # The test serves as a warning for accidental duplicates 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. Primitives are called from runtime-evaluated SX (island bodies, user components) via getPrimitive(). If a primitive is declared in primitives.sx but not in PRIMITIVES[...], island code gets "Undefined symbol" errors. """ 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 — declared in primitives.sx under alternate names but # served via canonical PRIMITIVES entries 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 (require continuations extension) 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. These are the reactive primitives that island SX code calls via getPrimitive(). If any is missing, islands with reactive state fail at runtime. """ 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)) )