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

@@ -2,11 +2,12 @@
import pytest
from shared.sx.evaluator import evaluate, EvalError
from shared.sx.parser import parse
from shared.sx.relations import (
RelationError,
_RELATION_REGISTRY,
clear_registry,
evaluate_defrelation,
get_relation,
load_relation_registry,
relations_from,
@@ -38,7 +39,7 @@ class TestDefrelation:
:nav-icon "fa fa-shopping-bag"
:nav-label "markets")
''')
result = evaluate(tree)
result = evaluate_defrelation(tree)
assert isinstance(result, RelationDef)
assert result.name == "page->market"
assert result.from_type == "page"
@@ -54,7 +55,7 @@ class TestDefrelation:
(defrelation :a->b
:from "a" :to "b" :cardinality :one-to-one :nav :hidden)
''')
evaluate(tree)
evaluate_defrelation(tree)
assert get_relation("a->b") is not None
assert get_relation("a->b").cardinality == "one-to-one"
@@ -64,7 +65,7 @@ class TestDefrelation:
:from "page" :to "menu_node"
:cardinality :one-to-one :nav :hidden)
''')
result = evaluate(tree)
result = evaluate_defrelation(tree)
assert result.cardinality == "one-to-one"
assert result.inverse is None
assert result.nav == "hidden"
@@ -79,7 +80,7 @@ class TestDefrelation:
:nav-icon "fa fa-file-alt"
:nav-label "events")
''')
result = evaluate(tree)
result = evaluate_defrelation(tree)
assert result.cardinality == "many-to-many"
def test_default_nav_is_hidden(self):
@@ -87,7 +88,7 @@ class TestDefrelation:
(defrelation :x->y
:from "x" :to "y" :cardinality :one-to-many)
''')
result = evaluate(tree)
result = evaluate_defrelation(tree)
assert result.nav == "hidden"
def test_invalid_cardinality_raises(self):
@@ -95,42 +96,42 @@ class TestDefrelation:
(defrelation :bad
:from "a" :to "b" :cardinality :wrong)
''')
with pytest.raises(EvalError, match="invalid cardinality"):
evaluate(tree)
with pytest.raises(RelationError, match="invalid cardinality"):
evaluate_defrelation(tree)
def test_invalid_nav_raises(self):
tree = parse('''
(defrelation :bad
:from "a" :to "b" :cardinality :one-to-one :nav :bogus)
''')
with pytest.raises(EvalError, match="invalid nav"):
evaluate(tree)
with pytest.raises(RelationError, match="invalid nav"):
evaluate_defrelation(tree)
def test_missing_from_raises(self):
tree = parse('''
(defrelation :bad :to "b" :cardinality :one-to-one)
''')
with pytest.raises(EvalError, match="missing required :from"):
evaluate(tree)
with pytest.raises(RelationError, match="missing required :from"):
evaluate_defrelation(tree)
def test_missing_to_raises(self):
tree = parse('''
(defrelation :bad :from "a" :cardinality :one-to-one)
''')
with pytest.raises(EvalError, match="missing required :to"):
evaluate(tree)
with pytest.raises(RelationError, match="missing required :to"):
evaluate_defrelation(tree)
def test_missing_cardinality_raises(self):
tree = parse('''
(defrelation :bad :from "a" :to "b")
''')
with pytest.raises(EvalError, match="missing required :cardinality"):
evaluate(tree)
with pytest.raises(RelationError, match="missing required :cardinality"):
evaluate_defrelation(tree)
def test_name_must_be_keyword(self):
tree = parse('(defrelation "not-keyword" :from "a" :to "b" :cardinality :one-to-one)')
with pytest.raises(EvalError, match="must be a keyword"):
evaluate(tree)
with pytest.raises(RelationError, match="must be a keyword"):
evaluate_defrelation(tree)
# ---------------------------------------------------------------------------
@@ -154,7 +155,7 @@ class TestRegistry:
:from "page" :to "menu_node" :cardinality :one-to-one
:nav :hidden))
''')
evaluate(tree)
evaluate_defrelation(tree)
def test_get_relation(self):
self._load_sample()