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>
821 lines
28 KiB
Elixir
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
|