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