From 1147a9f4fd51957cf2fe1faa772c9639a3573aac Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Thu, 21 May 2026 20:41:10 +0200 Subject: [PATCH] Add map, filter, and reverse to all persistent data structures Add map/filter operations to lists, queues, deques, tries, ord-maps, and hash-maps. Lists also get reverse. Map types get map-values (since keys are fixed by the type). All implementations use the existing reduce infrastructure for simplicity and correctness. Hash-map map-values/filter use trie-reduce-values directly to avoid a pre-existing nested closure capture bug in hash-map reduce. --- persistent.carp | 148 ++++++++++++++++++++++++++++++++++ test/persistent_deque.carp | 91 ++++++++++++++++----- test/persistent_hash_map.carp | 85 ++++++++++++++++--- test/persistent_list.carp | 112 ++++++++++++++++++++----- test/persistent_ord_map.carp | 100 ++++++++++++++++++----- test/persistent_queue.carp | 102 ++++++++++++++++------- test/persistent_trie.carp | 86 +++++++++++++++----- 7 files changed, 602 insertions(+), 122 deletions(-) diff --git a/persistent.carp b/persistent.carp index cdbfbb5..aef7850 100644 --- a/persistent.carp +++ b/persistent.carp @@ -201,6 +201,32 @@ This generates: (Maybe.Just b-head) (%node-rc-ptr-eq &a-head &b-head) (Maybe.Nothing) false))) + (doc reverse "Return a reversed copy of the list.") + (sig reverse (Fn [(Ref %name q)] %name)) + (defn reverse [list-ref] + (reduce &(fn [acc x] (prepend x &acc)) (empty) list-ref)) + + (doc map + "Apply a function to each element, returning a new list in the same order.") + (sig map + (Fn [(Ref (Fn [%value-type] %value-type) q) (Ref %name r)] %name)) + (defn map [f list-ref] + (let [rev (reduce &(fn [acc x] (prepend (~f x) &acc)) + (empty) + list-ref)] + (reverse &rev))) + + (doc filter + "Keep only elements satisfying a predicate, preserving order.") + (sig filter + (Fn [(Ref (Fn [(Ref %value-type q)] Bool) r) (Ref %name s)] %name)) + (defn filter [pred list-ref] + (let [rev (reduce + &(fn [acc x] (if (~pred &x) (prepend x &acc) acc)) + (empty) + list-ref)] + (reverse &rev))) + (doc str "Diagnostic formatting for a list.") (sig str (Fn [(Ref %name q)] String)) (defn str [list-ref] @@ -366,6 +392,23 @@ This generates: ab (to-array b-ref)] (= &aa &ab)))) (implements = %name-eq) + (doc map + "Apply a function to each element, returning a new queue in FIFO order.") + (sig map + (Fn [(Ref (Fn [%value-type] %value-type) q) (Ref %name r)] %name)) + (defn map [f q-ref] + (reduce &(fn [acc x] (enqueue (~f x) &acc)) (empty) q-ref)) + + (doc filter + "Keep only elements satisfying a predicate, preserving FIFO order.") + (sig filter + (Fn [(Ref (Fn [(Ref %value-type q)] Bool) r) (Ref %name s)] %name)) + (defn filter [pred q-ref] + (reduce + &(fn [acc x] (if (~pred &x) (enqueue x &acc) acc)) + (empty) + q-ref)) + (doc str "Diagnostic formatting for a queue.") (sig str (Fn [(Ref %name q)] String)) (defn str [q-ref] (fmt "(PersistentQueue length=%ld)" (length q-ref))) @@ -1048,6 +1091,32 @@ This generates: a-ref))) (implements = %name-eq) + (doc map-values + "Apply a function to each value, returning a new trie with the same keys.") + (sig map-values + (Fn [(Ref (Fn [%value-type] %value-type) q) (Ref %name r)] %name)) + (defn map-values [f trie-ref] + (reduce + &(fn [acc p] (insert (Pair.a &p) (~f @(Pair.b &p)) &acc)) + (empty) + trie-ref)) + + (doc filter + "Keep only entries whose `(Pair key value)` satisfies a predicate.") + (sig filter + (Fn + [(Ref + (Fn [(Ref (Pair (Array %key-part-type) %value-type) q)] Bool) + r) + (Ref %name s)] + %name)) + (defn filter [pred trie-ref] + (reduce + &(fn [acc p] + (if (~pred &p) (insert (Pair.a &p) @(Pair.b &p) &acc) acc)) + (empty) + trie-ref)) + (doc str "Diagnostic formatting for a trie.") (sig str (Fn [(Ref %name q)] String)) (defn str [trie-ref] @@ -1261,6 +1330,23 @@ This generates: ab (to-array b-ref)] (= &aa &ab)))) (implements = %name-eq) + (doc map + "Apply a function to each element, returning a new deque in front-to-back order.") + (sig map + (Fn [(Ref (Fn [%value-type] %value-type) q) (Ref %name r)] %name)) + (defn map [f dq-ref] + (reduce &(fn [acc x] (push-back (~f x) &acc)) (empty) dq-ref)) + + (doc filter + "Keep only elements satisfying a predicate, preserving front-to-back order.") + (sig filter + (Fn [(Ref (Fn [(Ref %value-type q)] Bool) r) (Ref %name s)] %name)) + (defn filter [pred dq-ref] + (reduce + &(fn [acc x] (if (~pred &x) (push-back x &acc) acc)) + (empty) + dq-ref)) + (doc str "Diagnostic formatting for a deque.") (sig str (Fn [(Ref %name q)] String)) (defn str [dq-ref] @@ -1712,6 +1798,30 @@ This generates: ab (to-array b-ref)] (= &aa &ab)))) (implements = %name-eq) + (doc map-values + "Apply a function to each value, returning a new map with the same keys.") + (sig map-values + (Fn [(Ref (Fn [%value-type] %value-type) q) (Ref %name r)] %name)) + (defn map-values [f map-ref] + (reduce + &(fn [acc p] (insert @(Pair.a &p) (~f @(Pair.b &p)) &acc)) + (empty) + map-ref)) + + (doc filter + "Keep only entries whose `(Pair key value)` satisfies a predicate.") + (sig filter + (Fn + [(Ref (Fn [(Ref (Pair %key-type %value-type) q)] Bool) r) + (Ref %name s)] + %name)) + (defn filter [pred map-ref] + (reduce + &(fn [acc p] + (if (~pred &p) (insert @(Pair.a &p) @(Pair.b &p) &acc) acc)) + (empty) + map-ref)) + (doc str "Diagnostic formatting for an ordered map.") (sig str (Fn [(Ref %name q)] String)) (defn str [map-ref] @@ -2310,6 +2420,44 @@ Example: a-ref))) (implements = %name-eq) + (doc map-values + "Apply a function to each value, returning a new map with the same keys.") + (sig map-values + (Fn [(Ref (Fn [%value-type] %value-type) q) (Ref %name r)] %name)) + (defn map-values [f map-ref] + (%trie-reduce-values + &(fn [acc bucket] + (let-do [m acc] + (for [i 0 (Array.length &bucket)] + (let [pair-ref (Array.unsafe-nth &bucket i)] + (set! m + (insert @(Pair.a pair-ref) + (~f @(Pair.b pair-ref)) + &m)))) + m)) + (empty) + (%map-trie map-ref))) + + (doc filter + "Keep only entries whose `(Pair key value)` satisfies a predicate.") + (sig filter + (Fn + [(Ref (Fn [(Ref (Pair %key-type %value-type) q)] Bool) r) + (Ref %name s)] + %name)) + (defn filter [pred map-ref] + (%trie-reduce-values + &(fn [acc bucket] + (let-do [m acc] + (for [i 0 (Array.length &bucket)] + (let [pair-ref (Array.unsafe-nth &bucket i)] + (when (~pred pair-ref) + (set! m + (insert @(Pair.a pair-ref) @(Pair.b pair-ref) &m))))) + m)) + (empty) + (%map-trie map-ref))) + (doc str "Diagnostic formatting for a hash map.") (sig str (Fn [(Ref %name q)] String)) (defn str [map-ref] diff --git a/test/persistent_deque.carp b/test/persistent_deque.carp index 60c5c03..bf4790a 100644 --- a/test/persistent_deque.carp +++ b/test/persistent_deque.carp @@ -15,8 +15,7 @@ (defn build-long-back-deque [n] (let-do [d (IntDeque.empty)] - (for [i 0 n] - (set! d (IntDeque.push-back i &d))) + (for [i 0 n] (set! d (IntDeque.push-back i &d))) d)) (defn deque-branch-lifecycle [] @@ -25,13 +24,10 @@ d2 (IntDeque.push-back 2 &d1) d3 (IntDeque.push-front 0 &d2) d4 (IntDeque.push-back 3 &d3)] - (do - (ignore d2) - (ignore d4)))) + (do (ignore d2) (ignore d4)))) (defn pop-front-values [dq-ref] - (let [out0 []] - (pop-front-values-iter dq-ref &out0))) + (let [out0 []] (pop-front-values-iter dq-ref &out0))) (defn pop-front-values-iter [dq-ref out-ref] (match (IntDeque.pop-front dq-ref) @@ -46,14 +42,12 @@ (deftest test (assert-equal test true - (let [d (IntDeque.empty)] - (IntDeque.empty? &d)) + (let [d (IntDeque.empty)] (IntDeque.empty? &d)) "empty deque reports empty") (assert-equal test 0l - (let [d (IntDeque.empty)] - (IntDeque.length &d)) + (let [d (IntDeque.empty)] (IntDeque.length &d)) "empty deque has length 0") (assert-equal test @@ -62,12 +56,13 @@ d1 (IntDeque.push-back 1 &d0) d2 (IntDeque.push-front 0 &d1) d3 (IntDeque.push-back 2 &d2)] - (and (match (IntDeque.peek-front &d3) - (Maybe.Just v) (= v 0) - (Maybe.Nothing) false) - (match (IntDeque.peek-back &d3) - (Maybe.Just v) (= v 2) - (Maybe.Nothing) false))) + (and + (match (IntDeque.peek-front &d3) + (Maybe.Just v) (= v 0) + (Maybe.Nothing) false) + (match (IntDeque.peek-back &d3) + (Maybe.Just v) (= v 2) + (Maybe.Nothing) false))) "peek-front/peek-back read both ends") (assert-equal test @@ -119,8 +114,7 @@ (Maybe.Just p) (let [v @(Pair.a &p) d2 @(Pair.b &p)] - (and (= v 0) - (= (IntDeque.length &d2) 99999l))))) + (and (= v 0) (= (IntDeque.length &d2) 99999l))))) "pop-front on 100k rear-only deque rebalances without blowing the C stack") (assert-equal test @@ -153,5 +147,60 @@ (and (= &a &b) (not (= &a &c)))) "= is structural regardless of front/rear split") - (assert-memory-balance test deque-branch-lifecycle 0l "persistent deque branch lifecycle is memory-balanced") -) + (assert-equal test + true + (let [d0 (IntDeque.empty) + d1 (IntDeque.push-back 1 &d0) + d2 (IntDeque.push-back 2 &d1) + d3 (IntDeque.push-front 0 &d2) + mapped (IntDeque.map &(fn [x] (+ x 10)) &d3) + arr (IntDeque.to-array &mapped)] + (and (= (Array.length &arr) 3) + (= @(Array.unsafe-nth &arr 0) 10) + (= @(Array.unsafe-nth &arr 1) 11) + (= @(Array.unsafe-nth &arr 2) 12))) + "map applies function preserving front-to-back order") + + (assert-equal test + true + (let [d (IntDeque.empty) + m (IntDeque.map &(fn [x] (+ x 1)) &d)] + (IntDeque.empty? &m)) + "map over empty deque yields empty") + + (assert-equal test + true + (let [d0 (IntDeque.empty) + d1 (IntDeque.push-back 1 &d0) + d2 (IntDeque.push-back 2 &d1) + d3 (IntDeque.push-back 3 &d2) + d4 (IntDeque.push-front 0 &d3) + filtered (IntDeque.filter &(fn [x] (> @x 1)) &d4) + arr (IntDeque.to-array &filtered)] + (and (= (Array.length &arr) 2) + (= @(Array.unsafe-nth &arr 0) 2) + (= @(Array.unsafe-nth &arr 1) 3))) + "filter keeps elements matching predicate in order") + + (assert-equal test + true + (let [d0 (IntDeque.empty) + d1 (IntDeque.push-back 1 &d0) + d2 (IntDeque.push-back 2 &d1) + filtered (IntDeque.filter &(fn [x] false) &d2)] + (IntDeque.empty? &filtered)) + "filter with always-false yields empty deque") + + (assert-equal test + true + (let [d0 (IntDeque.empty) + d1 (IntDeque.push-back 1 &d0) + d2 (IntDeque.push-back 2 &d1) + filtered (IntDeque.filter &(fn [x] true) &d2)] + (= &filtered &d2)) + "filter with always-true preserves all elements") + + (assert-memory-balance test + deque-branch-lifecycle + 0l + "persistent deque branch lifecycle is memory-balanced")) diff --git a/test/persistent_hash_map.carp b/test/persistent_hash_map.carp index 547fc4d..75ac9cb 100644 --- a/test/persistent_hash_map.carp +++ b/test/persistent_hash_map.carp @@ -20,15 +20,12 @@ m3 (IntIntMap.insert 3 30 &m1) k1 1 m4 (IntIntMap.remove &k1 &m2)] - (do - (ignore m3) - (ignore m4)))) + (do (ignore m3) (ignore m4)))) (deftest test (assert-equal test true - (let [m (IntIntMap.empty)] - (IntIntMap.empty? &m)) + (let [m (IntIntMap.empty)] (IntIntMap.empty? &m)) "empty hash map reports empty") (assert-equal test @@ -82,8 +79,7 @@ (let [m0 (IntIntMap.empty) m1 (IntIntMap.insert 9 1 &m0) k 9] - (and (not (IntIntMap.contains? &k &m0)) - (IntIntMap.contains? &k &m1))) + (and (not (IntIntMap.contains? &k &m0)) (IntIntMap.contains? &k &m1))) "persistence keeps prior map unchanged") (assert-equal test @@ -115,6 +111,75 @@ (Array.length &ks)) "keys returns all keys") + (assert-equal test + true + (let [m0 (IntIntMap.empty) + m1 (IntIntMap.insert 1 10 &m0) + m2 (IntIntMap.insert 2 20 &m1) + m3 (IntIntMap.insert 3 30 &m2) + mapped (IntIntMap.map-values &(fn [v] (+ v 100)) &m3) + k1 1 + k2 2 + k3 3] + (and (= (IntIntMap.length &mapped) 3l) + (match (IntIntMap.get &k1 &mapped) + (Maybe.Just v) (= v 110) + (Maybe.Nothing) false) + (match (IntIntMap.get &k2 &mapped) + (Maybe.Just v) (= v 120) + (Maybe.Nothing) false) + (match (IntIntMap.get &k3 &mapped) + (Maybe.Just v) (= v 130) + (Maybe.Nothing) false))) + "map-values transforms values keeping keys") + + (assert-equal test + true + (let [m (IntIntMap.empty) + mapped (IntIntMap.map-values &(fn [v] (+ v 1)) &m)] + (IntIntMap.empty? &mapped)) + "map-values over empty map yields empty") + + (assert-equal test + true + (let [m0 (IntIntMap.empty) + m1 (IntIntMap.insert 1 10 &m0) + m2 (IntIntMap.insert 2 5 &m1) + m3 (IntIntMap.insert 3 30 &m2) + filtered (IntIntMap.filter &(fn [p] (> @(Pair.b p) 8)) &m3) + k1 1 + k2 2 + k3 3] + (and (= (IntIntMap.length &filtered) 2l) + (IntIntMap.contains? &k1 &filtered) + (not (IntIntMap.contains? &k2 &filtered)) + (IntIntMap.contains? &k3 &filtered))) + "filter keeps entries matching predicate") + + (assert-equal test + true + (let [m0 (IntIntMap.empty) + m1 (IntIntMap.insert 1 10 &m0) + filtered (IntIntMap.filter &(fn [p] false) &m1)] + (IntIntMap.empty? &filtered)) + "filter with always-false yields empty map") + + (assert-equal test + true + (let [m0 (IntIntMap.empty) + m1 (IntIntMap.insert 1 10 &m0) + m2 (IntIntMap.insert 2 20 &m1) + filtered (IntIntMap.filter &(fn [p] true) &m2) + k1 1 + k2 2] + (and (= (IntIntMap.length &filtered) 2l) + (IntIntMap.contains? &k1 &filtered) + (IntIntMap.contains? &k2 &filtered))) + "filter with always-true preserves all entries") + + ; NOTE: IntIntMap.= crashes on ARM64 due to a pre-existing Carp compiler + ; bug with nested closure captures in hash-map reduce. The test is + ; retained for platforms where it works. (assert-equal test true (let [a (IntIntMap.insert 2 20 &(IntIntMap.insert 1 10 &(IntIntMap.empty))) @@ -123,5 +188,7 @@ (and (= &a &b) (not (= &a &c)))) "= is structural regardless of insertion order") - (assert-memory-balance test map-branch-lifecycle 0l "persistent hash map branch lifecycle is memory-balanced") -) + (assert-memory-balance test + map-branch-lifecycle + 0l + "persistent hash map branch lifecycle is memory-balanced")) diff --git a/test/persistent_list.carp b/test/persistent_list.carp index 3c96e14..c331b60 100644 --- a/test/persistent_list.carp +++ b/test/persistent_list.carp @@ -14,30 +14,23 @@ (assert-equal state expected-balance (Debug.memory-balance) descr))) (defn build-long-list [n] - (let-do [l (IntList.empty)] - (for [i 0 n] - (set! l (IntList.prepend i &l))) - l)) + (let-do [l (IntList.empty)] (for [i 0 n] (set! l (IntList.prepend i &l))) l)) (defn list-branch-lifecycle [] (let [base (IntList.singleton 1) left (IntList.prepend 2 &base) right (IntList.prepend 3 &base)] - (do - (ignore left) - (ignore right)))) + (do (ignore left) (ignore right)))) (deftest test (assert-equal test true - (let [l (IntList.empty)] - (IntList.empty? &l)) + (let [l (IntList.empty)] (IntList.empty? &l)) "empty list reports empty") (assert-equal test 0l - (let [l (IntList.empty)] - (IntList.length &l)) + (let [l (IntList.empty)] (IntList.length &l)) "empty list has length 0") (assert-equal test @@ -53,9 +46,7 @@ (let [l0 (IntList.empty) l1 (IntList.prepend 1 &l0) l2 (IntList.prepend 2 &l1)] - (match (IntList.head &l2) - (Maybe.Just v) v - (Maybe.Nothing) -1)) + (match (IntList.head &l2) (Maybe.Just v) v (Maybe.Nothing) -1)) "head reads first value") (assert-equal test @@ -65,9 +56,7 @@ l2 (IntList.prepend 2 &l1)] (match (IntList.tail &l2) (Maybe.Just t) - (match (IntList.head &t) - (Maybe.Just v) v - (Maybe.Nothing) -1) + (match (IntList.head &t) (Maybe.Just v) v (Maybe.Nothing) -1) (Maybe.Nothing) -1)) "tail exposes next value") @@ -135,10 +124,89 @@ c0 (IntList.empty) c1 (IntList.prepend 9 &c0) c2 (IntList.prepend 1 &c1)] - (and (= &a2 &b2) - (not (= &a2 &c2)) - (not (= &a2 &a1)))) + (and (= &a2 &b2) (not (= &a2 &c2)) (not (= &a2 &a1)))) "= is structural: equal shape + elements") - (assert-memory-balance test list-branch-lifecycle 0l "persistent branch lifecycle is memory-balanced") -) + (assert-equal test + true + (let [l (IntList.empty) + r (IntList.reverse &l)] (IntList.empty? &r)) + "reverse of empty list is empty") + + (assert-equal test + true + (let [l (IntList.singleton 42) + r (IntList.reverse &l)] (= &r &l)) + "reverse of singleton is itself") + + (assert-equal test + true + (let [l0 (IntList.empty) + l1 (IntList.prepend 1 &l0) + l2 (IntList.prepend 2 &l1) + l3 (IntList.prepend 3 &l2) + rev (IntList.reverse &l3) + arr (IntList.to-array &rev)] + (and (= (Array.length &arr) 3) + (= @(Array.unsafe-nth &arr 0) 1) + (= @(Array.unsafe-nth &arr 1) 2) + (= @(Array.unsafe-nth &arr 2) 3))) + "reverse reverses element order") + + (assert-equal test + true + (let [l0 (IntList.empty) + l1 (IntList.prepend 1 &l0) + l2 (IntList.prepend 2 &l1) + l3 (IntList.prepend 3 &l2) + mapped (IntList.map &(fn [x] (+ x 10)) &l3) + arr (IntList.to-array &mapped)] + (and (= (Array.length &arr) 3) + (= @(Array.unsafe-nth &arr 0) 13) + (= @(Array.unsafe-nth &arr 1) 12) + (= @(Array.unsafe-nth &arr 2) 11))) + "map applies function preserving order") + + (assert-equal test + true + (let [l (IntList.empty) + m (IntList.map &(fn [x] (+ x 1)) &l)] + (IntList.empty? &m)) + "map over empty list yields empty") + + (assert-equal test + true + (let [l0 (IntList.empty) + l1 (IntList.prepend 1 &l0) + l2 (IntList.prepend 2 &l1) + l3 (IntList.prepend 3 &l2) + l4 (IntList.prepend 4 &l3) + filtered (IntList.filter &(fn [x] (> @x 2)) &l4) + arr (IntList.to-array &filtered)] + (and (= (Array.length &arr) 2) + (= @(Array.unsafe-nth &arr 0) 4) + (= @(Array.unsafe-nth &arr 1) 3))) + "filter keeps elements matching predicate in order") + + (assert-equal test + true + (let [l0 (IntList.empty) + l1 (IntList.prepend 1 &l0) + l2 (IntList.prepend 2 &l1) + filtered (IntList.filter &(fn [x] false) &l2)] + (IntList.empty? &filtered)) + "filter with always-false yields empty") + + (assert-equal test + true + (let [l0 (IntList.empty) + l1 (IntList.prepend 1 &l0) + l2 (IntList.prepend 2 &l1) + filtered (IntList.filter &(fn [x] true) &l2)] + (= &filtered &l2)) + "filter with always-true preserves all elements") + + (assert-memory-balance test + list-branch-lifecycle + 0l + "persistent branch lifecycle is memory-balanced")) diff --git a/test/persistent_ord_map.carp b/test/persistent_ord_map.carp index 91e8c9d..888ec86 100644 --- a/test/persistent_ord_map.carp +++ b/test/persistent_ord_map.carp @@ -10,8 +10,7 @@ (defn build-ascending [n] (let-do [m (IntIntOMap.empty)] - (for [i 0 n] - (set! m (IntIntOMap.insert i i &m))) + (for [i 0 n] (set! m (IntIntOMap.insert i i &m))) m)) (defn assert-memory-balance [state f expected-balance descr] @@ -27,15 +26,12 @@ m3 (IntStrMap.insert 3 @"c" &m1) m4 (IntStrMap.remove 2 &m2) m5 (IntStrMap.remove 1 &m4)] - (do - (ignore m3) - (ignore m5)))) + (do (ignore m3) (ignore m5)))) (deftest test (assert-equal test true - (let [m (IntStrMap.empty)] - (IntStrMap.empty? &m)) + (let [m (IntStrMap.empty)] (IntStrMap.empty? &m)) "empty ordered map reports empty") (assert-equal test @@ -87,20 +83,20 @@ m1 (IntStrMap.insert 2 @"two" &m0) m2 (IntStrMap.insert 1 @"one" &m1) m3 (IntStrMap.insert 3 @"three" &m2)] - (and (match (IntStrMap.min-key &m3) - (Maybe.Just v) (= v 1) - (Maybe.Nothing) false) - (match (IntStrMap.max-key &m3) - (Maybe.Just v) (= v 3) - (Maybe.Nothing) false))) + (and + (match (IntStrMap.min-key &m3) + (Maybe.Just v) (= v 1) + (Maybe.Nothing) false) + (match (IntStrMap.max-key &m3) + (Maybe.Just v) (= v 3) + (Maybe.Nothing) false))) "min-key and max-key follow ordering") (assert-equal test true (let [m0 (IntStrMap.empty) m1 (IntStrMap.insert 1 @"one" &m0)] - (and (Maybe.nothing? &(IntStrMap.get 1 &m0)) - (IntStrMap.contains? 1 &m1))) + (and (Maybe.nothing? &(IntStrMap.get 1 &m0)) (IntStrMap.contains? 1 &m1))) "persistence keeps prior map unchanged") (assert-equal test @@ -140,11 +136,75 @@ (assert-equal test true - (let [a (IntStrMap.insert 2 @"b" &(IntStrMap.insert 1 @"a" &(IntStrMap.empty))) - b (IntStrMap.insert 1 @"a" &(IntStrMap.insert 2 @"b" &(IntStrMap.empty))) - c (IntStrMap.insert 1 @"a" &(IntStrMap.insert 2 @"c" &(IntStrMap.empty)))] + (let [a (IntStrMap.insert 2 + @"b" + &(IntStrMap.insert 1 @"a" &(IntStrMap.empty))) + b (IntStrMap.insert 1 + @"a" + &(IntStrMap.insert 2 @"b" &(IntStrMap.empty))) + c (IntStrMap.insert 1 + @"a" + &(IntStrMap.insert 2 @"c" &(IntStrMap.empty)))] (and (= &a &b) (not (= &a &c)))) "= is structural regardless of insertion order") - (assert-memory-balance test map-branch-lifecycle 0l "persistent ordered map branch lifecycle is memory-balanced") -) + (assert-equal test + true + (let [m0 (IntIntOMap.empty) + m1 (IntIntOMap.insert 1 10 &m0) + m2 (IntIntOMap.insert 2 20 &m1) + m3 (IntIntOMap.insert 3 30 &m2) + mapped (IntIntOMap.map-values &(fn [v] (+ v 100)) &m3)] + (and (= (IntIntOMap.length &mapped) 3l) + (match (IntIntOMap.get 1 &mapped) + (Maybe.Just v) (= v 110) + (Maybe.Nothing) false) + (match (IntIntOMap.get 2 &mapped) + (Maybe.Just v) (= v 120) + (Maybe.Nothing) false) + (match (IntIntOMap.get 3 &mapped) + (Maybe.Just v) (= v 130) + (Maybe.Nothing) false))) + "map-values transforms values keeping keys") + + (assert-equal test + true + (let [m (IntIntOMap.empty) + mapped (IntIntOMap.map-values &(fn [v] (+ v 1)) &m)] + (IntIntOMap.empty? &mapped)) + "map-values over empty map yields empty") + + (assert-equal test + true + (let [m0 (IntIntOMap.empty) + m1 (IntIntOMap.insert 1 10 &m0) + m2 (IntIntOMap.insert 2 5 &m1) + m3 (IntIntOMap.insert 3 30 &m2) + filtered (IntIntOMap.filter &(fn [p] (> @(Pair.b p) 8)) &m3)] + (and (= (IntIntOMap.length &filtered) 2l) + (IntIntOMap.contains? 1 &filtered) + (not (IntIntOMap.contains? 2 &filtered)) + (IntIntOMap.contains? 3 &filtered))) + "filter keeps entries matching predicate") + + (assert-equal test + true + (let [m0 (IntIntOMap.empty) + m1 (IntIntOMap.insert 1 10 &m0) + filtered (IntIntOMap.filter &(fn [p] false) &m1)] + (IntIntOMap.empty? &filtered)) + "filter with always-false yields empty map") + + (assert-equal test + true + (let [m0 (IntIntOMap.empty) + m1 (IntIntOMap.insert 1 10 &m0) + m2 (IntIntOMap.insert 2 20 &m1) + filtered (IntIntOMap.filter &(fn [p] true) &m2)] + (= &filtered &m2)) + "filter with always-true preserves all entries") + + (assert-memory-balance test + map-branch-lifecycle + 0l + "persistent ordered map branch lifecycle is memory-balanced")) diff --git a/test/persistent_queue.carp b/test/persistent_queue.carp index ce9ddbd..fa658c0 100644 --- a/test/persistent_queue.carp +++ b/test/persistent_queue.carp @@ -14,23 +14,16 @@ (assert-equal state expected-balance (Debug.memory-balance) descr))) (defn build-long-queue [n] - (let-do [q (IntQueue.empty)] - (for [i 0 n] - (set! q (IntQueue.enqueue i &q))) - q)) + (let-do [q (IntQueue.empty)] (for [i 0 n] (set! q (IntQueue.enqueue i &q))) q)) (defn queue-branch-lifecycle [] (let [q0 (IntQueue.empty) q1 (IntQueue.enqueue 1 &q0) q2 (IntQueue.enqueue 2 &q1) q3 (IntQueue.enqueue 3 &q1)] - (do - (ignore q2) - (ignore q3)))) + (do (ignore q2) (ignore q3)))) -(defn queue-drain-values [q-ref] - (let [out0 []] - (queue-drain-iter q-ref &out0))) +(defn queue-drain-values [q-ref] (let [out0 []] (queue-drain-iter q-ref &out0))) (defn queue-drain-iter [q-ref out-ref] (match (IntQueue.dequeue q-ref) @@ -38,33 +31,27 @@ (Maybe.Just p) (let [value @(Pair.a &p) next-q @(Pair.b &p)] - (do - (Array.push-back! out-ref value) - (queue-drain-iter &next-q out-ref))))) + (do (Array.push-back! out-ref value) (queue-drain-iter &next-q out-ref))))) (deftest test (assert-equal test true - (let [q (IntQueue.empty)] - (IntQueue.empty? &q)) + (let [q (IntQueue.empty)] (IntQueue.empty? &q)) "empty queue reports empty") (assert-equal test 0l - (let [q (IntQueue.empty)] - (IntQueue.length &q)) + (let [q (IntQueue.empty)] (IntQueue.length &q)) "empty queue has length 0") (assert-equal test true - (let [q (IntQueue.empty)] - (Maybe.nothing? &(IntQueue.peek &q))) + (let [q (IntQueue.empty)] (Maybe.nothing? &(IntQueue.peek &q))) "peek on empty queue returns Nothing") (assert-equal test true - (let [q (IntQueue.empty)] - (Maybe.nothing? &(IntQueue.dequeue &q))) + (let [q (IntQueue.empty)] (Maybe.nothing? &(IntQueue.dequeue &q))) "dequeue on empty queue returns Nothing") (assert-equal test @@ -73,9 +60,7 @@ q1 (IntQueue.enqueue 1 &q0) q2 (IntQueue.enqueue 2 &q1) q3 (IntQueue.enqueue 3 &q2)] - (match (IntQueue.peek &q3) - (Maybe.Just v) v - (Maybe.Nothing) -1)) + (match (IntQueue.peek &q3) (Maybe.Just v) v (Maybe.Nothing) -1)) "peek returns FIFO front") (assert-equal test @@ -125,8 +110,7 @@ (Maybe.Just p) (let [v @(Pair.a &p) q2 @(Pair.b &p)] - (and (= v 0) - (= (IntQueue.length &q2) 99999l))))) + (and (= v 0) (= (IntQueue.length &q2) 99999l))))) "first dequeue on a 100k-rear queue reverses without blowing the C stack") (assert-equal test @@ -155,11 +139,67 @@ (let [a0 (IntQueue.empty) a1 (IntQueue.enqueue 1 &a0) a2 (IntQueue.enqueue 2 &a1) - b (match (IntQueue.dequeue &(IntQueue.enqueue 2 &(IntQueue.enqueue 1 &a0))) - (Maybe.Just p) (IntQueue.enqueue 2 &(IntQueue.enqueue 1 &a0)) - (Maybe.Nothing) a0)] + b (match (IntQueue.dequeue + &(IntQueue.enqueue 2 &(IntQueue.enqueue 1 &a0))) + (Maybe.Just p) (IntQueue.enqueue 2 &(IntQueue.enqueue 1 &a0)) + (Maybe.Nothing) a0)] (= &a2 &b)) "= is structural across internal front/rear splits") - (assert-memory-balance test queue-branch-lifecycle 0l "persistent queue branch lifecycle is memory-balanced") -) + (assert-equal test + true + (let [q0 (IntQueue.empty) + q1 (IntQueue.enqueue 1 &q0) + q2 (IntQueue.enqueue 2 &q1) + q3 (IntQueue.enqueue 3 &q2) + mapped (IntQueue.map &(fn [x] (+ x 10)) &q3) + arr (IntQueue.to-array &mapped)] + (and (= (Array.length &arr) 3) + (= @(Array.unsafe-nth &arr 0) 11) + (= @(Array.unsafe-nth &arr 1) 12) + (= @(Array.unsafe-nth &arr 2) 13))) + "map applies function preserving FIFO order") + + (assert-equal test + true + (let [q (IntQueue.empty) + m (IntQueue.map &(fn [x] (+ x 1)) &q)] + (IntQueue.empty? &m)) + "map over empty queue yields empty") + + (assert-equal test + true + (let [q0 (IntQueue.empty) + q1 (IntQueue.enqueue 1 &q0) + q2 (IntQueue.enqueue 2 &q1) + q3 (IntQueue.enqueue 3 &q2) + q4 (IntQueue.enqueue 4 &q3) + filtered (IntQueue.filter &(fn [x] (> @x 2)) &q4) + arr (IntQueue.to-array &filtered)] + (and (= (Array.length &arr) 2) + (= @(Array.unsafe-nth &arr 0) 3) + (= @(Array.unsafe-nth &arr 1) 4))) + "filter keeps elements matching predicate in FIFO order") + + (assert-equal test + true + (let [q0 (IntQueue.empty) + q1 (IntQueue.enqueue 1 &q0) + q2 (IntQueue.enqueue 2 &q1) + filtered (IntQueue.filter &(fn [x] false) &q2)] + (IntQueue.empty? &filtered)) + "filter with always-false yields empty queue") + + (assert-equal test + true + (let [q0 (IntQueue.empty) + q1 (IntQueue.enqueue 1 &q0) + q2 (IntQueue.enqueue 2 &q1) + filtered (IntQueue.filter &(fn [x] true) &q2)] + (= &filtered &q2)) + "filter with always-true preserves all elements") + + (assert-memory-balance test + queue-branch-lifecycle + 0l + "persistent queue branch lifecycle is memory-balanced")) diff --git a/test/persistent_trie.carp b/test/persistent_trie.carp index a372f60..5557ea8 100644 --- a/test/persistent_trie.carp +++ b/test/persistent_trie.carp @@ -16,8 +16,7 @@ (defn build-long-key [n] (let-do [k (the (Array Int) [])] - (for [i 0 n] - (set! k (Array.push-back k i))) + (for [i 0 n] (set! k (Array.push-back k i))) k)) (defn trie-branch-lifecycle [] @@ -35,21 +34,18 @@ (deftest test (assert-equal test true - (let [t (IntTrie.empty)] - (IntTrie.empty? &t)) + (let [t (IntTrie.empty)] (IntTrie.empty? &t)) "empty trie reports empty") (assert-equal test 0l - (let [t (IntTrie.empty)] - (IntTrie.length &t)) + (let [t (IntTrie.empty)] (IntTrie.length &t)) "empty trie has size 0") (assert-equal test true (let [t (IntTrie.empty) - k [1 2 3]] - (Maybe.nothing? &(IntTrie.get &k &t))) + k [1 2 3]] (Maybe.nothing? &(IntTrie.get &k &t))) "get on empty trie returns Nothing") (assert-equal test @@ -67,8 +63,7 @@ (let [t0 (IntTrie.empty) k [1 2 3] t1 (IntTrie.insert &k @"alpha" &t0)] - (and (Maybe.nothing? &(IntTrie.get &k &t0)) - (IntTrie.contains? &k &t1))) + (and (Maybe.nothing? &(IntTrie.get &k &t0)) (IntTrie.contains? &k &t1))) "persistence keeps previous version unchanged") (assert-equal test @@ -129,10 +124,11 @@ t0 (IntTrie.empty) t1 (IntTrie.insert &empty-key @"root" &t0) t2 (IntTrie.remove &empty-key &t1)] - (and (match (IntTrie.get &empty-key &t1) - (Maybe.Just v) (= v @"root") - (Maybe.Nothing) false) - (= (IntTrie.length &t2) 0l))) + (and + (match (IntTrie.get &empty-key &t1) + (Maybe.Just v) (= v @"root") + (Maybe.Nothing) false) + (= (IntTrie.length &t2) 0l))) "empty key is supported at root") (assert-equal test @@ -163,8 +159,7 @@ k1 [1 2] t1 (IntTrie.insert &k1 @"a" &t0) vs (IntTrie.values &t1)] - (and (= (Array.length &vs) 1) - (= @(Array.unsafe-nth &vs 0) @"a"))) + (and (= (Array.length &vs) 1) (= @(Array.unsafe-nth &vs 0) @"a"))) "values collects into Array") (assert-equal test @@ -173,9 +168,62 @@ k2 [1 2 3] a (IntTrie.insert &k2 @"b" &(IntTrie.insert &k1 @"a" &(IntTrie.empty))) b (IntTrie.insert &k1 @"a" &(IntTrie.insert &k2 @"b" &(IntTrie.empty))) - c (IntTrie.insert &k2 @"DIFF" &(IntTrie.insert &k1 @"a" &(IntTrie.empty)))] + c (IntTrie.insert &k2 + @"DIFF" + &(IntTrie.insert &k1 @"a" &(IntTrie.empty)))] (and (= &a &b) (not (= &a &c)))) "= is structural across insertion orders") - (assert-memory-balance test trie-branch-lifecycle 0l "persistent trie branch lifecycle is memory-balanced") -) + (assert-equal test + true + (let [k1 [1 2] + k2 [3 4] + t0 (IntTrieInt.empty) + t1 (IntTrieInt.insert &k1 10 &t0) + t2 (IntTrieInt.insert &k2 20 &t1) + mapped (IntTrieInt.map-values &(fn [v] (+ v 100)) &t2)] + (and (= (IntTrieInt.length &mapped) 2l) + (match (IntTrieInt.get &k1 &mapped) + (Maybe.Just v) (= v 110) + (Maybe.Nothing) false) + (match (IntTrieInt.get &k2 &mapped) + (Maybe.Just v) (= v 120) + (Maybe.Nothing) false))) + "map-values transforms values keeping keys") + + (assert-equal test + true + (let [t (IntTrieInt.empty) + mapped (IntTrieInt.map-values &(fn [v] (+ v 1)) &t)] + (IntTrieInt.empty? &mapped)) + "map-values over empty trie yields empty") + + (assert-equal test + true + (let [k1 [1 2] + k2 [3 4] + k3 [5 6] + t0 (IntTrieInt.empty) + t1 (IntTrieInt.insert &k1 10 &t0) + t2 (IntTrieInt.insert &k2 20 &t1) + t3 (IntTrieInt.insert &k3 5 &t2) + filtered (IntTrieInt.filter &(fn [p] (> @(Pair.b p) 8)) &t3)] + (and (= (IntTrieInt.length &filtered) 2l) + (IntTrieInt.contains? &k1 &filtered) + (IntTrieInt.contains? &k2 &filtered) + (not (IntTrieInt.contains? &k3 &filtered)))) + "filter keeps entries matching predicate") + + (assert-equal test + true + (let [k1 [1] + t0 (IntTrieInt.empty) + t1 (IntTrieInt.insert &k1 10 &t0) + filtered (IntTrieInt.filter &(fn [p] false) &t1)] + (IntTrieInt.empty? &filtered)) + "filter with always-false yields empty trie") + + (assert-memory-balance test + trie-branch-lifecycle + 0l + "persistent trie branch lifecycle is memory-balanced"))