Phase 1: s-expression core library + test infrastructure
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m9s

S-expression parser, evaluator, and primitive registry in shared/sexp/.
109 unit tests covering parsing, evaluation, special forms, lambdas,
closures, components (defcomp), and 60+ pure builtins.

Test infrastructure: Dockerfile.unit (tier 1, fast) and
Dockerfile.integration (tier 2, ffmpeg). Dev watch mode auto-reruns
on file changes. Deploy gate blocks push on test failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 13:26:18 +00:00
parent 996ddad2ea
commit 0fb87e3b1c
15 changed files with 2293 additions and 0 deletions

View File

View File

@@ -0,0 +1,326 @@
"""Tests for the s-expression evaluator."""
import pytest
from shared.sexp import parse, evaluate, EvalError, Symbol, Keyword, NIL
from shared.sexp.types import Lambda, Component
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def ev(text, env=None):
"""Parse and evaluate a single expression."""
return evaluate(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(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_negate(self):
assert ev("(- 5)") == -5
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
def test_clamp(self):
assert ev("(clamp 15 0 10)") == 10
assert ev("(clamp -5 0 10)") == 0
assert ev("(clamp 5 0 10)") == 5
# ---------------------------------------------------------------------------
# Comparison and predicates
# ---------------------------------------------------------------------------
class TestComparison:
def test_eq(self):
assert ev("(= 1 1)") is True
assert ev("(= 1 2)") is False
def test_lt_gt(self):
assert ev("(< 1 2)") is True
assert ev("(> 2 1)") is True
def test_predicates(self):
assert ev("(odd? 3)") is True
assert ev("(even? 4)") is True
assert ev("(zero? 0)") is True
assert ev("(nil? nil)") is True
assert ev('(string? "hi")') is True
assert ev("(number? 42)") is True
assert ev("(list? (list 1))") is True
assert ev("(dict? {:a 1})") is True
# ---------------------------------------------------------------------------
# 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_keys_vals(self):
assert ev("(keys {:a 1 :b 2})") == ["a", "b"]
assert ev("(vals {:a 1 :b 2})") == [1, 2]
def test_merge(self):
assert ev("(merge {:a 1} {:b 2} {:a 3})") == {"a": 3, "b": 2}
def test_assoc(self):
assert ev('(assoc {:a 1} :b 2)') == {"a": 1, "b": 2}
def test_dissoc(self):
assert ev('(dissoc {:a 1 :b 2} :a)') == {"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
def test_contains(self):
assert ev('(contains? {:a 1} "a")') is True
assert ev("(contains? (list 1 2 3) 2)") 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_map_indexed(self):
result = ev("(map-indexed (fn (i x) (+ i x)) (list 10 20 30))")
assert result == [10, 21, 32]
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) (if (> x 3) x nil)) (list 1 2 4 5))") == 4
def test_every(self):
assert ev("(every? (fn (x) (> x 0)) (list 1 2 3))") is True
assert ev("(every? (fn (x) (> x 2)) (list 1 2 3))") is False
# ---------------------------------------------------------------------------
# Strings
# ---------------------------------------------------------------------------
class TestStrings:
def test_str(self):
assert ev('(str "hello" " " "world")') == "hello world"
def test_str_numbers(self):
assert ev('(str "val=" 42)') == "val=42"
def test_upper_lower(self):
assert ev('(upper "hello")') == "HELLO"
assert ev('(lower "HELLO")') == "hello"
def test_join(self):
assert ev('(join ", " (list "a" "b" "c"))') == "a, b, c"
def test_split(self):
assert ev('(split "a,b,c" ",")') == ["a", "b", "c"]
# ---------------------------------------------------------------------------
# defcomp
# ---------------------------------------------------------------------------
class TestDefcomp:
def test_basic_component(self):
env = {}
ev("(defcomp ~card (&key title) title)", env)
assert isinstance(env["~card"], Component)
assert env["~card"].name == "card"
def test_component_call(self):
env = {}
ev("(defcomp ~greeting (&key name) (str \"Hello, \" name \"!\"))", env)
result = ev('(~greeting :name "Alice")', env)
assert result == "Hello, Alice!"
def test_component_with_children(self):
env = {}
ev("(defcomp ~wrapper (&key class &rest children) (list class children))", env)
result = ev('(~wrapper :class "box" 1 2 3)', env)
assert result == ["box", [1, 2, 3]]
def test_component_missing_kwarg_is_nil(self):
env = {}
ev("(defcomp ~opt (&key x y) (list x y))", env)
result = ev("(~opt :x 1)", env)
assert result == [1, NIL]
# ---------------------------------------------------------------------------
# Dict literal evaluation
# ---------------------------------------------------------------------------
class TestDictLiteral:
def test_dict_values_evaluated(self):
assert ev("{:a (+ 1 2) :b (* 3 4)}") == {"a": 3, "b": 12}
# ---------------------------------------------------------------------------
# set!
# ---------------------------------------------------------------------------
class TestSetBang:
def test_set_bang(self):
env = {"x": 1}
ev("(set! x 42)", env)
assert env["x"] == 42

View File

@@ -0,0 +1,191 @@
"""Tests for the s-expression parser."""
import pytest
from shared.sexp.parser import parse, parse_all, serialize, ParseError
from shared.sexp.types import Symbol, Keyword, NIL
# ---------------------------------------------------------------------------
# Atoms
# ---------------------------------------------------------------------------
class TestAtoms:
def test_integer(self):
assert parse("42") == 42
def test_negative_integer(self):
assert parse("-7") == -7
def test_float(self):
assert parse("3.14") == 3.14
def test_scientific(self):
assert parse("1e-3") == 0.001
def test_string(self):
assert parse('"hello world"') == "hello world"
def test_string_escapes(self):
assert parse(r'"line1\nline2"') == "line1\nline2"
assert parse(r'"tab\there"') == "tab\there"
assert parse(r'"say \"hi\""') == 'say "hi"'
def test_symbol(self):
assert parse("foo") == Symbol("foo")
def test_component_symbol(self):
s = parse("~card")
assert s == Symbol("~card")
assert s.is_component
def test_keyword(self):
assert parse(":class") == Keyword("class")
def test_true(self):
assert parse("true") is True
def test_false(self):
assert parse("false") is False
def test_nil(self):
assert parse("nil") is NIL
# ---------------------------------------------------------------------------
# Lists
# ---------------------------------------------------------------------------
class TestLists:
def test_empty_list(self):
assert parse("()") == []
def test_simple_list(self):
assert parse("(1 2 3)") == [1, 2, 3]
def test_mixed_list(self):
result = parse('(div :class "main")')
assert result == [Symbol("div"), Keyword("class"), "main"]
def test_nested_list(self):
result = parse("(a (b c) d)")
assert result == [Symbol("a"), [Symbol("b"), Symbol("c")], Symbol("d")]
def test_vector_sugar(self):
assert parse("[1 2 3]") == [1, 2, 3]
# ---------------------------------------------------------------------------
# Maps
# ---------------------------------------------------------------------------
class TestMaps:
def test_simple_map(self):
result = parse('{:a 1 :b 2}')
assert result == {"a": 1, "b": 2}
def test_nested_map(self):
result = parse('{:x {:y 3}}')
assert result == {"x": {"y": 3}}
def test_string_keys(self):
result = parse('{"name" "alice"}')
assert result == {"name": "alice"}
# ---------------------------------------------------------------------------
# Comments
# ---------------------------------------------------------------------------
class TestComments:
def test_line_comment(self):
assert parse("; comment\n42") == 42
def test_inline_comment(self):
result = parse("(a ; stuff\nb)")
assert result == [Symbol("a"), Symbol("b")]
# ---------------------------------------------------------------------------
# parse_all
# ---------------------------------------------------------------------------
class TestParseAll:
def test_multiple(self):
results = parse_all("1 2 3")
assert results == [1, 2, 3]
def test_multiple_lists(self):
results = parse_all("(a) (b)")
assert results == [[Symbol("a")], [Symbol("b")]]
def test_empty(self):
assert parse_all("") == []
assert parse_all(" ; only comments\n") == []
# ---------------------------------------------------------------------------
# Errors
# ---------------------------------------------------------------------------
class TestErrors:
def test_unterminated_list(self):
with pytest.raises(ParseError):
parse("(a b")
def test_unterminated_string(self):
with pytest.raises(ParseError):
parse('"hello')
def test_unexpected_closer(self):
with pytest.raises(ParseError):
parse(")")
def test_trailing_content(self):
with pytest.raises(ParseError):
parse("1 2")
# ---------------------------------------------------------------------------
# Serialization
# ---------------------------------------------------------------------------
class TestSerialize:
def test_int(self):
assert serialize(42) == "42"
def test_float(self):
assert serialize(3.14) == "3.14"
def test_string(self):
assert serialize("hello") == '"hello"'
def test_string_escapes(self):
assert serialize('say "hi"') == '"say \\"hi\\""'
def test_symbol(self):
assert serialize(Symbol("foo")) == "foo"
def test_keyword(self):
assert serialize(Keyword("class")) == ":class"
def test_bool(self):
assert serialize(True) == "true"
assert serialize(False) == "false"
def test_nil(self):
assert serialize(None) == "nil"
assert serialize(NIL) == "nil"
def test_list(self):
assert serialize([Symbol("a"), 1, 2]) == "(a 1 2)"
def test_empty_list(self):
assert serialize([]) == "()"
def test_dict(self):
result = serialize({"a": 1, "b": 2})
assert result == "{:a 1 :b 2}"
def test_roundtrip(self):
original = '(div :class "main" (p "hello") (span 42))'
assert serialize(parse(original)) == original