Files
rose-ash/shared/sx/tests/test_bootstrapper.py
giles 56589a81b2
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Fix lambda multi-body, reactive island demos, and add React is Hypermedia essay
Lambda multi-body fix: sf-lambda used (nth args 1), dropping all but the first
body expression. Fixed to collect all body expressions and wrap in (begin ...).
This was foundational — every multi-expression lambda in every island silently
dropped expressions after the first.

Reactive islands: fix dom-parent marker timing (first effect run before marker
is in DOM), fix :key eager evaluation, fix error boundary scope isolation,
fix resource/suspense reactive cond tracking, fix inc not available as JS var.

New essay: "React is Hypermedia" — argues that reactive islands are hypermedia
controls whose behavior is specified in SX, not a departure from hypermedia.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 20:00:44 +00:00

230 lines
8.3 KiB
Python

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