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>
418 lines
12 KiB
Elixir
418 lines
12 KiB
Elixir
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
|