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

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

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

495 lines
14 KiB
Elixir

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