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