Fix lambda multi-body, reactive island demos, and add React is Hypermedia essay
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
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>
This commit is contained in:
@@ -1,10 +1,20 @@
|
||||
"""Test bootstrapper transpilation: JSEmitter and PyEmitter."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from shared.sx.parser import parse
|
||||
from shared.sx.ref.bootstrap_js import JSEmitter
|
||||
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:
|
||||
@@ -61,3 +71,159 @@ class TestPyEmitterNativeDict:
|
||||
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))
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user