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>
This commit is contained in:
2026-03-08 10:38:22 -04:00
commit d8719b6d48
26 changed files with 11487 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
defmodule CljElixir.CompilerTest do
use ExUnit.Case, async: true
describe "compile_string/2" do
test "returns {:ok, ast} for valid source" do
# This test exercises the full pipeline. It requires Reader and Transformer
# to be implemented. Until then, it verifies the Compiler module compiles
# and the function heads are correct.
source = "(+ 1 2)"
case CljElixir.Compiler.compile_string(source) do
{:ok, _ast} ->
:ok
{:error, diagnostics} ->
# Expected when Reader/Transformer are not yet implemented
assert is_list(diagnostics)
end
end
test "returns {:error, diagnostics} for missing file" do
{:error, diagnostics} = CljElixir.Compiler.compile_file("/nonexistent/path.clje")
assert is_list(diagnostics)
assert length(diagnostics) > 0
[diag | _] = diagnostics
assert diag.severity == :error
assert diag.message =~ "could not read file"
end
test "passes file option through" do
source = "(+ 1 2)"
opts = [file: "test.clje"]
case CljElixir.Compiler.compile_string(source, opts) do
{:ok, _ast} -> :ok
{:error, _diagnostics} -> :ok
end
end
end
describe "eval_string/2" do
test "returns {:ok, result, bindings} or {:error, diagnostics}" do
source = "(+ 1 2)"
case CljElixir.Compiler.eval_string(source) do
{:ok, result, bindings} ->
assert result == 3
assert is_list(bindings)
{:error, diagnostics} ->
# Expected when Reader/Transformer are not yet implemented
assert is_list(diagnostics)
end
end
end
describe "compile_to_beam/2" do
test "returns {:ok, modules} or {:error, diagnostics}" do
source = ~S"""
(defmodule TestBeamCompile
(defn hello [] :world))
"""
case CljElixir.Compiler.compile_to_beam(source) do
{:ok, modules} ->
assert is_list(modules)
assert Enum.any?(modules, fn {mod, _binary} ->
mod == TestBeamCompile
end)
{:error, diagnostics} ->
# Expected when Reader/Transformer are not yet implemented
assert is_list(diagnostics)
end
end
end
describe "compile_file/2" do
test "reads file and compiles" do
# Write a temp file
tmp_dir = System.tmp_dir!()
path = Path.join(tmp_dir, "test_compile_#{System.unique_integer([:positive])}.clje")
File.write!(path, "(+ 1 2)")
try do
case CljElixir.Compiler.compile_file(path) do
{:ok, _ast} -> :ok
{:error, _diagnostics} -> :ok
end
after
File.rm(path)
end
end
test "returns error for nonexistent file" do
{:error, [diag | _]} = CljElixir.Compiler.compile_file("/does/not/exist.clje")
assert diag.severity == :error
assert diag.message =~ "could not read file"
end
end
describe "eval_file/2" do
test "reads file, compiles, and evaluates" do
tmp_dir = System.tmp_dir!()
path = Path.join(tmp_dir, "test_eval_#{System.unique_integer([:positive])}.clje")
File.write!(path, "(+ 1 2)")
try do
case CljElixir.Compiler.eval_file(path) do
{:ok, 3, _bindings} -> :ok
{:ok, _result, _bindings} -> :ok
{:error, _diagnostics} -> :ok
end
after
File.rm(path)
end
end
end
describe "compile_file_to_beam/2" do
test "compiles file and writes .beam output" do
tmp_dir = System.tmp_dir!()
source_path = Path.join(tmp_dir, "test_beam_#{System.unique_integer([:positive])}.clje")
output_dir = Path.join(tmp_dir, "beam_output_#{System.unique_integer([:positive])}")
File.write!(source_path, ~S"""
(defmodule TestBeamOutput
(defn greet [] "hi"))
""")
try do
case CljElixir.Compiler.compile_file_to_beam(source_path, output_dir: output_dir) do
{:ok, modules} ->
assert is_list(modules)
# Check .beam files were written
beam_files = Path.wildcard(Path.join(output_dir, "*.beam"))
assert length(beam_files) > 0
{:error, _diagnostics} ->
# Expected when Reader/Transformer are not yet implemented
:ok
end
after
File.rm(source_path)
File.rm_rf(output_dir)
end
end
end
end
+501
View File
@@ -0,0 +1,501 @@
defmodule CljElixir.MalliTest do
use ExUnit.Case, async: true
alias CljElixir.Malli
# Atoms that need quoted syntax in Elixir source
@arrow :"=>"
@opt :"?"
# ── Helper ──────────────────────────────────────────────────────────
defp string_t do
{{:., [], [{:__aliases__, [alias: false], [:String]}, :t]}, [], []}
end
defp mapset_t do
{{:., [], [{:__aliases__, [alias: false], [:MapSet]}, :t]}, [], []}
end
defp pv_t do
{{:., [], [{:__aliases__, [alias: false], [:CljElixir, :PersistentVector]}, :t]}, [], []}
end
# ── Primitive types ─────────────────────────────────────────────────
describe "primitive types" do
test "string" do
assert string_t() == Malli.schema_to_typespec(:string)
end
test "int" do
assert {:integer, [], []} = Malli.schema_to_typespec(:int)
end
test "integer" do
assert {:integer, [], []} = Malli.schema_to_typespec(:integer)
end
test "float" do
assert {:float, [], []} = Malli.schema_to_typespec(:float)
end
test "number" do
assert {:number, [], []} = Malli.schema_to_typespec(:number)
end
test "boolean" do
assert {:boolean, [], []} = Malli.schema_to_typespec(:boolean)
end
test "atom" do
assert {:atom, [], []} = Malli.schema_to_typespec(:atom)
end
test "keyword" do
assert {:atom, [], []} = Malli.schema_to_typespec(:keyword)
end
test "any" do
assert {:any, [], []} = Malli.schema_to_typespec(:any)
end
test "nil" do
assert nil == Malli.schema_to_typespec(:nil)
end
test "pid" do
assert {:pid, [], []} = Malli.schema_to_typespec(:pid)
end
test "port" do
assert {:port, [], []} = Malli.schema_to_typespec(:port)
end
test "reference" do
assert {:reference, [], []} = Malli.schema_to_typespec(:reference)
end
test "pos-int" do
assert {:pos_integer, [], []} = Malli.schema_to_typespec(:"pos-int")
end
test "neg-int" do
assert {:neg_integer, [], []} = Malli.schema_to_typespec(:"neg-int")
end
test "nat-int" do
assert {:non_neg_integer, [], []} = Malli.schema_to_typespec(:"nat-int")
end
end
# ── Compound types ─────────────────────────────────────────────────
describe "compound types" do
test "or with two types" do
ast = Malli.schema_to_typespec([:or, :int, :string])
expected_string = string_t()
assert {:|, [], [{:integer, [], []}, ^expected_string]} = ast
end
test "or with three types (right-associative)" do
ast = Malli.schema_to_typespec([:or, :int, :string, :boolean])
assert {:|, [], [{:integer, [], []}, {:|, [], [_, {:boolean, [], []}]}]} = ast
end
test "maybe type" do
ast = Malli.schema_to_typespec([:maybe, :string])
expected_string = string_t()
assert {:|, [], [^expected_string, nil]} = ast
end
test "enum type" do
ast = Malli.schema_to_typespec([:enum, :a, :b, :c])
assert {:|, [], [:a, {:|, [], [:b, :c]}]} = ast
end
test "enum with single value" do
assert :a = Malli.schema_to_typespec([:enum, :a])
end
test "= literal" do
assert :hello = Malli.schema_to_typespec([:=, :hello])
assert 42 = Malli.schema_to_typespec([:=, 42])
end
test "and with base type and constraint (general)" do
ast = Malli.schema_to_typespec([:and, :int, [:<, 100]])
assert {:integer, [], []} = ast
end
test "and :int [:> 0] produces pos_integer()" do
ast = Malli.schema_to_typespec([:and, :int, [:>, 0]])
assert {:pos_integer, [], []} = ast
end
test "and :int [:>= 0] produces non_neg_integer()" do
ast = Malli.schema_to_typespec([:and, :int, [:>=, 0]])
assert {:non_neg_integer, [], []} = ast
end
end
# ── Container types ────────────────────────────────────────────────
describe "container types" do
test "map with fields" do
ast = Malli.schema_to_typespec([:map, [:name, :string], [:age, :int]])
assert {:%{}, [], kv} = ast
assert Keyword.has_key?(kv, :name)
assert Keyword.has_key?(kv, :age)
expected_string = string_t()
assert ^expected_string = Keyword.get(kv, :name)
assert {:integer, [], []} = Keyword.get(kv, :age)
end
test "map with optional field" do
ast = Malli.schema_to_typespec([:map, [:name, :string], [:email, {:optional, true}, :string]])
assert {:%{}, [], kv} = ast
assert Keyword.has_key?(kv, :name)
assert Keyword.has_key?(kv, :email)
end
test "map-of" do
ast = Malli.schema_to_typespec([:"map-of", :string, :int])
assert {:%{}, [], [optional_entry]} = ast
assert {{:optional, [], [_key_t]}, {:integer, [], []}} = optional_entry
end
test "list" do
ast = Malli.schema_to_typespec([:list, :int])
assert [{:integer, [], []}] = ast
end
test "vector" do
ast = Malli.schema_to_typespec([:vector, :int])
assert ^ast = pv_t()
end
test "set" do
ast = Malli.schema_to_typespec([:set, :int])
assert ^ast = mapset_t()
end
test "tuple" do
ast = Malli.schema_to_typespec([:tuple, :int, :string])
expected_string = string_t()
assert {:{}, [], [{:integer, [], []}, ^expected_string]} = ast
end
test "tuple with three elements" do
ast = Malli.schema_to_typespec([:tuple, :int, :string, :boolean])
assert {:{}, [], [{:integer, [], []}, _, {:boolean, [], []}]} = ast
end
end
# ── Function specs ─────────────────────────────────────────────────
describe "function specs" do
test "simple function spec" do
specs = Malli.spec_ast(:hello, [@arrow, [:cat, :string], :string])
assert length(specs) == 1
[{:@, [], [{:spec, [], [spec_body]}]}] = specs
assert {:"::", [], [{:hello, [], [_arg]}, _ret]} = spec_body
end
test "function with two params" do
specs = Malli.spec_ast(:add, [@arrow, [:cat, :int, :int], :int])
assert length(specs) == 1
[{:@, [], [{:spec, [], [{:"::", [], [{:add, [], args}, _ret]}]}]}] = specs
assert length(args) == 2
end
test "function with optional param produces two specs" do
specs = Malli.spec_ast(:greet, [@arrow, [:cat, :string, [@opt, :string]], :string])
assert length(specs) == 2
arities = Enum.map(specs, fn
{:@, [], [{:spec, [], [{:"::", [], [{:greet, [], args}, _ret]}]}]} ->
length(args)
end)
assert Enum.sort(arities) == [1, 2]
end
test "function with multiple optional params" do
specs = Malli.spec_ast(:f, [@arrow, [:cat, :int, [@opt, :string], [@opt, :boolean]], :any])
assert length(specs) == 3
arities = Enum.map(specs, fn
{:@, [], [{:spec, [], [{:"::", [], [{:f, [], args}, _ret]}]}]} ->
length(args)
end)
assert Enum.sort(arities) == [1, 2, 3]
end
test "multi-arity function via :function" do
specs =
Malli.spec_ast(:greet, [
:function,
[@arrow, [:cat, :string], :string],
[@arrow, [:cat, :string, :string], :string]
])
assert length(specs) == 2
arities = Enum.map(specs, fn
{:@, [], [{:spec, [], [{:"::", [], [{:greet, [], args}, _ret]}]}]} ->
length(args)
end)
assert Enum.sort(arities) == [1, 2]
end
end
# ── Type generation ────────────────────────────────────────────────
describe "type generation" do
test "named type from map schema" do
ast = Malli.type_ast(:user, [:map, [:name, :string], [:age, :int]])
assert {:@, [], [{:type, [], [{:"::", [], [{:user, [], []}, _map_type]}]}]} = ast
end
test "named type from primitive" do
ast = Malli.type_ast(:name, :string)
expected_string = string_t()
assert {:@, [], [{:type, [], [{:"::", [], [{:name, [], []}, ^expected_string]}]}]} = ast
end
end
# ── Schema references ─────────────────────────────────────────────
describe "schema references" do
test "known type reference" do
ast = Malli.schema_to_typespec("User", known_types: %{"User" => :user})
assert {:user, [], []} = ast
end
test "unknown string reference falls back to any()" do
ast = Malli.schema_to_typespec("Unknown")
assert {:any, [], []} = ast
end
end
# ── Recursive types ───────────────────────────────────────────────
describe "recursive types" do
test "ref produces type call" do
ast = Malli.schema_to_typespec([:ref, :tree], registry: %{tree: [:or, :int, :nil]})
assert {:tree, [], []} = ast
end
test "schema with registry via type_ast/2 dispatches to type_ast/3" do
schema = [
:schema,
%{registry: %{tree: [:or, :int, [:tuple, [:ref, :tree], [:ref, :tree]]]}},
[:ref, :tree]
]
types = Malli.type_ast(:tree, schema)
assert is_list(types)
assert length(types) >= 1
[{:@, [], [{:type, [], [{:"::", [], [{:tree, [], []}, _body]}]}]}] = types
end
test "schema with registry via type_ast/3 with list of pairs" do
registry = [{:tree, [:or, :int, [:tuple, [:ref, :tree], [:ref, :tree]]]}]
schema = [:schema, %{registry: Map.new(registry)}, [:ref, :tree]]
types = Malli.type_ast(:tree, schema, registry)
assert is_list(types)
assert length(types) == 1
end
end
# ── Compilation smoke tests ───────────────────────────────────────
describe "compilation smoke test" do
test "generated spec compiles in a module" do
spec_asts = Malli.spec_ast(:hello, [@arrow, [:cat, :string], :string])
fun_ast =
{:def, [], [
{:hello, [], [{:name, [], nil}]},
[do: {:name, [], nil}]
]}
module_body = spec_asts ++ [fun_ast]
block = {:__block__, [], module_body}
module_ast =
{:defmodule, [context: Elixir],
[{:__aliases__, [alias: false], [:MalliSmokeTest1]}, [do: block]]}
assert [{MalliSmokeTest1, _binary}] = Code.compile_quoted(module_ast)
after
:code.purge(MalliSmokeTest1)
:code.delete(MalliSmokeTest1)
end
test "generated type compiles in a module" do
type_ast = Malli.type_ast(:user, [:map, [:name, :string], [:age, :int]])
module_ast =
{:defmodule, [context: Elixir],
[{:__aliases__, [alias: false], [:MalliSmokeTest2]}, [do: type_ast]]}
assert [{MalliSmokeTest2, _binary}] = Code.compile_quoted(module_ast)
after
:code.purge(MalliSmokeTest2)
:code.delete(MalliSmokeTest2)
end
test "multi-arity spec compiles" do
specs =
Malli.spec_ast(:greet, [
:function,
[@arrow, [:cat, :string], :string],
[@arrow, [:cat, :string, :string], :string]
])
fun1 =
{:def, [], [
{:greet, [], [{:name, [], nil}]},
[do: {:name, [], nil}]
]}
fun2 =
{:def, [], [
{:greet, [], [{:greeting, [], nil}, {:name, [], nil}]},
[do: {:name, [], nil}]
]}
module_body = specs ++ [fun1, fun2]
block = {:__block__, [], module_body}
module_ast =
{:defmodule, [context: Elixir],
[{:__aliases__, [alias: false], [:MalliSmokeTest3]}, [do: block]]}
assert [{MalliSmokeTest3, _binary}] = Code.compile_quoted(module_ast)
after
:code.purge(MalliSmokeTest3)
:code.delete(MalliSmokeTest3)
end
test "map-of type compiles" do
type_ast = Malli.type_ast(:counts, [:"map-of", :string, :int])
module_ast =
{:defmodule, [context: Elixir],
[{:__aliases__, [alias: false], [:MalliSmokeTest4]}, [do: type_ast]]}
assert [{MalliSmokeTest4, _binary}] = Code.compile_quoted(module_ast)
after
:code.purge(MalliSmokeTest4)
:code.delete(MalliSmokeTest4)
end
test "tuple type compiles" do
type_ast = Malli.type_ast(:point, [:tuple, :int, :int])
module_ast =
{:defmodule, [context: Elixir],
[{:__aliases__, [alias: false], [:MalliSmokeTest5]}, [do: type_ast]]}
assert [{MalliSmokeTest5, _binary}] = Code.compile_quoted(module_ast)
after
:code.purge(MalliSmokeTest5)
:code.delete(MalliSmokeTest5)
end
test "list type compiles" do
type_ast = Malli.type_ast(:names, [:list, :string])
module_ast =
{:defmodule, [context: Elixir],
[{:__aliases__, [alias: false], [:MalliSmokeTest6]}, [do: type_ast]]}
assert [{MalliSmokeTest6, _binary}] = Code.compile_quoted(module_ast)
after
:code.purge(MalliSmokeTest6)
:code.delete(MalliSmokeTest6)
end
test "or type compiles" do
type_ast = Malli.type_ast(:string_or_int, [:or, :string, :int])
module_ast =
{:defmodule, [context: Elixir],
[{:__aliases__, [alias: false], [:MalliSmokeTest7]}, [do: type_ast]]}
assert [{MalliSmokeTest7, _binary}] = Code.compile_quoted(module_ast)
after
:code.purge(MalliSmokeTest7)
:code.delete(MalliSmokeTest7)
end
test "maybe type compiles" do
type_ast = Malli.type_ast(:opt_string, [:maybe, :string])
module_ast =
{:defmodule, [context: Elixir],
[{:__aliases__, [alias: false], [:MalliSmokeTest8]}, [do: type_ast]]}
assert [{MalliSmokeTest8, _binary}] = Code.compile_quoted(module_ast)
after
:code.purge(MalliSmokeTest8)
:code.delete(MalliSmokeTest8)
end
test "recursive type compiles" do
schema = [
:schema,
%{registry: %{tree: [:or, :int, [:tuple, [:ref, :tree], [:ref, :tree]]]}},
[:ref, :tree]
]
types = Malli.type_ast(:tree, schema)
block = {:__block__, [], types}
module_ast =
{:defmodule, [context: Elixir],
[{:__aliases__, [alias: false], [:MalliSmokeTest9]}, [do: block]]}
assert [{MalliSmokeTest9, _binary}] = Code.compile_quoted(module_ast)
after
:code.purge(MalliSmokeTest9)
:code.delete(MalliSmokeTest9)
end
test "optional params spec compiles" do
specs =
Malli.spec_ast(:greet, [@arrow, [:cat, :string, [@opt, :string]], :string])
fun1 =
{:def, [], [
{:greet, [], [{:name, [], nil}]},
[do: {:name, [], nil}]
]}
fun2 =
{:def, [], [
{:greet, [], [{:name, [], nil}, {:greeting, [], nil}]},
[do: {:name, [], nil}]
]}
module_body = specs ++ [fun1, fun2]
block = {:__block__, [], module_body}
module_ast =
{:defmodule, [context: Elixir],
[{:__aliases__, [alias: false], [:MalliSmokeTest10]}, [do: block]]}
assert [{MalliSmokeTest10, _binary}] = Code.compile_quoted(module_ast)
after
:code.purge(MalliSmokeTest10)
:code.delete(MalliSmokeTest10)
end
end
end
+417
View File
@@ -0,0 +1,417 @@
defmodule CljElixir.Phase2Test do
use ExUnit.Case, async: false
# Helper to compile and evaluate CljElixir code
# Uses vector_as_list: true until PersistentVector is implemented (Phase 3 WS-3)
defp eval!(source) do
case CljElixir.Compiler.eval_string(source, vector_as_list: true) do
{:ok, result, _bindings} -> result
{:error, errors} -> raise "CljElixir eval error: #{inspect(errors)}"
end
end
# Protocols and core modules are compiled by the Mix compiler plugin
# (compilers: [..., :clj_elixir] in mix.exs). No setup needed.
# ==========================================================================
# ILookup - get
# ==========================================================================
describe "get (ILookup)" do
test "get from map with existing key" do
assert eval!("(get {:a 1 :b 2} :a)") == 1
end
test "get from map with missing key returns nil" do
assert eval!("(get {:a 1} :b)") == nil
end
test "get from map with missing key and default" do
assert eval!("(get {:a 1} :b 42)") == 42
end
test "get from map with existing key ignores default" do
assert eval!("(get {:a 1} :a 42)") == 1
end
end
# ==========================================================================
# IAssociative - assoc, contains?
# ==========================================================================
describe "assoc (IAssociative)" do
test "assoc adds new key to map" do
assert eval!("(assoc {:a 1} :b 2)") == %{a: 1, b: 2}
end
test "assoc updates existing key" do
assert eval!("(assoc {:a 1} :a 2)") == %{a: 2}
end
test "assoc on empty map" do
assert eval!("(assoc {} :a 1)") == %{a: 1}
end
end
describe "contains? (IAssociative)" do
test "contains? returns true for existing key" do
assert eval!("(contains? {:a 1 :b 2} :a)") == true
end
test "contains? returns false for missing key" do
assert eval!("(contains? {:a 1} :c)") == false
end
end
# ==========================================================================
# IMap - dissoc
# ==========================================================================
describe "dissoc (IMap)" do
test "dissoc removes key from map" do
assert eval!("(dissoc {:a 1 :b 2} :a)") == %{b: 2}
end
test "dissoc with missing key returns same map" do
assert eval!("(dissoc {:a 1} :b)") == %{a: 1}
end
end
# ==========================================================================
# ICounted - count
# ==========================================================================
describe "count (ICounted)" do
test "count of map" do
assert eval!("(count {:a 1 :b 2 :c 3})") == 3
end
test "count of list" do
assert eval!("(count (list 1 2 3))") == 3
end
test "count of empty map" do
assert eval!("(count {})") == 0
end
test "count of tuple" do
assert eval!("(count #el[1 2 3])") == 3
end
test "count of string" do
assert eval!("(count \"hello\")") == 5
end
end
# ==========================================================================
# ISeq - first, rest
# ==========================================================================
describe "first/rest (ISeq)" do
test "first of list" do
assert eval!("(first (list 1 2 3))") == 1
end
test "rest of list" do
assert eval!("(rest (list 1 2 3))") == [2, 3]
end
test "first of empty list" do
assert eval!("(first (list))") == nil
end
test "rest of empty list" do
assert eval!("(rest (list))") == []
end
end
# ==========================================================================
# ISeqable - seq
# ==========================================================================
describe "seq (ISeqable)" do
test "seq of non-empty list returns the list" do
assert eval!("(seq (list 1 2 3))") == [1, 2, 3]
end
test "seq of empty list returns nil" do
assert eval!("(seq (list))") == nil
end
test "seq of map returns key-value pairs" do
result = eval!("(seq {:a 1})")
assert is_list(result)
assert length(result) == 1
end
end
# ==========================================================================
# ICollection - conj
# ==========================================================================
describe "conj (ICollection)" do
test "conj onto list prepends" do
assert eval!("(conj (list 2 3) 1)") == [1, 2, 3]
end
test "conj onto map merges tuple entry" do
result = eval!("(conj {:a 1} {:b 2})")
assert result == %{a: 1, b: 2}
end
end
# ==========================================================================
# IIndexed - nth
# ==========================================================================
describe "nth (IIndexed)" do
test "nth from tuple" do
assert eval!("(nth #el[10 20 30] 1)") == 20
end
test "nth with default" do
assert eval!("(nth #el[10 20] 5 :not-found)") == :"not-found"
end
end
# ==========================================================================
# IStack - peek, pop (via protocol on List)
# ==========================================================================
# Note: peek and pop are not in the builtin dispatch yet, they go through
# the protocol directly - skip for now unless dispatch was added
# ==========================================================================
# Sequence wrapper functions
# ==========================================================================
describe "map (sequence function)" do
test "map over list" do
assert eval!("(map (fn [x] (inc x)) (list 1 2 3))") == [2, 3, 4]
end
end
describe "filter" do
test "filter list" do
assert eval!("(filter (fn [x] (> x 2)) (list 1 2 3 4))") == [3, 4]
end
end
describe "reduce" do
test "reduce with initial value" do
assert eval!("(reduce (fn [a b] (+ a b)) 0 (list 1 2 3))") == 6
end
test "reduce without initial value" do
assert eval!("(reduce (fn [a b] (+ a b)) (list 1 2 3))") == 6
end
end
describe "concat" do
test "concat two lists" do
assert eval!("(concat (list 1 2) (list 3 4))") == [1, 2, 3, 4]
end
end
describe "take and drop" do
test "take from list" do
assert eval!("(take 2 (list 1 2 3 4))") == [1, 2]
end
test "drop from list" do
assert eval!("(drop 2 (list 1 2 3 4))") == [3, 4]
end
end
describe "sort" do
test "sort list" do
assert eval!("(sort (list 3 1 2))") == [1, 2, 3]
end
end
describe "distinct" do
test "distinct removes duplicates" do
assert eval!("(distinct (list 1 2 1 3 2))") == [1, 2, 3]
end
end
describe "frequencies" do
test "frequencies counts occurrences" do
assert eval!("(frequencies (list :a :b :a :c :b :a))") == %{a: 3, b: 2, c: 1}
end
end
describe "partition" do
test "partition into chunks" do
assert eval!("(partition 2 (list 1 2 3 4))") == [[1, 2], [3, 4]]
end
end
describe "mapcat" do
test "mapcat flattens results" do
assert eval!("(mapcat (fn [x] (list x x)) (list 1 2 3))") == [1, 1, 2, 2, 3, 3]
end
end
# ==========================================================================
# Map-specific functions
# ==========================================================================
describe "keys" do
test "keys of map" do
result = eval!("(keys {:a 1 :b 2})")
assert Enum.sort(result) == [:a, :b]
end
end
describe "vals" do
test "vals of map" do
result = eval!("(vals {:a 1 :b 2})")
assert Enum.sort(result) == [1, 2]
end
end
describe "merge" do
test "merge two maps" do
assert eval!("(merge {:a 1} {:b 2})") == %{a: 1, b: 2}
end
test "merge with overwrite" do
assert eval!("(merge {:a 1} {:a 2})") == %{a: 2}
end
end
describe "select-keys" do
test "select-keys from map" do
assert eval!("(select-keys {:a 1 :b 2 :c 3} (list :a :c))") == %{a: 1, c: 3}
end
end
describe "into" do
test "into map from list of tuples" do
assert eval!("(into {} (list #el[:a 1] #el[:b 2]))") == %{a: 1, b: 2}
end
end
# ==========================================================================
# update
# ==========================================================================
describe "update" do
test "update map value with function" do
assert eval!("(update {:a 1} :a (fn [x] (inc x)))") == %{a: 2}
end
end
# ==========================================================================
# empty?
# ==========================================================================
describe "empty?" do
test "empty? on empty map" do
assert eval!("(empty? {})") == true
end
test "empty? on non-empty map" do
assert eval!("(empty? {:a 1})") == false
end
end
# ==========================================================================
# Keyword-as-function through ILookup
# ==========================================================================
describe "keyword-as-function" do
test "keyword as function on map" do
assert eval!("(:name {:name \"Ada\"})") == "Ada"
end
test "keyword with default value" do
assert eval!("(:age {:name \"Ada\"} 25)") == 25
end
end
# ==========================================================================
# Compound functions (get-in, assoc-in, update-in)
# ==========================================================================
describe "get-in" do
test "get-in nested map" do
assert eval!("(get-in {:a {:b {:c 42}}} (list :a :b :c))") == 42
end
test "get-in with missing key" do
assert eval!("(get-in {:a {:b 1}} (list :a :c))") == nil
end
end
describe "assoc-in" do
test "assoc-in nested map" do
assert eval!("(assoc-in {:a {:b 1}} (list :a :b) 2)") == %{a: %{b: 2}}
end
end
describe "update-in" do
test "update-in nested map" do
assert eval!("(update-in {:a {:b 1}} (list :a :b) (fn [x] (inc x)))") == %{a: %{b: 2}}
end
end
# ==========================================================================
# reduce-kv
# ==========================================================================
describe "reduce-kv" do
test "reduce-kv over map" do
# Reduce a map collecting keys into a list
result = eval!("(reduce-kv (fn [acc _k v] (+ acc v)) 0 {:a 1 :b 2 :c 3})")
assert result == 6
end
end
# ==========================================================================
# FFI ? and ! preservation
# ==========================================================================
describe "FFI name munging fix" do
test "Map/has-key? works correctly" do
assert eval!("(Map/has-key? {:a 1} :a)") == true
end
test "Map/has-key? returns false for missing key" do
assert eval!("(Map/has-key? {:a 1} :b)") == false
end
end
# ==========================================================================
# End-to-end integration
# ==========================================================================
describe "end-to-end integration" do
test "realistic data transformation pipeline" do
source = """
(let [data (list {:name "Alice" :age 30}
{:name "Bob" :age 25}
{:name "Carol" :age 35})]
(map (fn [p] (:name p)) (filter (fn [p] (> (get p :age) 28)) data)))
"""
assert eval!(source) == ["Alice", "Carol"]
end
test "nested map operations" do
source = """
(let [m {:a 1 :b 2 :c 3}]
(dissoc (assoc m :d 4) :b))
"""
assert eval!(source) == %{a: 1, c: 3, d: 4}
end
test "count and empty? together" do
source = """
(let [m {:a 1}]
(list (count m) (empty? m) (empty? {})))
"""
assert eval!(source) == [1, false, true]
end
end
end
+452
View File
@@ -0,0 +1,452 @@
defmodule CljElixir.Phase3Test do
use ExUnit.Case, async: false
# Evaluate CljElixir code with PersistentVector enabled (no vector_as_list flag)
defp eval!(source) do
case CljElixir.Compiler.eval_string(source) do
{:ok, result, _bindings} -> result
{:error, errors} -> raise "CljElixir eval error: #{inspect(errors)}"
end
end
# ==========================================================================
# Vector literal construction
# ==========================================================================
describe "vector literal construction" do
test "vector literal produces PersistentVector" do
result = eval!("[1 2 3]")
assert result.__struct__ == CljElixir.PersistentVector
end
test "empty vector literal produces PersistentVector" do
result = eval!("[]")
assert result.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.pv_count(result) == 0
end
test "single element vector" do
result = eval!("[42]")
assert CljElixir.PersistentVector.pv_count(result) == 1
assert CljElixir.PersistentVector.pv_nth(result, 0) == 42
end
test "nested vectors" do
result = eval!("[[1 2] [3 4]]")
assert CljElixir.PersistentVector.pv_count(result) == 2
inner = CljElixir.PersistentVector.pv_nth(result, 0)
assert inner.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.pv_nth(inner, 0) == 1
end
test "vector with mixed types" do
result = eval!("[1 :two \"three\" nil true]")
assert CljElixir.PersistentVector.pv_count(result) == 5
assert CljElixir.PersistentVector.pv_nth(result, 0) == 1
assert CljElixir.PersistentVector.pv_nth(result, 1) == :two
assert CljElixir.PersistentVector.pv_nth(result, 2) == "three"
assert CljElixir.PersistentVector.pv_nth(result, 3) == nil
assert CljElixir.PersistentVector.pv_nth(result, 4) == true
end
end
# ==========================================================================
# vec and vector builtins
# ==========================================================================
describe "vec and vector builtins" do
test "vec converts list to PersistentVector" do
result = eval!("(vec (list 1 2 3))")
assert result.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.pv_count(result) == 3
end
test "vector creates PersistentVector from args" do
result = eval!("(vector 4 5 6)")
assert result.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(result) == [4, 5, 6]
end
test "vector with no args creates empty vector" do
result = eval!("(vector)")
assert CljElixir.PersistentVector.pv_count(result) == 0
end
test "vector? returns true for PersistentVector" do
assert eval!("(vector? [1 2 3])") == true
end
test "vector? returns false for list" do
assert eval!("(vector? (list 1 2 3))") == false
end
test "vector? returns false for map" do
assert eval!("(vector? {:a 1})") == false
end
end
# ==========================================================================
# Protocol dispatch — indexed access
# ==========================================================================
describe "indexed access" do
test "nth on vector" do
assert eval!("(nth [10 20 30] 0)") == 10
assert eval!("(nth [10 20 30] 1)") == 20
assert eval!("(nth [10 20 30] 2)") == 30
end
test "nth with not-found" do
assert eval!("(nth [10 20 30] 5 :missing)") == :missing
end
test "get on vector" do
assert eval!("(get [10 20 30] 1)") == 20
end
test "get with not-found" do
assert eval!("(get [10 20 30] 5 :missing)") == :missing
end
test "get with non-integer key returns nil" do
assert eval!("(get [10 20 30] :foo)") == nil
end
end
# ==========================================================================
# Protocol dispatch — mutation operations
# ==========================================================================
describe "mutation operations" do
test "conj appends to vector" do
result = eval!("(conj [1 2] 3)")
assert CljElixir.PersistentVector.to_list(result) == [1, 2, 3]
end
test "assoc updates index" do
result = eval!("(assoc [1 2 3] 1 :x)")
assert CljElixir.PersistentVector.to_list(result) == [1, :x, 3]
end
test "assoc at end appends" do
result = eval!("(assoc [1 2] 2 3)")
assert CljElixir.PersistentVector.to_list(result) == [1, 2, 3]
end
test "contains? with valid index" do
assert eval!("(contains? [1 2 3] 0)") == true
assert eval!("(contains? [1 2 3] 2)") == true
end
test "contains? with invalid index" do
assert eval!("(contains? [1 2 3] 3)") == false
assert eval!("(contains? [1 2 3] -1)") == false
end
end
# ==========================================================================
# Protocol dispatch — sequence operations
# ==========================================================================
describe "sequence operations" do
test "count on vector" do
assert eval!("(count [1 2 3])") == 3
assert eval!("(count [])") == 0
end
test "first on vector" do
assert eval!("(first [10 20 30])") == 10
end
test "first on empty vector" do
assert eval!("(first [])") == nil
end
test "seq on non-empty vector returns list" do
result = eval!("(seq [1 2 3])")
assert is_list(result)
assert result == [1, 2, 3]
end
test "seq on empty vector returns nil" do
assert eval!("(seq [])") == nil
end
test "empty? on vector" do
assert eval!("(empty? [])") == true
assert eval!("(empty? [1])") == false
end
end
# ==========================================================================
# Stack operations (peek/pop)
# ==========================================================================
describe "stack operations" do
test "peek returns last element" do
assert eval!("(peek [10 20 30])") == 30
end
test "pop removes last element" do
result = eval!("(pop [1 2 3])")
assert CljElixir.PersistentVector.to_list(result) == [1, 2]
end
test "pop single element returns empty" do
result = eval!("(pop [42])")
assert CljElixir.PersistentVector.pv_count(result) == 0
end
end
# ==========================================================================
# Vector as function (IFn)
# ==========================================================================
describe "vector as function" do
# TODO: Vector-as-function requires transformer support for struct invocation.
# The IFn protocol is implemented but Elixir doesn't auto-dispatch when a
# struct is in call position. Needs a transformer change to detect and wrap
# non-function call heads with IFn dispatch.
end
# ==========================================================================
# Pattern matching (unchanged — vectors match tuples in patterns)
# ==========================================================================
describe "pattern matching" do
test "vector in case pattern matches tuple" do
assert eval!("(case #el[:ok 42] [:ok x] x)") == 42
end
test "vector in let pattern matches tuple" do
assert eval!("(let [[:ok x] #el[:ok 99]] x)") == 99
end
test "nested vector patterns" do
result = eval!("""
(case #el[:ok #el[:inner 5]]
[:ok [:inner n]] n
_ nil)
""")
assert result == 5
end
end
# ==========================================================================
# Metadata
# ==========================================================================
describe "metadata" do
test "empty vector has nil meta" do
result = eval!("[]")
assert Map.get(result, :meta) == nil
end
end
# ==========================================================================
# Boundary conditions (trie level transitions)
# ==========================================================================
describe "boundary conditions" do
test "32 element vector (full tail, no trie)" do
v = CljElixir.PersistentVector.from_list(Enum.to_list(1..32))
assert CljElixir.PersistentVector.pv_count(v) == 32
assert CljElixir.PersistentVector.pv_nth(v, 0) == 1
assert CljElixir.PersistentVector.pv_nth(v, 31) == 32
end
test "33 element vector (trie overflow)" do
v = CljElixir.PersistentVector.from_list(Enum.to_list(1..33))
assert CljElixir.PersistentVector.pv_count(v) == 33
assert CljElixir.PersistentVector.pv_nth(v, 0) == 1
assert CljElixir.PersistentVector.pv_nth(v, 32) == 33
end
test "1025 element vector (multi-level trie)" do
v = CljElixir.PersistentVector.from_list(Enum.to_list(1..1025))
assert CljElixir.PersistentVector.pv_count(v) == 1025
assert CljElixir.PersistentVector.pv_nth(v, 0) == 1
assert CljElixir.PersistentVector.pv_nth(v, 1024) == 1025
end
test "conj across 32-element boundary" do
v32 = CljElixir.PersistentVector.from_list(Enum.to_list(1..32))
v33 = CljElixir.PersistentVector.pv_conj(v32, 33)
assert CljElixir.PersistentVector.pv_count(v33) == 33
assert CljElixir.PersistentVector.pv_nth(v33, 32) == 33
# Original unchanged (structural sharing)
assert CljElixir.PersistentVector.pv_count(v32) == 32
end
test "pop across 33-to-32 boundary" do
v33 = CljElixir.PersistentVector.from_list(Enum.to_list(1..33))
v32 = CljElixir.PersistentVector.pv_pop(v33)
assert CljElixir.PersistentVector.pv_count(v32) == 32
assert CljElixir.PersistentVector.pv_nth(v32, 31) == 32
end
test "assoc in trie (not tail)" do
v33 = CljElixir.PersistentVector.from_list(Enum.to_list(1..33))
v33b = CljElixir.PersistentVector.pv_assoc(v33, 0, :first)
assert CljElixir.PersistentVector.pv_nth(v33b, 0) == :first
assert CljElixir.PersistentVector.pv_nth(v33b, 1) == 2
# Original unchanged
assert CljElixir.PersistentVector.pv_nth(v33, 0) == 1
end
end
# ==========================================================================
# SubVector
# ==========================================================================
describe "subvec" do
test "subvec creates view into vector" do
sv = CljElixir.SubVector.sv_new(CljElixir.PersistentVector.from_list([1, 2, 3, 4, 5]), 1, 4)
assert CljElixir.SubVector.sv_count(sv) == 3
assert CljElixir.SubVector.sv_nth(sv, 0) == 2
assert CljElixir.SubVector.sv_nth(sv, 1) == 3
assert CljElixir.SubVector.sv_nth(sv, 2) == 4
end
test "subvec to_list" do
sv = CljElixir.SubVector.sv_new(CljElixir.PersistentVector.from_list([1, 2, 3, 4, 5]), 1, 4)
assert CljElixir.SubVector.sv_to_list(sv) == [2, 3, 4]
end
test "subvec 2-arity (start to end)" do
sv = CljElixir.SubVector.sv_new(CljElixir.PersistentVector.from_list([10, 20, 30]), 1)
assert CljElixir.SubVector.sv_count(sv) == 2
assert CljElixir.SubVector.sv_to_list(sv) == [20, 30]
end
test "subvec nth with not-found" do
sv = CljElixir.SubVector.sv_new(CljElixir.PersistentVector.from_list([1, 2, 3]), 0, 2)
assert CljElixir.SubVector.sv_nth(sv, 5, :missing) == :missing
end
end
# ==========================================================================
# Cross-type equality
# ==========================================================================
describe "cross-type equality" do
test "vector equals list with same elements" do
assert eval!("(= [1 2 3] (list 1 2 3))") == true
end
test "vector not equal to list with different elements" do
assert eval!("(= [1 2 3] (list 1 2 4))") == false
end
test "two vectors with same elements are equal" do
assert eval!("(= [1 2 3] [1 2 3])") == true
end
test "two vectors with different elements are not equal" do
assert eval!("(= [1 2] [1 2 3])") == false
end
test "not= works with cross-type" do
assert eval!("(not= [1 2 3] (list 1 2 3))") == false
assert eval!("(not= [1 2 3] (list 4 5 6))") == true
end
test "scalar equality still works" do
assert eval!("(= 1 1)") == true
assert eval!("(= 1 2)") == false
assert eval!("(= :a :a)") == true
end
end
# ==========================================================================
# Enumerable/Collectable protocols
# ==========================================================================
describe "Enumerable and Collectable" do
test "Enum.map over PersistentVector" do
pv = CljElixir.PersistentVector.from_list([1, 2, 3])
result = Enum.map(pv, &(&1 * 2))
assert result == [2, 4, 6]
end
test "Enum.filter over PersistentVector" do
pv = CljElixir.PersistentVector.from_list([1, 2, 3, 4, 5])
result = Enum.filter(pv, &(rem(&1, 2) == 0))
assert result == [2, 4]
end
test "Enum.count on PersistentVector" do
pv = CljElixir.PersistentVector.from_list([1, 2, 3])
assert Enum.count(pv) == 3
end
test "Enum.into PersistentVector" do
pv = Enum.into([1, 2, 3], CljElixir.PersistentVector.from_list([]))
assert pv.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(pv) == [1, 2, 3]
end
test "Enum.slice on PersistentVector" do
pv = CljElixir.PersistentVector.from_list([10, 20, 30, 40, 50])
assert Enum.slice(pv, 1, 3) == [20, 30, 40]
end
end
# ==========================================================================
# SubVector protocol dispatch
# ==========================================================================
describe "SubVector protocols" do
test "count via protocol" do
sv = CljElixir.SubVector.sv_new(CljElixir.PersistentVector.from_list([1, 2, 3, 4, 5]), 1, 4)
assert CljElixir.ICounted.count(sv) == 3
end
test "nth via protocol" do
sv = CljElixir.SubVector.sv_new(CljElixir.PersistentVector.from_list([10, 20, 30, 40]), 1, 3)
assert CljElixir.IIndexed.nth(sv, 0) == 20
assert CljElixir.IIndexed.nth(sv, 1) == 30
end
test "lookup via protocol" do
sv = CljElixir.SubVector.sv_new(CljElixir.PersistentVector.from_list([10, 20, 30]), 0, 2)
assert CljElixir.ILookup.lookup(sv, 0) == 10
assert CljElixir.ILookup.lookup(sv, 5) == nil
assert CljElixir.ILookup.lookup(sv, 5, :missing) == :missing
end
test "seq via protocol" do
sv = CljElixir.SubVector.sv_new(CljElixir.PersistentVector.from_list([1, 2, 3, 4]), 1, 3)
assert CljElixir.ISeqable.seq(sv) == [2, 3]
end
test "first/rest via protocol" do
sv = CljElixir.SubVector.sv_new(CljElixir.PersistentVector.from_list([10, 20, 30]), 0, 3)
assert CljElixir.ISeq.first(sv) == 10
assert CljElixir.ISeq.rest(sv) == [20, 30]
end
test "peek/pop via protocol" do
sv = CljElixir.SubVector.sv_new(CljElixir.PersistentVector.from_list([1, 2, 3, 4, 5]), 1, 4)
assert CljElixir.IStack.peek(sv) == 4
popped = CljElixir.IStack.pop(sv)
assert CljElixir.SubVector.sv_count(popped) == 2
end
end
# ==========================================================================
# Existing Phase 2 tests still pass (backward compatibility)
# ==========================================================================
describe "backward compatibility" do
test "maps still work with protocols" do
assert eval!("(get {:a 1 :b 2} :a)") == 1
assert eval!("(count {:a 1 :b 2})") == 2
end
test "lists still work with protocols" do
assert eval!("(first (list 10 20 30))") == 10
assert eval!("(count (list 1 2 3))") == 3
end
end
end
+392
View File
@@ -0,0 +1,392 @@
defmodule CljElixir.Phase4Test do
use ExUnit.Case, async: false
# Evaluate CljElixir code with PersistentVector enabled
defp eval!(source) do
case CljElixir.Compiler.eval_string(source) do
{:ok, result, _bindings} -> result
{:error, errors} -> raise "CljElixir eval error: #{inspect(errors)}"
end
end
# ==========================================================================
# tuple function
# ==========================================================================
describe "tuple function" do
test "empty tuple" do
result = eval!("(tuple)")
assert result == {}
end
test "single element tuple" do
result = eval!("(tuple :ok)")
assert result == {:ok}
end
test "two element tuple" do
result = eval!("(tuple :ok \"data\")")
assert result == {:ok, "data"}
end
test "three element tuple" do
result = eval!("(tuple 1 2 3)")
assert result == {1, 2, 3}
end
test "tuple with mixed types" do
result = eval!("(tuple :error 404 \"not found\")")
assert result == {:error, 404, "not found"}
end
test "tuple with nested tuple" do
result = eval!("(tuple :ok (tuple 1 2))")
assert result == {:ok, {1, 2}}
end
test "tuple-size on constructed tuple" do
result = eval!("(tuple-size (tuple :a :b :c))")
assert result == 3
end
test "elem on constructed tuple" do
result = eval!("(elem (tuple :a :b :c) 1)")
assert result == :b
end
test "tuple in let binding" do
result = eval!("""
(let [t (tuple :ok 42)]
(elem t 1))
""")
assert result == 42
end
test "tuple with expressions as arguments" do
result = eval!("(tuple (+ 1 2) (* 3 4))")
assert result == {3, 12}
end
end
# ==========================================================================
# clojurify
# ==========================================================================
describe "clojurify" do
test "tuple to vector" do
result = eval!("(clojurify #el[:ok \"data\"])")
assert result.__struct__ == CljElixir.PersistentVector
list = CljElixir.PersistentVector.to_list(result)
assert list == [:ok, "data"]
end
test "list to vector" do
result = eval!("(clojurify '(1 2 3))")
assert result.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(result) == [1, 2, 3]
end
test "nested tuple deep conversion" do
result = eval!("(clojurify #el[:ok #el[:nested \"data\"]])")
assert result.__struct__ == CljElixir.PersistentVector
list = CljElixir.PersistentVector.to_list(result)
assert hd(list) == :ok
inner = hd(tl(list))
assert inner.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(inner) == [:nested, "data"]
end
test "map values walked" do
result = eval!("(clojurify {:a #el[1 2]})")
assert is_map(result)
inner = Map.get(result, :a)
assert inner.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(inner) == [1, 2]
end
test "vector idempotent" do
result = eval!("(clojurify [1 2 3])")
assert result.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(result) == [1, 2, 3]
end
test "scalar passthrough - integer" do
assert eval!("(clojurify 42)") == 42
end
test "scalar passthrough - string" do
assert eval!("(clojurify \"hello\")") == "hello"
end
test "scalar passthrough - atom" do
assert eval!("(clojurify :foo)") == :foo
end
test "scalar passthrough - nil" do
assert eval!("(clojurify nil)") == nil
end
end
# ==========================================================================
# elixirify
# ==========================================================================
describe "elixirify" do
test "vector to list" do
result = eval!("(elixirify [1 2 3])")
assert is_list(result)
assert result == [1, 2, 3]
end
test "nested vector deep conversion" do
result = eval!("(elixirify [:ok [:nested \"data\"]])")
assert is_list(result)
assert result == [:ok, [:nested, "data"]]
end
test "map values walked" do
result = eval!("(elixirify {:a [1 2]})")
assert is_map(result)
assert Map.get(result, :a) == [1, 2]
end
test "list idempotent" do
result = eval!("(elixirify '(1 2 3))")
assert is_list(result)
assert result == [1, 2, 3]
end
test "tuple elements walked" do
result = eval!("(elixirify #el[:ok [1 2]])")
assert is_tuple(result)
assert elem(result, 0) == :ok
assert is_list(elem(result, 1))
assert elem(result, 1) == [1, 2]
end
test "scalar passthrough - integer" do
assert eval!("(elixirify 42)") == 42
end
test "scalar passthrough - string" do
assert eval!("(elixirify \"hello\")") == "hello"
end
test "scalar passthrough - atom" do
assert eval!("(elixirify :foo)") == :foo
end
end
# ==========================================================================
# Integration: roundtrips and composition
# ==========================================================================
describe "roundtrip conversions" do
test "clojurify then elixirify roundtrip on tuple" do
result = eval!("""
(elixirify (clojurify #el[:ok "data"]))
""")
assert is_list(result)
assert result == [:ok, "data"]
end
test "elixirify then clojurify roundtrip on vector" do
result = eval!("""
(clojurify (elixirify [1 2 3]))
""")
assert result.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(result) == [1, 2, 3]
end
test "deep nested roundtrip" do
result = eval!("""
(elixirify (clojurify #el[:ok #el[1 #el[2 3]]]))
""")
assert is_list(result)
assert result == [:ok, [1, [2, 3]]]
end
test "map with nested roundtrip" do
result = eval!("""
(elixirify (clojurify {:a #el[1 2] :b #el[3 4]}))
""")
assert is_map(result)
assert Map.get(result, :a) == [1, 2]
assert Map.get(result, :b) == [3, 4]
end
end
describe "tuple function with clojurify/elixirify" do
test "tuple function result can be clojurified" do
result = eval!("""
(clojurify (tuple :ok "data"))
""")
assert result.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(result) == [:ok, "data"]
end
test "elixirify vector matches tuple construction" do
# elixirify produces a list, not a tuple (by spec)
result = eval!("(elixirify [1 2 3])")
assert is_list(result)
assert result == [1, 2, 3]
end
test "tuple-size on tuple function result" do
result = eval!("(tuple-size (tuple :a :b :c :d))")
assert result == 4
end
end
describe "composition with core functions" do
test "map over list then clojurify" do
result = eval!("""
(clojurify (Enum/map '(1 2 3) (fn [x] (* x 2))))
""")
assert result.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(result) == [2, 4, 6]
end
test "elixirify vector for Enum interop" do
result = eval!("""
(Enum/sum (elixirify [1 2 3 4 5]))
""")
assert result == 15
end
test "clojurify in let binding" do
result = eval!("""
(let [v (clojurify #el[:ok 42])]
(nth v 1))
""")
assert result == 42
end
test "elixirify in let binding" do
result = eval!("""
(let [lst (elixirify [10 20 30])]
(hd lst))
""")
assert result == 10
end
end
# ==========================================================================
# SubVector clojurify/elixirify
# ==========================================================================
describe "SubVector clojurify" do
test "clojurify subvec returns vector" do
result = eval!("""
(let [v [1 2 3 4 5]
sv (subvec v 1 4)]
(clojurify sv))
""")
assert result.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(result) == [2, 3, 4]
end
end
describe "SubVector elixirify" do
test "elixirify subvec returns list" do
result = eval!("""
(let [v [1 2 3 4 5]
sv (subvec v 1 4)]
(elixirify sv))
""")
assert is_list(result)
assert result == [2, 3, 4]
end
end
# ==========================================================================
# Protocol extensibility
# ==========================================================================
describe "protocol extensibility" do
test "defrecord can extend IElixirify" do
result = eval!("""
(defmodule TestUser
(defrecord User [name age]
CljElixir.IElixirify
(-elixirify [u] {:name (Map/get u :name) :age (Map/get u :age) :type "user"})))
(let [u (TestUser.User/new "Alice" 30)]
(elixirify u))
""")
assert is_map(result)
assert Map.get(result, :name) == "Alice"
assert Map.get(result, :age) == 30
assert Map.get(result, :type) == "user"
end
test "defrecord can extend IClojurify" do
result = eval!("""
(defmodule TestPoint
(defrecord Point [x y]
CljElixir.IClojurify
(-clojurify [p] [(Map/get p :x) (Map/get p :y)])))
(let [p (TestPoint.Point/new 10 20)]
(clojurify p))
""")
assert result.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(result) == [10, 20]
end
end
# ==========================================================================
# Tuple sequence and collection operations
# ==========================================================================
describe "tuple sequence operations" do
test "seq on tuple" do
result = eval!("(seq #el[1 2 3])")
assert is_list(result)
assert result == [1, 2, 3]
end
test "seq on empty tuple" do
result = eval!("(seq (tuple))")
assert result == nil
end
test "first on tuple" do
result = eval!("(first #el[:a :b :c])")
assert result == :a
end
test "rest on tuple" do
result = eval!("(rest #el[:a :b :c])")
assert is_list(result)
assert result == [:b, :c]
end
test "conj on tuple" do
result = eval!("(conj #el[1 2] 3)")
assert is_tuple(result)
assert result == {1, 2, 3}
end
test "into empty tuple from vector" do
result = eval!("(into (tuple) [1 2 3])")
assert is_tuple(result)
assert result == {1, 2, 3}
end
test "into vector from tuple" do
result = eval!("(into [] #el[1 2 3])")
assert result.__struct__ == CljElixir.PersistentVector
assert CljElixir.PersistentVector.to_list(result) == [1, 2, 3]
end
test "into empty tuple from list" do
result = eval!("(into (tuple) '(1 2 3))")
assert is_tuple(result)
assert result == {1, 2, 3}
end
test "count on tuple via seq" do
result = eval!("(count #el[1 2 3 4])")
assert result == 4
end
end
end
+133
View File
@@ -0,0 +1,133 @@
defmodule CljElixir.Phase5Test do
use ExUnit.Case, async: false
# Helper to compile and evaluate CljElixir code
defp eval!(source) do
case CljElixir.Compiler.eval_string(source) do
{:ok, result, _bindings} -> result
{:error, errors} -> raise "CljElixir eval error: #{inspect(errors)}"
end
end
# ==========================================================================
# GenServer integration
# ==========================================================================
describe "GenServer - simple counter" do
test "define and use a counter GenServer" do
eval!("""
(defmodule TestCounter
(use GenServer)
(defn init [initial]
#el[:ok initial])
(defn handle-call
([:get _from state]
#el[:reply state state])
([:increment _from state]
#el[:reply :ok (+ state 1)])))
""")
{:ok, pid} = GenServer.start_link(TestCounter, 0)
assert GenServer.call(pid, :get) == 0
assert GenServer.call(pid, :increment) == :ok
assert GenServer.call(pid, :get) == 1
GenServer.stop(pid)
end
end
describe "GenServer - handle_cast" do
test "cast resets state" do
eval!("""
(defmodule TestCaster
(use GenServer)
(defn init [initial]
#el[:ok initial])
(defn handle-call
([:get _from state]
#el[:reply state state]))
(defn handle-cast
([:reset _state]
#el[:noreply 0])))
""")
{:ok, pid} = GenServer.start_link(TestCaster, 42)
assert GenServer.call(pid, :get) == 42
GenServer.cast(pid, :reset)
Process.sleep(50)
assert GenServer.call(pid, :get) == 0
GenServer.stop(pid)
end
end
describe "GenServer - handle_info" do
test "handle-info receives plain messages" do
eval!("""
(defmodule TestInfoHandler
(use GenServer)
(defn init [initial]
#el[:ok initial])
(defn handle-call
([:get _from state]
#el[:reply state state]))
(defn handle-info
([:bump state]
#el[:noreply (+ state 1)])))
""")
{:ok, pid} = GenServer.start_link(TestInfoHandler, 0)
send(pid, :bump)
Process.sleep(50)
assert GenServer.call(pid, :get) == 1
GenServer.stop(pid)
end
end
# ==========================================================================
# ChatRoom pattern: spawn + send + receive loop
# ==========================================================================
describe "ChatRoom pattern" do
test "spawn + send + receive loop" do
eval!("""
(defmodule TestChatLoop
(defn loop [state]
(receive
[:ping pid]
(do
(send pid #el[:pong state])
(TestChatLoop/loop (+ state 1)))
[:get pid]
(do
(send pid #el[:count state])
(TestChatLoop/loop state))
:stop
:stopped
:after 5000
:timeout)))
""")
pid = spawn(fn -> TestChatLoop.loop(0) end)
send(pid, {:ping, self()})
assert_receive {:pong, 0}, 1000
send(pid, {:ping, self()})
assert_receive {:pong, 1}, 1000
send(pid, {:get, self()})
assert_receive {:count, 2}, 1000
send(pid, :stop)
Process.sleep(50)
refute Process.alive?(pid)
end
end
end
+494
View File
@@ -0,0 +1,494 @@
defmodule CljElixir.Phase6Test do
use ExUnit.Case, async: false
# Helper to compile and evaluate CljElixir code
defp eval!(source) do
case CljElixir.Compiler.eval_string(source) do
{:ok, result, _bindings} -> result
{:error, errors} -> raise "CljElixir eval error: #{inspect(errors)}"
end
end
# ==========================================================================
# Thread-first (->)
# ==========================================================================
describe "-> (thread-first)" do
test "single value passthrough: (-> 1) => 1" do
assert eval!("(-> 1)") == 1
end
test "basic threading with bare symbols: (-> 1 inc inc) => 3" do
assert eval!("(-> 1 inc inc)") == 3
end
test "threading into multi-arg function: (-> \"hello\" (str \" world\"))" do
assert eval!("(-> \"hello\" (str \" world\"))") == "hello world"
end
test "threading with arithmetic: (-> 5 inc (+ 10)) => 16" do
assert eval!("(-> 5 inc (+ 10))") == 16
end
test "threading with module calls: (-> \"hello\" (String/upcase))" do
assert eval!("(-> \"hello\" (String/upcase))") == "HELLO"
end
test "threading into first position of list operations" do
# (-> [1 2 3] (Enum/at 0)) => 1
assert eval!("(-> [1 2 3] (Enum/at 0))") == 1
end
test "nested threading" do
# (-> 1 (-> inc inc)) is valid — inner -> produces 3? No:
# (-> 1 inc (+ (-> 10 dec))) = (+ (inc 1) (dec 10)) = (+ 2 9) = 11
assert eval!("(-> 1 inc (+ (-> 10 dec)))") == 11
end
test "thread-first with let binding" do
result = eval!("""
(let [x 5]
(-> x inc inc))
""")
assert result == 7
end
test "threading with comparison" do
assert eval!("(-> 5 inc (> 3))") == true
end
end
# ==========================================================================
# Thread-last (->>)
# ==========================================================================
describe "->> (thread-last)" do
test "single value passthrough: (->> 1) => 1" do
assert eval!("(->> 1)") == 1
end
test "thread-last with bare symbols: (->> 1 inc inc) => 3" do
assert eval!("(->> 1 inc inc)") == 3
end
test "thread-last inserts as last argument" do
# (->> 1 (+ 10)) => (+ 10 1) => 11
assert eval!("(->> 1 (+ 10))") == 11
end
test "thread-last with map over list" do
# (->> [1 2 3] (map (fn [x] (inc x)))) => (map (fn [x] (inc x)) [1 2 3]) => [2 3 4]
assert eval!("(->> [1 2 3] (map (fn [x] (inc x))))") == [2, 3, 4]
end
test "thread-last with filter" do
# (->> [1 2 3 4 5] (filter (fn [x] (> x 2)))) => [3, 4, 5]
assert eval!("(->> [1 2 3 4 5] (filter (fn [x] (> x 2))))") == [3, 4, 5]
end
test "thread-last chaining collection ops" do
# (->> [1 2 3 4 5] (map (fn [x] (inc x))) (filter (fn [x] (> x 3))))
# => (filter (fn [x] (> x 3)) (map (fn [x] (inc x)) [1 2 3 4 5]))
# => (filter (fn [x] (> x 3)) [2 3 4 5 6])
# => [4 5 6]
assert eval!("(->> [1 2 3 4 5] (map (fn [x] (inc x))) (filter (fn [x] (> x 3))))") == [4, 5, 6]
end
test "nested thread-last" do
assert eval!("(->> 10 dec (+ (->> 1 inc)))") == 11
end
end
# ==========================================================================
# Mixed / edge cases
# ==========================================================================
describe "threading edge cases" do
test "threading with keyword-as-function" do
# (-> {:name "Alice"} :name) => "Alice"
assert eval!("(-> {:name \"Alice\"} :name)") == "Alice"
end
test "thread-first string operations" do
assert eval!("(-> \"hello world\" (String/upcase) (String/split \" \"))") == ["HELLO", "WORLD"]
end
test "deeply nested threading" do
# (-> 0 inc inc inc inc inc) => 5
assert eval!("(-> 0 inc inc inc inc inc)") == 5
end
test "thread-first with dec" do
assert eval!("(-> 10 dec dec dec)") == 7
end
test "thread-last with Enum/reduce" do
# (->> [1 2 3 4] (Enum/sum)) => 10
assert eval!("(->> [1 2 3 4] (Enum/sum))") == 10
end
end
# ==========================================================================
# try / catch / finally
# ==========================================================================
describe "try/catch/finally" do
test "try with rescue catches exception" do
result = eval!("(try (throw \"boom\") (catch e (str \"caught: \" (Exception/message e))))")
assert result == "caught: boom"
end
test "try with typed rescue" do
result = eval!("""
(try
(throw "boom")
(catch RuntimeError e
(str "runtime: " (Exception/message e))))
""")
assert result == "runtime: boom"
end
test "try with finally" do
# finally runs but doesn't affect return value
result = eval!("""
(try
42
(finally (println "cleanup")))
""")
assert result == 42
end
test "try with catch :throw" do
result = eval!("""
(try
(Kernel/throw :oops)
(catch :throw val val))
""")
assert result == :oops
end
test "try with catch :exit" do
result = eval!("""
(try
(Kernel/exit :shutdown)
(catch :exit reason reason))
""")
assert result == :shutdown
end
test "try returns body value when no exception" do
result = eval!("(try (+ 1 2) (catch e e))")
assert result == 3
end
test "try with multiple catch clauses" do
result = eval!("""
(try
(throw "oops")
(catch ArgumentError e :arg_error)
(catch RuntimeError e :runtime_error))
""")
assert result == :runtime_error
end
test "try with rescue and finally" do
result = eval!("""
(try
(throw "oops")
(catch e :caught)
(finally (println "done")))
""")
assert result == :caught
end
end
# ==========================================================================
# & rest variadic params
# ==========================================================================
describe "& rest variadic params" do
test "defn with & rest, no rest args (uses default [])" do
result = eval!("""
(defmodule VarTest1
(defn foo [x & rest]
(count rest)))
(VarTest1/foo 1)
""")
assert result == 0
end
test "defn with & rest, with rest args passed as list" do
result = eval!("""
(defmodule VarTest2
(defn foo [x & rest]
rest))
(VarTest2/foo 1 (list 2 3 4))
""")
assert result == [2, 3, 4]
end
test "defn with & rest uses rest in body" do
result = eval!("""
(defmodule VarTest3
(defn foo [x & rest]
(+ x (count rest))))
(VarTest3/foo 10)
""")
assert result == 10
end
test "defn with & rest, multiple required params" do
result = eval!("""
(defmodule VarTest4
(defn foo [a b & rest]
(+ a b (count rest))))
(VarTest4/foo 1 2)
""")
assert result == 3
end
test "defn with & rest, with rest args and multiple required params" do
result = eval!("""
(defmodule VarTest4b
(defn foo [a b & rest]
(+ a b (count rest))))
(VarTest4b/foo 1 2 (list 10 20 30))
""")
assert result == 6
end
test "fn with & rest called inline" do
# Call the fn inline since let-bound fn variable calls aren't supported yet
result = eval!("""
((fn [x & rest] (+ x (count rest))) 5 (list 1 2 3))
""")
assert result == 8
end
test "defn with only & rest param" do
result = eval!("""
(defmodule VarTest5
(defn foo [& args]
(count args)))
(VarTest5/foo)
""")
assert result == 0
end
test "defn with only & rest param, with args" do
result = eval!("""
(defmodule VarTest6
(defn foo [& args]
args))
(VarTest6/foo (list 1 2 3))
""")
assert result == [1, 2, 3]
end
end
# ==========================================================================
# Destructuring
# ==========================================================================
describe "destructuring" do
test "map :keys destructuring in let" do
result = eval!("""
(let [{:keys [name age]} {:name "alice" :age 30}]
(str name " is " age))
""")
assert result == "alice is 30"
end
test "map :keys with :as" do
result = eval!("""
(let [{:keys [name] :as person} {:name "bob" :age 25}]
(str name " " (count person)))
""")
# count on a map returns number of k/v pairs
assert result == "bob 2"
end
test "map :strs destructuring" do
result = eval!("""
(let [{:strs [name]} {"name" "charlie"}]
name)
""")
assert result == "charlie"
end
test "map destructuring with literal keys" do
result = eval!("""
(let [{x :x y :y} {:x 1 :y 2}]
(+ x y))
""")
assert result == 3
end
test "sequential destructuring with & rest in let" do
result = eval!("""
(let [[a b & rest] (list 1 2 3 4 5)]
rest)
""")
assert result == [3, 4, 5]
end
test "sequential destructuring without rest" do
# Without &, vector in pattern still matches tuple
result = eval!("""
(let [[a b] #el[1 2]]
(+ a b))
""")
assert result == 3
end
test "map :keys in defn params" do
result = eval!("""
(defmodule DestructTest1
(defn greet [{:keys [name greeting]}]
(str greeting " " name)))
(DestructTest1/greet {:name "alice" :greeting "hi"})
""")
assert result == "hi alice"
end
test "sequential destructuring in fn params" do
# Call fn inline since let-bound fn variable calls aren't supported yet
result = eval!("""
((fn [[a b & rest]] (+ a b (count rest))) (list 10 20 30 40))
""")
assert result == 32
end
test "nested map destructuring" do
result = eval!("""
(let [{:keys [name] {:keys [city]} :address} {:name "alice" :address {:city "NYC"}}]
(str name " in " city))
""")
assert result == "alice in NYC"
end
test "map :keys in for binding" do
result = eval!("""
(for [{:keys [name]} (list {:name "a"} {:name "b"} {:name "c"})]
name)
""")
assert result == ["a", "b", "c"]
end
test "sequential destructuring in for with &" do
result = eval!("""
(for [[a b & _rest] (list (list 1 2 99) (list 3 4 99))]
(+ a b))
""")
assert result == [3, 7]
end
test "map destructuring with hyphenated keys" do
result = eval!("""
(let [{:keys [first-name]} {:"first-name" "alice"}]
first-name)
""")
assert result == "alice"
end
end
# ==========================================================================
# defmacro
# ==========================================================================
describe "defmacro" do
test "simple macro - unless" do
result = eval!("""
(defmodule MacroTest1
(defmacro unless [test then]
`(if (not ~test) ~then))
(defn check [x]
(unless (> x 0) :negative)))
(MacroTest1/check -5)
""")
assert result == :negative
end
test "macro with & body and splice-unquote" do
result = eval!("""
(defmodule MacroTest2
(defmacro unless [test & body]
`(if (not ~test) (do ~@body)))
(defn check [x]
(unless (> x 0) :negative)))
(MacroTest2/check -1)
""")
assert result == :negative
end
test "macro with multiple body forms" do
result = eval!("""
(defmodule MacroTest3
(defmacro unless [test & body]
`(if (not ~test) (do ~@body)))
(defn check [x]
(unless (> x 0)
(println "not positive")
:negative)))
(MacroTest3/check -1)
""")
assert result == :negative
end
test "macro expands correctly with complex expressions" do
result = eval!("""
(defmodule MacroTest4
(defmacro when-positive [x & body]
`(if (> ~x 0) (do ~@body)))
(defn test-it [n]
(when-positive n
(+ n 10))))
(MacroTest4/test-it 5)
""")
assert result == 15
end
test "macro returns nil when condition not met" do
result = eval!("""
(defmodule MacroTest5
(defmacro when-positive [x & body]
`(if (> ~x 0) (do ~@body)))
(defn test-it [n]
(when-positive n
(+ n 10))))
(MacroTest5/test-it -5)
""")
assert result == nil
end
test "auto-gensym in macro" do
result = eval!("""
(defmodule MacroTest6
(defmacro my-let1 [val & body]
`(let [result# ~val]
(do ~@body)))
(defn use-it []
(my-let1 42
:ok)))
(MacroTest6/use-it)
""")
assert result == :ok
end
test "multiple macros in same module" do
result = eval!("""
(defmodule MacroTest7
(defmacro unless [test & body]
`(if (not ~test) (do ~@body)))
(defmacro when-positive [x & body]
`(if (> ~x 0) (do ~@body)))
(defn check [x]
(if (when-positive x (> x 10))
:big
(unless (> x 0) :non-positive))))
(MacroTest7/check 20)
""")
assert result == :big
end
end
end
+232
View File
@@ -0,0 +1,232 @@
defmodule CljElixir.Phase7Test do
use ExUnit.Case, async: false
defp eval!(source) do
case CljElixir.Compiler.eval_string(source) do
{:ok, result, _} -> result
{:error, errors} -> raise "Compilation failed: #{inspect(errors)}"
end
end
defp compile!(source) do
case CljElixir.Compiler.compile_to_beam(source) do
{:ok, modules} -> modules
{:error, errors} -> raise "Compilation failed: #{inspect(errors)}"
end
end
describe "m/=> function specs" do
test "simple function spec compiles" do
compile!("""
(defmodule SpecTest1
(defn hello [name]
(str "hello " name))
(m/=> hello [:=> [:cat :string] :string]))
""")
assert true
after
:code.purge(SpecTest1)
:code.delete(SpecTest1)
end
test "multi-param function spec" do
compile!("""
(defmodule SpecTest2
(defn add [a b]
(+ a b))
(m/=> add [:=> [:cat :int :int] :int]))
""")
assert true
after
:code.purge(SpecTest2)
:code.delete(SpecTest2)
end
test "optional param generates multiple specs" do
compile!("""
(defmodule SpecTest3
(defn greet
([name] (greet name "hello"))
([name greeting] (str greeting " " name)))
(m/=> greet [:=> [:cat :string [:? :string]] :string]))
""")
assert true
after
:code.purge(SpecTest3)
:code.delete(SpecTest3)
end
test "multi-arity via :function" do
compile!("""
(defmodule SpecTest4
(defn greet
([name] (greet name "hello"))
([name greeting] (str greeting " " name)))
(m/=> greet [:function
[:=> [:cat :string] :string]
[:=> [:cat :string :string] :string]]))
""")
assert true
after
:code.purge(SpecTest4)
:code.delete(SpecTest4)
end
test "spec with various types" do
compile!("""
(defmodule SpecTest5
(defn process [x]
x)
(m/=> process [:=> [:cat :any] [:or :int :string :nil]]))
""")
assert true
after
:code.purge(SpecTest5)
:code.delete(SpecTest5)
end
test "spec with map type" do
compile!("""
(defmodule SpecTest6
(defn get-name [user]
(:name user))
(m/=> get-name [:=> [:cat [:map [:name :string] [:age :int]]] :string]))
""")
assert true
after
:code.purge(SpecTest6)
:code.delete(SpecTest6)
end
test "spec with tuple return" do
compile!("""
(defmodule SpecTest7
(defn fetch [id]
#el[:ok id])
(m/=> fetch [:=> [:cat :int] [:tuple :atom :int]]))
""")
assert true
after
:code.purge(SpecTest7)
:code.delete(SpecTest7)
end
end
describe "auto @type from def schemas" do
test "def with map schema generates type" do
compile!("""
(defmodule TypeTest1
(def User [:map [:name :string] [:age :int]])
(defn get-name [user]
(:name user)))
""")
assert true
after
:code.purge(TypeTest1)
:code.delete(TypeTest1)
end
test "def with or schema" do
compile!("""
(defmodule TypeTest2
(def Status [:enum :active :inactive :pending])
(defn check [s] s))
""")
assert true
after
:code.purge(TypeTest2)
:code.delete(TypeTest2)
end
test "def with and schema" do
compile!("""
(defmodule TypeTest3
(def PositiveInt [:and :int [:> 0]])
(defn check [n] n))
""")
assert true
after
:code.purge(TypeTest3)
:code.delete(TypeTest3)
end
end
describe "schema cross-references" do
test "spec references a named schema type" do
compile!("""
(defmodule CrossRefTest1
(def User [:map [:name :string] [:age :int]])
(defn get-user [id]
{:name "alice" :age 30})
(m/=> get-user [:=> [:cat :int] User]))
""")
assert true
after
:code.purge(CrossRefTest1)
:code.delete(CrossRefTest1)
end
test "schema references another schema" do
compile!("""
(defmodule CrossRefTest2
(def PositiveInt [:and :int [:> 0]])
(def Config [:map
[:host :string]
[:port PositiveInt]
[:ssl? :boolean]])
(defn load-config []
{:host "localhost" :port 8080 :"ssl?" true}))
""")
assert true
after
:code.purge(CrossRefTest2)
:code.delete(CrossRefTest2)
end
end
describe "recursive schemas" do
test "recursive schema with registry" do
compile!("""
(defmodule RecursiveTest1
(def Tree [:schema {:registry {:tree [:or :int [:tuple [:ref :tree] [:ref :tree]]]}} [:ref :tree]])
(defn make-leaf [n] n))
""")
assert true
after
:code.purge(RecursiveTest1)
:code.delete(RecursiveTest1)
end
end
describe "functions still work correctly" do
test "module with spec can be called" do
result = eval!("""
(defmodule FuncSpecTest1
(defn hello [name]
(str "hello " name))
(m/=> hello [:=> [:cat :string] :string]))
(FuncSpecTest1/hello "world")
""")
assert result == "hello world"
after
:code.purge(FuncSpecTest1)
:code.delete(FuncSpecTest1)
end
test "module with type and spec" do
result = eval!("""
(defmodule FuncSpecTest2
(def User [:map [:name :string] [:age :int]])
(defn make-user [name age]
{:name name :age age})
(m/=> make-user [:=> [:cat :string :int] User]))
(FuncSpecTest2/make-user "alice" 30)
""")
assert result == %{name: "alice", age: 30}
after
:code.purge(FuncSpecTest2)
:code.delete(FuncSpecTest2)
end
end
end
+820
View File
@@ -0,0 +1,820 @@
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
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
ExUnit.start()