Fix quasiquote flattening bug, decouple relations from evaluator

- Fix qq-expand in eval.sx: use concat+list instead of append to prevent
  nested lists from being flattened during quasiquote expansion
- Update append primitive to match spec ("if x is list, concatenate")
- Rebuild sx_ref.py with quasiquote fix
- Make relations.py self-contained: parse defrelation AST directly
  without depending on the evaluator (25/25 tests pass)
- Replace hand-written JSEmitter with js.sx self-hosting bootstrapper
- Guard server-only tests in test-eval.sx with runtime check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 04:53:34 +00:00
parent 46cd179703
commit 3906ab3558
12 changed files with 678 additions and 4526 deletions

View File

@@ -4,11 +4,14 @@ Relation registry — declarative entity relationship definitions.
Relations are defined as s-expressions using ``defrelation`` and stored
in a global registry. All services load the same definitions at startup
via ``load_relation_registry()``.
No evaluator dependency — defrelation forms are parsed directly from the
AST since they're just structured data (keyword args → RelationDef).
"""
from __future__ import annotations
from shared.sx.types import RelationDef
from shared.sx.types import Keyword, RelationDef, Symbol
# ---------------------------------------------------------------------------
@@ -48,6 +51,102 @@ def clear_registry() -> None:
_RELATION_REGISTRY.clear()
# ---------------------------------------------------------------------------
# defrelation parsing — direct AST walk, no evaluator needed
# ---------------------------------------------------------------------------
_VALID_CARDINALITIES = {"one-to-one", "one-to-many", "many-to-many"}
_VALID_NAV = {"submenu", "tab", "badge", "inline", "hidden"}
class RelationError(Exception):
"""Error parsing a defrelation form."""
pass
def _parse_defrelation(expr: list) -> RelationDef:
"""Parse a (defrelation :name :key val ...) AST into a RelationDef."""
if len(expr) < 2:
raise RelationError("defrelation requires a name")
name_kw = expr[1]
if not isinstance(name_kw, Keyword):
raise RelationError(
f"defrelation name must be a keyword, got {type(name_kw).__name__}"
)
rel_name = name_kw.name
# Parse keyword args
kwargs: dict[str, str | None] = {}
i = 2
while i < len(expr):
key = expr[i]
if isinstance(key, Keyword):
if i + 1 < len(expr):
val = expr[i + 1]
kwargs[key.name] = val.name if isinstance(val, Keyword) else val
i += 2
else:
kwargs[key.name] = None
i += 1
else:
i += 1
for field in ("from", "to", "cardinality"):
if field not in kwargs:
raise RelationError(
f"defrelation {rel_name} missing required :{field}"
)
card = kwargs["cardinality"]
if card not in _VALID_CARDINALITIES:
raise RelationError(
f"defrelation {rel_name}: invalid cardinality {card!r}, "
f"expected one of {_VALID_CARDINALITIES}"
)
nav = kwargs.get("nav", "hidden")
if nav not in _VALID_NAV:
raise RelationError(
f"defrelation {rel_name}: invalid nav {nav!r}, "
f"expected one of {_VALID_NAV}"
)
return RelationDef(
name=rel_name,
from_type=kwargs["from"],
to_type=kwargs["to"],
cardinality=card,
inverse=kwargs.get("inverse"),
nav=nav,
nav_icon=kwargs.get("nav-icon"),
nav_label=kwargs.get("nav-label"),
)
def evaluate_defrelation(expr: list) -> RelationDef:
"""Parse a defrelation form, register it, and return the RelationDef.
Also handles (begin (defrelation ...) ...) wrappers.
"""
if not isinstance(expr, list) or not expr:
raise RelationError(f"Expected list expression, got {type(expr).__name__}")
head = expr[0]
if isinstance(head, Symbol) and head.name == "begin":
result = None
for child in expr[1:]:
result = evaluate_defrelation(child)
return result
if not (isinstance(head, Symbol) and head.name == "defrelation"):
raise RelationError(f"Expected defrelation, got {head}")
defn = _parse_defrelation(expr)
register_relation(defn)
return defn
# ---------------------------------------------------------------------------
# Built-in relation definitions (s-expression source)
# ---------------------------------------------------------------------------
@@ -94,8 +193,7 @@ _BUILTIN_RELATIONS = '''
def load_relation_registry() -> None:
"""Parse built-in defrelation s-expressions and populate the registry."""
from shared.sx.evaluator import evaluate
from shared.sx.parser import parse
tree = parse(_BUILTIN_RELATIONS)
evaluate(tree)
evaluate_defrelation(tree)