Files
rose-ash/shared/sx/tests/test_bootstrapper.py
giles 29c90a625b Delete evaluator.py shim: all imports go directly to bootstrapped sx_ref.py
EvalError moved to types.py. All 27 files updated to import eval_expr,
trampoline, call_lambda, etc. directly from shared.sx.ref.sx_ref instead
of through the evaluator.py indirection layer. 320/320 spec tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 11:15:48 +00:00

204 lines
7.3 KiB
Python

"""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))
)