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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user