"""Tests for the s-expression parser.""" import pytest from shared.sx.parser import parse, parse_all, serialize, ParseError from shared.sx.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") == [] # --------------------------------------------------------------------------- # Quasiquote # --------------------------------------------------------------------------- class TestQuasiquote: def test_quasiquote_symbol(self): result = parse("`x") assert result == [Symbol("quasiquote"), Symbol("x")] def test_quasiquote_list(self): result = parse("`(a b c)") assert result == [Symbol("quasiquote"), [Symbol("a"), Symbol("b"), Symbol("c")]] def test_unquote(self): result = parse(",x") assert result == [Symbol("unquote"), Symbol("x")] def test_splice_unquote(self): result = parse(",@xs") assert result == [Symbol("splice-unquote"), Symbol("xs")] def test_quasiquote_with_unquote(self): result = parse("`(a ,x b)") assert result == [Symbol("quasiquote"), [ Symbol("a"), [Symbol("unquote"), Symbol("x")], Symbol("b"), ]] def test_quasiquote_with_splice(self): result = parse("`(a ,@rest)") assert result == [Symbol("quasiquote"), [ Symbol("a"), [Symbol("splice-unquote"), Symbol("rest")], ]] def test_roundtrip_quasiquote(self): assert serialize(parse("`(a ,x ,@rest)")) == "`(a ,x ,@rest)" def test_roundtrip_unquote(self): assert serialize(parse(",x")) == ",x" def test_roundtrip_splice_unquote(self): assert serialize(parse(",@xs")) == ",@xs" # --------------------------------------------------------------------------- # 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 # --------------------------------------------------------------------------- # Reader macros # --------------------------------------------------------------------------- class TestReaderMacros: """Test #; datum comment, #|...| raw string, and #' quote shorthand.""" def test_datum_comment_discards(self): assert parse_all("#;(ignored) 42") == [42] def test_datum_comment_in_list(self): assert parse("(1 #;2 3)") == [1, 3] def test_datum_comment_nested(self): assert parse_all("#;(a (b c) d) 99") == [99] def test_raw_string_basic(self): assert parse('#|hello|') == "hello" def test_raw_string_with_quotes(self): assert parse('#|say "hi"|') == 'say "hi"' def test_raw_string_with_backslashes(self): assert parse('#|a\\nb|') == 'a\\nb' def test_raw_string_empty(self): assert parse('#||') == "" def test_quote_shorthand_symbol(self): assert parse("#'foo") == [Symbol("quote"), Symbol("foo")] def test_quote_shorthand_list(self): assert parse("#'(1 2 3)") == [Symbol("quote"), [1, 2, 3]] def test_hash_at_eof_errors(self): with pytest.raises(ParseError): parse("#") def test_unknown_reader_macro_errors(self): with pytest.raises(ParseError, match="Unknown reader macro"): parse("#x foo") def test_extensible_reader_macro(self): """Registered reader macros transform the next expression.""" from shared.sx.parser import register_reader_macro register_reader_macro("upper", lambda expr: str(expr).upper()) try: result = parse('#upper "hello"') assert result == "HELLO" finally: from shared.sx.parser import _READER_MACROS del _READER_MACROS["upper"]