HO forms (map, filter, reduce, etc.) now use call-fn which dispatches Lambda → call-lambda, native callable → apply, else → clear EvalError. Previously call-lambda crashed with AttributeError on native functions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
416 lines
13 KiB
Python
416 lines
13 KiB
Python
"""Tests for the transpiled sx_ref.py evaluator.
|
|
|
|
Runs the same test cases as test_evaluator.py and test_html.py but
|
|
against the bootstrap-compiled evaluator to verify correctness.
|
|
"""
|
|
|
|
import pytest
|
|
from shared.sx.parser import parse
|
|
from shared.sx.types import Symbol, Keyword, NIL, Lambda, Component, Macro
|
|
from shared.sx.ref import sx_ref
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def ev(text, env=None):
|
|
"""Parse and evaluate a single expression via sx_ref."""
|
|
return sx_ref.evaluate(parse(text), env)
|
|
|
|
|
|
def render(text, env=None):
|
|
"""Parse and render via sx_ref."""
|
|
return sx_ref.render(parse(text), env)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Literals and lookups
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestLiterals:
|
|
def test_int(self):
|
|
assert ev("42") == 42
|
|
|
|
def test_string(self):
|
|
assert ev('"hello"') == "hello"
|
|
|
|
def test_true(self):
|
|
assert ev("true") is True
|
|
|
|
def test_nil(self):
|
|
assert ev("nil") is NIL
|
|
|
|
def test_symbol_lookup(self):
|
|
assert ev("x", {"x": 10}) == 10
|
|
|
|
def test_undefined_symbol(self):
|
|
with pytest.raises(sx_ref.EvalError, match="Undefined symbol"):
|
|
ev("xyz")
|
|
|
|
def test_keyword_evaluates_to_name(self):
|
|
assert ev(":foo") == "foo"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Arithmetic
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestArithmetic:
|
|
def test_add(self):
|
|
assert ev("(+ 1 2 3)") == 6
|
|
|
|
def test_sub(self):
|
|
assert ev("(- 10 3)") == 7
|
|
|
|
def test_mul(self):
|
|
assert ev("(* 2 3 4)") == 24
|
|
|
|
def test_div(self):
|
|
assert ev("(/ 10 4)") == 2.5
|
|
|
|
def test_mod(self):
|
|
assert ev("(mod 7 3)") == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Special forms
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSpecialForms:
|
|
def test_if_true(self):
|
|
assert ev("(if true 1 2)") == 1
|
|
|
|
def test_if_false(self):
|
|
assert ev("(if false 1 2)") == 2
|
|
|
|
def test_if_no_else(self):
|
|
assert ev("(if false 1)") is NIL
|
|
|
|
def test_when_true(self):
|
|
assert ev("(when true 42)") == 42
|
|
|
|
def test_when_false(self):
|
|
assert ev("(when false 42)") is NIL
|
|
|
|
def test_and_short_circuit(self):
|
|
assert ev("(and true true 3)") == 3
|
|
assert ev("(and true false 3)") is False
|
|
|
|
def test_or_short_circuit(self):
|
|
assert ev("(or false false 3)") == 3
|
|
assert ev("(or false 2 3)") == 2
|
|
|
|
def test_let_scheme_style(self):
|
|
assert ev("(let ((x 10) (y 20)) (+ x y))") == 30
|
|
|
|
def test_let_clojure_style(self):
|
|
assert ev("(let (x 10 y 20) (+ x y))") == 30
|
|
|
|
def test_let_sequential(self):
|
|
assert ev("(let ((x 1) (y (+ x 1))) y)") == 2
|
|
|
|
def test_begin(self):
|
|
assert ev("(begin 1 2 3)") == 3
|
|
|
|
def test_quote(self):
|
|
result = ev("(quote (a b c))")
|
|
assert result == [Symbol("a"), Symbol("b"), Symbol("c")]
|
|
|
|
def test_cond_clojure(self):
|
|
assert ev("(cond false 1 true 2 :else 3)") == 2
|
|
|
|
def test_cond_else(self):
|
|
assert ev("(cond false 1 false 2 :else 99)") == 99
|
|
|
|
def test_case(self):
|
|
assert ev('(case 2 1 "one" 2 "two" :else "other")') == "two"
|
|
|
|
def test_thread_first(self):
|
|
assert ev("(-> 5 (+ 3) (* 2))") == 16
|
|
|
|
def test_define(self):
|
|
env = {}
|
|
ev("(define x 42)", env)
|
|
assert env["x"] == 42
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lambda
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestLambda:
|
|
def test_create_and_call(self):
|
|
assert ev("((fn (x) (* x x)) 5)") == 25
|
|
|
|
def test_closure(self):
|
|
result = ev("(let ((a 10)) ((fn (x) (+ x a)) 5))")
|
|
assert result == 15
|
|
|
|
def test_higher_order(self):
|
|
result = ev("(let ((double (fn (x) (* x 2)))) (double 7))")
|
|
assert result == 14
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Collections
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestCollections:
|
|
def test_list_constructor(self):
|
|
assert ev("(list 1 2 3)") == [1, 2, 3]
|
|
|
|
def test_dict_constructor(self):
|
|
assert ev("(dict :a 1 :b 2)") == {"a": 1, "b": 2}
|
|
|
|
def test_get_dict(self):
|
|
assert ev('(get {:a 1 :b 2} "a")') == 1
|
|
|
|
def test_get_list(self):
|
|
assert ev("(get (list 10 20 30) 1)") == 20
|
|
|
|
def test_first_last_rest(self):
|
|
assert ev("(first (list 1 2 3))") == 1
|
|
assert ev("(last (list 1 2 3))") == 3
|
|
assert ev("(rest (list 1 2 3))") == [2, 3]
|
|
|
|
def test_len(self):
|
|
assert ev("(len (list 1 2 3))") == 3
|
|
|
|
def test_concat(self):
|
|
assert ev("(concat (list 1 2) (list 3 4))") == [1, 2, 3, 4]
|
|
|
|
def test_cons(self):
|
|
assert ev("(cons 0 (list 1 2))") == [0, 1, 2]
|
|
|
|
def test_merge(self):
|
|
assert ev("(merge {:a 1} {:b 2} {:a 3})") == {"a": 3, "b": 2}
|
|
|
|
def test_empty(self):
|
|
assert ev("(empty? (list))") is True
|
|
assert ev("(empty? (list 1))") is False
|
|
assert ev("(empty? nil)") is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Higher-order forms
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestHigherOrder:
|
|
def test_map(self):
|
|
assert ev("(map (fn (x) (* x x)) (list 1 2 3 4))") == [1, 4, 9, 16]
|
|
|
|
def test_filter(self):
|
|
assert ev("(filter (fn (x) (> x 2)) (list 1 2 3 4))") == [3, 4]
|
|
|
|
def test_reduce(self):
|
|
assert ev("(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3))") == 6
|
|
|
|
def test_some(self):
|
|
assert ev("(some (fn (x) (> x 3)) (list 1 2 3 4 5))") is True
|
|
|
|
def test_for_each(self):
|
|
result = ev("(for-each (fn (x) x) (list 1 2 3))")
|
|
assert result is NIL
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# String ops
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestStrings:
|
|
def test_str(self):
|
|
assert ev('(str "hello" " " "world")') == "hello world"
|
|
|
|
def test_upper_lower(self):
|
|
assert ev('(upper "hello")') == "HELLO"
|
|
assert ev('(lower "HELLO")') == "hello"
|
|
|
|
def test_split_join(self):
|
|
assert ev('(split "a,b,c" ",")') == ["a", "b", "c"]
|
|
assert ev('(join "-" (list "a" "b"))') == "a-b"
|
|
|
|
def test_starts_ends(self):
|
|
assert ev('(starts-with? "hello" "hel")') is True
|
|
assert ev('(ends-with? "hello" "llo")') is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Components
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestComponents:
|
|
def test_defcomp_and_render(self):
|
|
env = {}
|
|
ev("(defcomp ~box (&key title) (div :class \"box\" title))", env)
|
|
result = render("(~box :title \"hi\")", env)
|
|
assert result == '<div class="box">hi</div>'
|
|
|
|
def test_defcomp_with_children(self):
|
|
env = {}
|
|
ev("(defcomp ~wrap (&rest children) (div children))", env)
|
|
result = render('(~wrap (span "a") (span "b"))', env)
|
|
assert result == '<div><span>a</span><span>b</span></div>'
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# HTML rendering
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestHTMLRendering:
|
|
def test_basic_element(self):
|
|
assert render("(div)") == "<div></div>"
|
|
|
|
def test_text_content(self):
|
|
assert render('(p "hello")') == "<p>hello</p>"
|
|
|
|
def test_attributes(self):
|
|
result = render('(a :href "/about" "link")')
|
|
assert result == '<a href="/about">link</a>'
|
|
|
|
def test_void_element(self):
|
|
result = render('(br)')
|
|
assert result == "<br />"
|
|
|
|
def test_nested(self):
|
|
result = render('(div (p "a") (p "b"))')
|
|
assert result == "<div><p>a</p><p>b</p></div>"
|
|
|
|
def test_fragment(self):
|
|
result = render('(<> (span "a") (span "b"))')
|
|
assert result == "<span>a</span><span>b</span>"
|
|
|
|
def test_conditional_rendering(self):
|
|
result = render('(if true (span "yes") (span "no"))')
|
|
assert result == "<span>yes</span>"
|
|
|
|
def test_map_rendering(self):
|
|
result = render('(map (fn (x) (li x)) (list "a" "b"))')
|
|
assert result == "<li>a</li><li>b</li>"
|
|
|
|
def test_html_escaping(self):
|
|
result = render('(span "<b>bold</b>")')
|
|
assert result == "<span><b>bold</b></span>"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Aser (SX wire format)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestAser:
|
|
def test_render_to_sx_basic(self):
|
|
expr = parse("(div :class \"foo\" \"hello\")")
|
|
result = sx_ref.render_to_sx(expr, {})
|
|
assert result == '(div :class "foo" "hello")'
|
|
|
|
def test_component_not_expanded(self):
|
|
expr = parse('(~card :title "hi")')
|
|
result = sx_ref.render_to_sx(expr, {})
|
|
assert result == '(~card :title "hi")'
|
|
|
|
def test_fragment(self):
|
|
expr = parse('(<> "a" "b")')
|
|
result = sx_ref.render_to_sx(expr, {})
|
|
assert result == '(<> "a" "b")'
|
|
|
|
def test_let_evaluates(self):
|
|
expr = parse('(let ((x 5)) x)')
|
|
result = sx_ref.render_to_sx(expr, {})
|
|
assert result == "5"
|
|
|
|
def test_if_evaluates(self):
|
|
expr = parse('(if true "yes" "no")')
|
|
result = sx_ref.render_to_sx(expr, {})
|
|
assert result == 'yes' # strings pass through unserialized
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Macros
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDefcomp:
|
|
def test_defcomp_basic(self):
|
|
"""defcomp should parse &key params without trying to eval &key as a symbol."""
|
|
env = {}
|
|
ev('(defcomp ~card (&key title) (div title))', env)
|
|
assert isinstance(env.get("~card"), Component)
|
|
|
|
def test_defcomp_render(self):
|
|
env = {}
|
|
ev('(defcomp ~card (&key title) (div title))', env)
|
|
result = render('(~card :title "hello")', env)
|
|
assert result == '<div>hello</div>'
|
|
|
|
def test_defcomp_with_children(self):
|
|
env = {}
|
|
ev('(defcomp ~wrap (&key title &rest children) (div title children))', env)
|
|
comp = env["~wrap"]
|
|
assert comp.has_children is True
|
|
assert "title" in comp.params
|
|
|
|
def test_defcomp_multiple_params(self):
|
|
env = {}
|
|
ev('(defcomp ~box (&key a b c) (div a b c))', env)
|
|
result = render('(~box :a "1" :b "2" :c "3")', env)
|
|
assert result == '<div>123</div>'
|
|
|
|
|
|
class TestDefhandler:
|
|
def test_defhandler_basic(self):
|
|
"""defhandler should parse &key params and create a HandlerDef."""
|
|
from shared.sx.types import HandlerDef
|
|
env = {}
|
|
ev('(defhandler link-card (&key slug keys) (div slug))', env)
|
|
hdef = env.get("handler:link-card")
|
|
assert isinstance(hdef, HandlerDef)
|
|
assert hdef.name == "link-card"
|
|
assert hdef.params == ["slug", "keys"]
|
|
|
|
def test_defquery_basic(self):
|
|
from shared.sx.types import QueryDef
|
|
env = {}
|
|
ev('(defquery get-post (&key slug) "Fetch a post" (list slug))', env)
|
|
qdef = env.get("query:get-post")
|
|
assert isinstance(qdef, QueryDef)
|
|
assert qdef.params == ["slug"]
|
|
assert qdef.doc == "Fetch a post"
|
|
|
|
def test_defaction_basic(self):
|
|
from shared.sx.types import ActionDef
|
|
env = {}
|
|
ev('(defaction save-post (&key title body) (list title body))', env)
|
|
adef = env.get("action:save-post")
|
|
assert isinstance(adef, ActionDef)
|
|
assert adef.params == ["title", "body"]
|
|
|
|
|
|
class TestHOWithNativeCallable:
|
|
def test_map_with_native_fn(self):
|
|
"""map should work with native callables (primitives), not just Lambda."""
|
|
result = ev('(map str (list 1 2 3))')
|
|
assert result == ["1", "2", "3"]
|
|
|
|
def test_filter_with_native_fn(self):
|
|
result = ev('(filter number? (list 1 "a" 2 "b" 3))')
|
|
assert result == [1, 2, 3]
|
|
|
|
def test_map_with_env_fn(self):
|
|
"""map should work with Python functions registered in env."""
|
|
env = {"double": lambda x: x * 2}
|
|
result = ev('(map double (list 1 2 3))', env)
|
|
assert result == [2, 4, 6]
|
|
|
|
def test_ho_non_callable_fails_fast(self):
|
|
"""Passing a non-callable to map should error clearly."""
|
|
import pytest
|
|
with pytest.raises(sx_ref.EvalError, match="Not callable"):
|
|
ev('(map 42 (list 1 2 3))')
|
|
|
|
|
|
class TestMacros:
|
|
def test_defmacro_and_expand(self):
|
|
env = {}
|
|
ev("(defmacro unless (test body) (list (quote if) (list (quote not) test) body))", env)
|
|
result = ev('(unless false "ran")', env)
|
|
assert result == "ran"
|