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>
495 lines
14 KiB
Elixir
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
|