Files
CljElixir/test/clj_elixir/reader_test.exs
Adam d8719b6d48 Phases 1-7: Complete CljElixir compiler through Malli schema adapter
Bootstrap compiler (reader, analyzer, transformer, compiler, Mix plugin),
core protocols (16 protocols for Map/List/Tuple/BitString), PersistentVector
(bit-partitioned trie), domain tools (clojurify/elixirify), BEAM concurrency
(receive, spawn, GenServer), control flow & macros (threading, try/catch,
destructuring, defmacro with quasiquote/auto-gensym), and Malli schema
adapter (m/=> specs, auto @type, recursive schemas, cross-references).

537 compiler tests + 55 Malli unit tests + 15 integration tests = 607 total.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:38:22 -04:00

821 lines
28 KiB
Elixir

defmodule CljElixir.ReaderTest do
use ExUnit.Case, async: true
alias CljElixir.Reader
# ═══════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════
defp read!(source) do
{:ok, forms} = Reader.read_string(source)
forms
end
defp read_one!(source) do
[form] = read!(source)
form
end
# ═══════════════════════════════════════════════════════════════════
# Literal types
# ═══════════════════════════════════════════════════════════════════
describe "integers" do
test "positive integer" do
assert read_one!("42") == 42
end
test "zero" do
assert read_one!("0") == 0
end
test "negative integer" do
assert read_one!("-3") == -3
end
test "multi-digit" do
assert read_one!("12345") == 12345
end
end
describe "floats" do
test "simple float" do
assert read_one!("3.14") == 3.14
end
test "negative float" do
assert read_one!("-2.5") == -2.5
end
test "float starting with zero" do
assert read_one!("0.001") == 0.001
end
end
describe "strings" do
test "simple string" do
assert read_one!(~s("hello")) == "hello"
end
test "empty string" do
assert read_one!(~s("")) == ""
end
test "string with spaces" do
assert read_one!(~s("hello world")) == "hello world"
end
test "escaped double quote" do
assert read_one!(~s("say \\"hi\\"")) == ~s(say "hi")
end
test "escaped backslash" do
assert read_one!(~s("path\\\\to")) == "path\\to"
end
test "escaped newline" do
assert read_one!(~s("line1\\nline2")) == "line1\nline2"
end
test "escaped tab" do
assert read_one!(~s("col1\\tcol2")) == "col1\tcol2"
end
test "escaped carriage return" do
assert read_one!(~s("before\\rafter")) == "before\rafter"
end
test "multiline string" do
input = ~s("line1\nline2")
assert read_one!(input) == "line1\nline2"
end
end
describe "keywords" do
test "simple keyword" do
assert read_one!(":ok") == :ok
end
test "keyword with hyphen" do
assert read_one!(":my-key") == :"my-key"
end
test "keyword with numbers" do
assert read_one!(":v2") == :v2
end
test "quoted keyword" do
assert read_one!(~s(:"quoted-name")) == :"quoted-name"
end
test "keyword with question mark" do
assert read_one!(":empty?") == :empty?
end
test "keyword with exclamation" do
assert read_one!(":reset!") == :reset!
end
end
describe "booleans" do
test "true" do
assert read_one!("true") == true
end
test "false" do
assert read_one!("false") == false
end
end
describe "nil" do
test "nil literal" do
assert read_one!("nil") == nil
end
end
# ═══════════════════════════════════════════════════════════════════
# Symbols
# ═══════════════════════════════════════════════════════════════════
describe "symbols" do
test "simple symbol" do
assert read_one!("hello") == {:symbol, %{line: 1, col: 1}, "hello"}
end
test "symbol with hyphen" do
assert read_one!("my-func") == {:symbol, %{line: 1, col: 1}, "my-func"}
end
test "qualified symbol with slash" do
assert read_one!("Enum/map") == {:symbol, %{line: 1, col: 1}, "Enum/map"}
end
test "erlang module call" do
assert read_one!("io/format") == {:symbol, %{line: 1, col: 1}, "io/format"}
end
test "dynamic var *name*" do
assert read_one!("*self*") == {:symbol, %{line: 1, col: 1}, "*self*"}
end
test "symbol with question mark" do
assert read_one!("empty?") == {:symbol, %{line: 1, col: 1}, "empty?"}
end
test "symbol with exclamation" do
assert read_one!("swap!") == {:symbol, %{line: 1, col: 1}, "swap!"}
end
test "operator symbols" do
assert read_one!("+") == {:symbol, %{line: 1, col: 1}, "+"}
assert read_one!("-") == {:symbol, %{line: 1, col: 1}, "-"}
assert read_one!("*") == {:symbol, %{line: 1, col: 1}, "*"}
assert read_one!(">=") == {:symbol, %{line: 1, col: 1}, ">="}
assert read_one!("<=") == {:symbol, %{line: 1, col: 1}, "<="}
assert read_one!("!=") == {:symbol, %{line: 1, col: 1}, "!="}
end
test "underscore symbol" do
assert read_one!("_") == {:symbol, %{line: 1, col: 1}, "_"}
end
test "anon fn arg %" do
assert read_one!("%") == {:symbol, %{line: 1, col: 1}, "%"}
end
test "anon fn numbered args %1 %2" do
assert read_one!("%1") == {:symbol, %{line: 1, col: 1}, "%1"}
assert read_one!("%2") == {:symbol, %{line: 1, col: 1}, "%2"}
end
test "ampersand for rest args" do
assert read_one!("&") == {:symbol, %{line: 1, col: 1}, "&"}
end
test "defn- private function name" do
assert read_one!("defn-") == {:symbol, %{line: 1, col: 1}, "defn-"}
end
test "symbol starting with dot" do
assert read_one!(".method") == {:symbol, %{line: 1, col: 1}, ".method"}
end
end
# ═══════════════════════════════════════════════════════════════════
# Collection types
# ═══════════════════════════════════════════════════════════════════
describe "lists" do
test "simple list" do
{:list, meta, elements} = read_one!("(+ 1 2)")
assert meta == %{line: 1, col: 1}
assert elements == [{:symbol, %{line: 1, col: 2}, "+"}, 1, 2]
end
test "empty list" do
assert read_one!("()") == {:list, %{line: 1, col: 1}, []}
end
test "nested list" do
{:list, _, [_, {:list, inner_meta, inner_elems}]} = read_one!("(a (b c))")
assert inner_meta == %{line: 1, col: 4}
assert inner_elems == [{:symbol, %{line: 1, col: 5}, "b"}, {:symbol, %{line: 1, col: 7}, "c"}]
end
end
describe "vectors" do
test "simple vector" do
{:vector, meta, elements} = read_one!("[1 2 3]")
assert meta == %{line: 1, col: 1}
assert elements == [1, 2, 3]
end
test "empty vector" do
assert read_one!("[]") == {:vector, %{line: 1, col: 1}, []}
end
test "vector with mixed types" do
{:vector, _, elems} = read_one!("[:ok 42 \"hello\"]")
assert elems == [:ok, 42, "hello"]
end
end
describe "maps" do
test "simple map" do
{:map, meta, elements} = read_one!("{:name \"Ada\" :age 30}")
assert meta == %{line: 1, col: 1}
assert elements == [:name, "Ada", :age, 30]
end
test "empty map" do
assert read_one!("{}") == {:map, %{line: 1, col: 1}, []}
end
test "map with nested values" do
{:map, _, elements} = read_one!("{:a [1 2] :b {:c 3}}")
assert length(elements) == 4
assert Enum.at(elements, 0) == :a
assert {:vector, _, [1, 2]} = Enum.at(elements, 1)
assert Enum.at(elements, 2) == :b
assert {:map, _, [:c, 3]} = Enum.at(elements, 3)
end
end
describe "sets" do
test "simple set" do
{:set, meta, elements} = read_one!("\#{:a :b :c}")
assert meta == %{line: 1, col: 1}
assert elements == [:a, :b, :c]
end
test "empty set" do
assert read_one!("\#{}") == {:set, %{line: 1, col: 1}, []}
end
test "nested set containing a set" do
{:set, _, elements} = read_one!("\#{\#{:a}}")
assert [inner] = elements
assert {:set, _, [:a]} = inner
end
end
describe "tuples (#el[...])" do
test "simple tuple" do
{:tuple, meta, elements} = read_one!("#el[:ok value]")
assert meta == %{line: 1, col: 1}
assert elements == [:ok, {:symbol, %{line: 1, col: 9}, "value"}]
end
test "empty tuple" do
assert read_one!("#el[]") == {:tuple, %{line: 1, col: 1}, []}
end
test "tuple with nested data" do
{:tuple, _, elements} = read_one!("#el[:ok {:name \"Ada\"}]")
assert Enum.at(elements, 0) == :ok
assert {:map, _, [:name, "Ada"]} = Enum.at(elements, 1)
end
end
# ═══════════════════════════════════════════════════════════════════
# Nested structures
# ═══════════════════════════════════════════════════════════════════
describe "nested structures" do
test "deeply nested list" do
{:list, _, [_, {:list, _, [_, {:list, _, [sym]}]}]} = read_one!("(a (b (c)))")
assert sym == {:symbol, %{line: 1, col: 8}, "c"}
end
test "vector inside map inside list" do
{:list, _, [sym, {:map, _, [:data, {:vector, _, [1, 2, 3]}]}]} =
read_one!("(process {:data [1 2 3]})")
assert sym == {:symbol, %{line: 1, col: 2}, "process"}
end
test "let binding form" do
{:list, _, [let_sym, {:vector, _, bindings}, body]} =
read_one!("(let [x 1 y 2] (+ x y))")
assert let_sym == {:symbol, %{line: 1, col: 2}, "let"}
assert bindings == [{:symbol, %{line: 1, col: 7}, "x"}, 1, {:symbol, %{line: 1, col: 11}, "y"}, 2]
assert {:list, _, [{:symbol, _, "+"}, {:symbol, _, "x"}, {:symbol, _, "y"}]} = body
end
end
# ═══════════════════════════════════════════════════════════════════
# Prefix forms
# ═══════════════════════════════════════════════════════════════════
describe "quote" do
test "quote a list" do
{:quote, meta, inner} = read_one!("'(1 2 3)")
assert meta == %{line: 1, col: 1}
assert {:list, _, [1, 2, 3]} = inner
end
test "quote a symbol" do
{:quote, _, inner} = read_one!("'hello")
assert inner == {:symbol, %{line: 1, col: 2}, "hello"}
end
end
describe "quasiquote" do
test "quasiquote a list" do
{:quasiquote, meta, inner} = read_one!("`(list ~x ~@rest)")
assert meta == %{line: 1, col: 1}
assert {:list, _, [_, {:unquote, _, _}, {:splice_unquote, _, _}]} = inner
end
end
describe "unquote" do
test "unquote a symbol" do
{:unquote, meta, inner} = read_one!("~x")
assert meta == %{line: 1, col: 1}
assert inner == {:symbol, %{line: 1, col: 2}, "x"}
end
end
describe "splice-unquote" do
test "splice-unquote a symbol" do
{:splice_unquote, meta, inner} = read_one!("~@items")
assert meta == %{line: 1, col: 1}
assert inner == {:symbol, %{line: 1, col: 3}, "items"}
end
end
describe "deref" do
test "deref a symbol" do
{:deref, meta, inner} = read_one!("@my-atom")
assert meta == %{line: 1, col: 1}
assert inner == {:symbol, %{line: 1, col: 2}, "my-atom"}
end
end
describe "metadata" do
test "map metadata" do
{:with_meta, meta, {meta_map, target}} =
read_one!("^{:doc \"hello\"} my-fn")
assert meta == %{line: 1, col: 1}
assert {:map, _, [:doc, "hello"]} = meta_map
assert target == {:symbol, %{line: 1, col: 17}, "my-fn"}
end
test "keyword metadata shorthand" do
{:with_meta, _, {meta_map, target}} =
read_one!("^:private my-fn")
assert {:map, _, [:private, true]} = meta_map
assert target == {:symbol, %{line: 1, col: 11}, "my-fn"}
end
test "metadata on a vector" do
{:with_meta, _, {meta_map, target}} =
read_one!("^:dynamic [1 2]")
assert {:map, _, [:dynamic, true]} = meta_map
assert {:vector, _, [1, 2]} = target
end
end
# ═══════════════════════════════════════════════════════════════════
# Anonymous function shorthand
# ═══════════════════════════════════════════════════════════════════
describe "anonymous function #(...)" do
test "simple anon fn" do
{:anon_fn, meta, body} = read_one!("#(* % 2)")
assert meta == %{line: 1, col: 1}
assert {:list, _, [{:symbol, _, "*"}, {:symbol, _, "%"}, 2]} = body
end
test "anon fn with multiple args" do
{:anon_fn, _, body} = read_one!("#(+ %1 %2)")
assert {:list, _, [{:symbol, _, "+"}, {:symbol, _, "%1"}, {:symbol, _, "%2"}]} = body
end
test "anon fn with nested call" do
{:anon_fn, _, body} = read_one!("#(str \"hello \" %)")
assert {:list, _, [{:symbol, _, "str"}, "hello ", {:symbol, _, "%"}]} = body
end
end
# ═══════════════════════════════════════════════════════════════════
# Regex literals
# ═══════════════════════════════════════════════════════════════════
describe "regex literals" do
test "simple regex" do
{:regex, meta, pattern} = read_one!(~s(#"pattern"))
assert meta == %{line: 1, col: 1}
assert pattern == "pattern"
end
test "regex with special chars" do
{:regex, _, pattern} = read_one!(~s(#"^\\d{3}-\\d{4}$"))
assert pattern == "^\\d{3}-\\d{4}$"
end
end
# ═══════════════════════════════════════════════════════════════════
# Comments and whitespace
# ═══════════════════════════════════════════════════════════════════
describe "comments" do
test "single-line comment ignored" do
forms = read!("; this is a comment\n42")
assert forms == [42]
end
test "comment after form" do
forms = read!("42 ; a number")
assert forms == [42]
end
test "multiple comments" do
forms = read!("; comment 1\n; comment 2\n42")
assert forms == [42]
end
test "comment between forms" do
forms = read!("1\n; between\n2")
assert forms == [1, 2]
end
end
describe "whitespace handling" do
test "commas are whitespace" do
{:vector, _, elems} = read_one!("[1, 2, 3]")
assert elems == [1, 2, 3]
end
test "commas in maps" do
{:map, _, elems} = read_one!("{:a 1, :b 2}")
assert elems == [:a, 1, :b, 2]
end
test "tabs and spaces" do
forms = read!(" \t 42")
assert forms == [42]
end
test "multiple newlines" do
forms = read!("\n\n42\n\n")
assert forms == [42]
end
end
# ═══════════════════════════════════════════════════════════════════
# Edge cases
# ═══════════════════════════════════════════════════════════════════
describe "negative numbers" do
test "negative integer as standalone" do
assert read_one!("-3") == -3
end
test "negative float as standalone" do
assert read_one!("-3.14") == -3.14
end
test "negative numbers inside list" do
{:list, _, elems} = read_one!("(-3 -4)")
assert elems == [-3, -4]
end
test "subtraction symbol followed by space and number" do
{:list, _, [sym, num]} = read_one!("(- 3)")
assert sym == {:symbol, %{line: 1, col: 2}, "-"}
assert num == 3
end
test "negative number after symbol in list" do
{:list, _, [sym, num]} = read_one!("(x -3)")
assert sym == {:symbol, %{line: 1, col: 2}, "x"}
assert num == -3
end
test "negative number in vector" do
{:vector, _, elems} = read_one!("[-1 -2 -3]")
assert elems == [-1, -2, -3]
end
end
describe "keywords with special chars" do
test "keyword with hyphen" do
assert read_one!(":my-key") == :"my-key"
end
test "keyword with question mark" do
assert read_one!(":valid?") == :valid?
end
test "keyword with dot" do
assert read_one!(":some.ns") == :"some.ns"
end
end
describe "empty collections" do
test "empty list" do
assert read_one!("()") == {:list, %{line: 1, col: 1}, []}
end
test "empty vector" do
assert read_one!("[]") == {:vector, %{line: 1, col: 1}, []}
end
test "empty map" do
assert read_one!("{}") == {:map, %{line: 1, col: 1}, []}
end
test "empty set" do
assert read_one!("\#{}") == {:set, %{line: 1, col: 1}, []}
end
test "empty tuple" do
assert read_one!("#el[]") == {:tuple, %{line: 1, col: 1}, []}
end
end
# ═══════════════════════════════════════════════════════════════════
# Error cases
# ═══════════════════════════════════════════════════════════════════
describe "error cases" do
test "unclosed list" do
assert {:error, msg} = Reader.read_string("(1 2 3")
assert msg =~ "expected ')'"
end
test "unclosed vector" do
assert {:error, msg} = Reader.read_string("[1 2 3")
assert msg =~ "expected ']'"
end
test "unclosed map" do
assert {:error, msg} = Reader.read_string("{:a 1")
assert msg =~ "expected '}'"
end
test "unclosed set" do
assert {:error, msg} = Reader.read_string("\#{:a :b")
assert msg =~ "expected '}'"
end
test "unclosed string" do
assert {:error, msg} = Reader.read_string(~s("hello))
assert msg =~ "Unterminated string"
end
test "unclosed tuple" do
assert {:error, msg} = Reader.read_string("#el[:ok")
assert msg =~ "expected ']'"
end
test "unexpected closing paren" do
assert {:error, _msg} = Reader.read_string(")")
end
test "unexpected closing bracket" do
assert {:error, _msg} = Reader.read_string("]")
end
test "unexpected closing brace" do
assert {:error, _msg} = Reader.read_string("}")
end
end
# ═══════════════════════════════════════════════════════════════════
# Multi-form parsing
# ═══════════════════════════════════════════════════════════════════
describe "multi-form parsing" do
test "multiple top-level forms" do
forms = read!("1 2 3")
assert forms == [1, 2, 3]
end
test "multiple forms of different types" do
forms = read!(":ok 42 \"hello\" true nil")
assert forms == [:ok, 42, "hello", true, nil]
end
test "multiple lists" do
forms = read!("(+ 1 2) (* 3 4)")
assert length(forms) == 2
assert {:list, _, _} = Enum.at(forms, 0)
assert {:list, _, _} = Enum.at(forms, 1)
end
test "forms separated by newlines" do
forms = read!("1\n2\n3")
assert forms == [1, 2, 3]
end
test "empty input" do
assert read!("") == []
end
test "only whitespace" do
assert read!(" \n\t ") == []
end
test "only comments" do
assert read!("; just a comment\n; another comment") == []
end
end
# ═══════════════════════════════════════════════════════════════════
# Line and column tracking
# ═══════════════════════════════════════════════════════════════════
describe "line and column tracking" do
test "first form at line 1, col 1" do
{:symbol, meta, _} = read_one!("hello")
assert meta == %{line: 1, col: 1}
end
test "form after newline tracks correct line" do
[_, {:symbol, meta, _}] = read!("foo\nbar")
assert meta.line == 2
assert meta.col == 1
end
test "form after comment tracks correct line" do
[form] = read!("; comment\nhello")
assert {:symbol, %{line: 2, col: 1}, "hello"} = form
end
test "elements inside collection track position" do
{:list, _, [_, second, _]} = read_one!("(a b c)")
assert {:symbol, %{line: 1, col: 4}, "b"} = second
end
end
# ═══════════════════════════════════════════════════════════════════
# Tokenizer-specific edge cases
# ═══════════════════════════════════════════════════════════════════
describe "tokenizer edge cases" do
test "dispatch #el[ is recognized as tuple start" do
{:tuple, _, [:ok]} = read_one!("#el[:ok]")
end
test "#el[ does not consume extra chars" do
{:tuple, _, [num]} = read_one!("#el[42]")
assert num == 42
end
test "hash dispatch for set vs tuple vs anon fn" do
assert {:set, _, _} = read_one!("\#{1 2}")
assert {:tuple, _, _} = read_one!("#el[1 2]")
assert {:anon_fn, _, _} = read_one!("#(+ 1 2)")
assert {:regex, _, _} = read_one!(~s(#"abc"))
end
end
# ═══════════════════════════════════════════════════════════════════
# Real-world: ChatRoom from the spec
# ═══════════════════════════════════════════════════════════════════
describe "ChatRoom example" do
test "parses the ChatRoom defmodule" do
source = """
(defmodule ChatRoom
(defn loop [state]
(receive
[:join username pid]
(let [members (assoc (:members state) username pid)]
(send pid [:welcome username (count members)])
(loop (assoc state :members members)))
[:message from body]
(do
(doseq [[_name pid] (:members state)]
(send pid [:chat from body]))
(loop state))
[:leave username]
(loop (update state :members dissoc username))
:shutdown
(do
(doseq [[_name pid] (:members state)]
(send pid :room-closed))
:ok))))
"""
{:ok, [form]} = Reader.read_string(source)
assert {:list, _, [defmod_sym, chatroom_sym | body]} = form
assert {:symbol, _, "defmodule"} = defmod_sym
assert {:symbol, _, "ChatRoom"} = chatroom_sym
# The body should contain the defn form
[defn_form] = body
assert {:list, _, [defn_sym, loop_sym, params | _rest_body]} = defn_form
assert {:symbol, _, "defn"} = defn_sym
assert {:symbol, _, "loop"} = loop_sym
assert {:vector, _, [{:symbol, _, "state"}]} = params
end
test "parses ChatRoom usage" do
source = """
(def room (spawn (fn [] (ChatRoom/loop {:owner "alice" :members {}}))))
(send room [:join "alice" *self*])
(send room [:join "bob" *self*])
(send room [:message "bob" "hey everyone"])
"""
{:ok, forms} = Reader.read_string(source)
assert length(forms) == 4
# First form: (def room ...)
[def_form | _] = forms
assert {:list, _, [{:symbol, _, "def"}, {:symbol, _, "room"}, spawn_call]} = def_form
assert {:list, _, [{:symbol, _, "spawn"}, _fn_form]} = spawn_call
# Last form: (send room [:message ...])
last = List.last(forms)
assert {:list, _, [{:symbol, _, "send"}, {:symbol, _, "room"}, msg_vec]} = last
assert {:vector, _, [:message, "bob", "hey everyone"]} = msg_vec
end
end
# ═══════════════════════════════════════════════════════════════════
# Complex real-world patterns
# ═══════════════════════════════════════════════════════════════════
describe "complex forms" do
test "defn with multiple clauses" do
source = "(defn greet ([name] (greet name \"hello\")) ([name greeting] (str greeting \" \" name)))"
{:ok, [form]} = Reader.read_string(source)
assert {:list, _, [{:symbol, _, "defn"}, {:symbol, _, "greet"} | clauses]} = form
assert length(clauses) == 2
end
test "let with destructuring" do
source = "(let [{:keys [name email]} user] (str name \" <\" email \">\"))"
{:ok, [form]} = Reader.read_string(source)
assert {:list, _, [{:symbol, _, "let"}, {:vector, _, _bindings}, _body]} = form
end
test "metadata on defmodule" do
source = "(defmodule ^{:author \"Ada\"} Greeter (defn hello [name] (str \"hello \" name)))"
{:ok, [form]} = Reader.read_string(source)
assert {:list, _, [{:symbol, _, "defmodule"}, {:with_meta, _, _}, _greeter | _]} = form
end
test "cond form" do
source = """
(cond
(< x 0) "negative"
(= x 0) "zero"
:else "positive")
"""
{:ok, [form]} = Reader.read_string(source)
assert {:list, _, [{:symbol, _, "cond"} | clauses]} = form
# 6 elements: 3 test/result pairs
assert length(clauses) == 6
end
test "quasiquote with unquote and splice-unquote" do
source = "`(defn ~name [~@args] ~@body)"
{:ok, [form]} = Reader.read_string(source)
assert {:quasiquote, _, {:list, _, elements}} = form
assert {:symbol, _, "defn"} = Enum.at(elements, 0)
assert {:unquote, _, {:symbol, _, "name"}} = Enum.at(elements, 1)
end
test "nested tuples and sets" do
source = "#el[:ok \#{:a :b}]"
{:ok, [form]} = Reader.read_string(source)
assert {:tuple, _, [:ok, {:set, _, [:a, :b]}]} = form
end
end
end