lattices/
test.rs

1//! Helper test utils to test lattice implementation correctness.
2
3use std::fmt::Debug;
4
5use crate::{
6    Atomize, IsBot, IsTop, Lattice, LatticeBimorphism, LatticeMorphism, LatticeOrd, Merge,
7    NaiveLatticeOrd,
8};
9
10/// Helper which calls many other `check_*` functions in this module. See source code for which
11/// functions are called.
12pub fn check_all<T: Lattice + Clone + PartialEq + Debug + Default>(items: &[T]) {
13    check_lattice_ord(items);
14    check_partial_ord_properties(items);
15    check_lattice_properties(items);
16    check_lattice_is_bot(items);
17    check_lattice_is_top(items);
18    check_lattice_default_is_bot::<T>();
19}
20
21/// Check that the lattice's `PartialOrd` implementation agrees with the `NaiveLatticeOrd` partial
22/// order derived from `Merge`.
23pub fn check_lattice_ord<T: LatticeOrd + NaiveLatticeOrd + Debug>(items: &[T]) {
24    // `NaiveLatticeOrd` is a better source of truth, as it is based on the `Merge` impl. But it
25    // is inefficient. It also could be wrong if `Merge` doesn't properly return true/false
26    // iff the merge changed things.
27    for [a, b] in cartesian_power(items) {
28        assert_eq!(a.naive_cmp(b), a.partial_cmp(b), "`{:?}`, `{:?}`", a, b);
29    }
30}
31
32/// Checks `PartialOrd` and `PartialEq`'s reflexivity, symmetry, transitivity, and duality.
33#[expect(
34    clippy::eq_op,
35    clippy::double_comparisons,
36    reason = "testing comparison properties"
37)]
38pub fn check_partial_ord_properties<T: PartialOrd + PartialEq + Debug>(items: &[T]) {
39    use std::cmp::Ordering::*;
40
41    // PartialEq:
42    // reflexive: a == a;
43    for a in items {
44        assert!(a == a, "Reflexivity: `{:?}` should equal itself.", a);
45    }
46    // symmetric: a == b implies b == a; and
47    for [a, b] in cartesian_power(items) {
48        assert_eq!(a == b, b == a, "`{:?}`, `{:?}`", a, b);
49    }
50    // transitive: a == b and b == c implies a == c.
51    for [a, b, c] in cartesian_power(items) {
52        if a == b && b == c {
53            assert_eq!(a == b && b == c, a == c, "`{:?}`, `{:?}`, `{:?}`", a, b, c);
54        }
55    }
56
57    // PartialOrd
58    for [a, b] in cartesian_power(items) {
59        // a == b if and only if partial_cmp(a, b) == Some(Equal).
60        assert_eq!(
61            a == b,
62            a.partial_cmp(b) == Some(Equal),
63            "`{:?}`, `{:?}`",
64            a,
65            b,
66        );
67        // a < b if and only if partial_cmp(a, b) == Some(Less)
68        assert_eq!(
69            a < b,
70            a.partial_cmp(b) == Some(Less),
71            "`{:?}`, `{:?}`",
72            a,
73            b,
74        );
75        // a > b if and only if partial_cmp(a, b) == Some(Greater)
76        assert_eq!(
77            a > b,
78            a.partial_cmp(b) == Some(Greater),
79            "`{:?}`, `{:?}`",
80            a,
81            b,
82        );
83        // a <= b if and only if a < b || a == b
84        assert_eq!(a <= b, a < b || a == b, "`{:?}`, `{:?}`", a, b);
85        // a >= b if and only if a > b || a == b
86        assert_eq!(a >= b, a > b || a == b, "`{:?}`, `{:?}`", a, b);
87        // PartialEq:
88        // a != b if and only if !(a == b).
89        // #[expect(clippy::nonminimal_bool, reason = "testing comparison properties")]
90        {
91            assert_eq!(a != b, !(a == b), "`{:?}`, `{:?}`", a, b);
92        }
93    }
94    // transitivity: a < b and b < c implies a < c. The same must hold for both == and >.
95    for [a, b, c] in cartesian_power(items) {
96        if a < b && b < c {
97            assert!(a < c, "`{:?}`, `{:?}`, `{:?}`", a, b, c);
98        }
99        if a == b && b == c {
100            assert!(a == c, "`{:?}`, `{:?}`, `{:?}`", a, b, c);
101        }
102        if a > b && b > c {
103            assert!(a > c, "`{:?}`, `{:?}`, `{:?}`", a, b, c);
104        }
105    }
106    // duality: a < b if and only if b > a.
107    for [a, b] in cartesian_power(items) {
108        assert_eq!(a < b, b > a, "`{:?}`, `{:?}`", a, b);
109    }
110}
111
112/// Check lattice associativity, commutativity, and idempotence.
113pub fn check_lattice_properties<T: Merge<T> + Clone + PartialEq + Debug>(items: &[T]) {
114    // Idempotency
115    // x ∧ x = x
116    for x in items {
117        assert_eq!(
118            Merge::merge_owned(x.clone(), x.clone()),
119            x.clone(),
120            "`{:?}`",
121            x,
122        );
123    }
124
125    // Commutativity
126    // x ∧ y = y ∧ x
127    for [x, y] in cartesian_power(items) {
128        assert_eq!(
129            Merge::merge_owned(x.clone(), y.clone()),
130            Merge::merge_owned(y.clone(), x.clone()),
131            "`{:?}`, `{:?}`",
132            x,
133            y,
134        );
135    }
136
137    // Associativity
138    // x ∧ (y ∧ z) = (x ∧ y) ∧ z
139    for [x, y, z] in cartesian_power(items) {
140        assert_eq!(
141            Merge::merge_owned(x.clone(), Merge::merge_owned(y.clone(), z.clone())),
142            Merge::merge_owned(Merge::merge_owned(x.clone(), y.clone()), z.clone()),
143            "`{:?}`, `{:?}`, `{:?}`",
144            x,
145            y,
146            z,
147        );
148    }
149}
150
151/// Checks that the item which is bot is less than (or equal to) all other items.
152pub fn check_lattice_is_bot<T: IsBot + LatticeOrd + Debug>(items: &[T]) {
153    let Some(bot) = items.iter().find(|&x| IsBot::is_bot(x)) else {
154        return;
155    };
156    for x in items {
157        assert!(bot <= x);
158        assert_eq!(bot == x, x.is_bot(), "{:?}", x);
159    }
160}
161
162/// Checks that the item which is top is greater than (or equal to) all other items.
163pub fn check_lattice_is_top<T: IsTop + LatticeOrd + Debug>(items: &[T]) {
164    let Some(top) = items.iter().find(|&x| IsTop::is_top(x)) else {
165        return;
166    };
167    for x in items {
168        assert!(x <= top);
169        assert_eq!(top == x, x.is_top(), "{:?}", x);
170    }
171}
172
173/// Asserts that [`IsBot`] is true for [`Default::default()`].
174pub fn check_lattice_default_is_bot<T: IsBot + Default>() {
175    assert!(T::is_bot(&T::default()));
176}
177
178/// Check that the atomized lattice points re-merge to form the same original lattice point, for each item in `items`.
179pub fn check_atomize_each<
180    T: Atomize + Merge<T::Atom> + LatticeOrd + IsBot + Default + Clone + Debug,
181>(
182    items: &[T],
183) where
184    T::Atom: Debug,
185{
186    for item in items {
187        let mut reformed = T::default();
188        let mut atoms = item.clone().atomize().peekable();
189        assert_eq!(
190            atoms.peek().is_none(),
191            item.is_bot(),
192            "`{:?}` atomize should return empty iterator ({}) if and only if item is bot ({}).",
193            item,
194            atoms.peek().is_none(),
195            item.is_bot()
196        );
197        for atom in atoms {
198            assert!(
199                !atom.is_bot(),
200                "`{:?}` atomize illegally returned a bottom atom `{:?}`.",
201                item,
202                atom,
203            );
204            reformed.merge(atom);
205        }
206        assert_eq!(item, &reformed, "`{:?}` atomize failed to reform", item);
207    }
208}
209
210/// Checks that the [`LatticeMorphism`] is valid, i.e. that merge distributes over it.
211pub fn check_lattice_morphism<LatIn, Func>(mut func: Func, items: &[LatIn])
212where
213    Func: LatticeMorphism<LatIn>,
214    LatIn: Merge<LatIn> + Clone + PartialEq + Debug,
215    Func::Output: Merge<Func::Output> + Clone + PartialEq + Debug,
216{
217    for [a, b] in cartesian_power(items) {
218        assert_eq!(
219            func.call(Merge::merge_owned(a.clone(), b.clone())),
220            Merge::merge_owned(func.call(a.clone()), func.call(b.clone())),
221            "Func not a morphism: `f(a ⊔ b) != f(a) ⊔ f(b)`
222            \n`a = {:?}`, `b = {:?}`",
223            a,
224            b
225        )
226    }
227}
228
229/// Checks that the [`LatticeBimorphism`] is valid, i.e. that merge distributes over both arguments of it.
230pub fn check_lattice_bimorphism<LatA, LatB, Func>(
231    mut func: Func,
232    items_a: &[LatA],
233    items_b: &[LatB],
234) where
235    Func: LatticeBimorphism<LatA, LatB>,
236    LatA: Merge<LatA> + Clone + PartialEq + Debug,
237    LatB: Merge<LatB> + Clone + PartialEq + Debug,
238    Func::Output: Merge<Func::Output> + Clone + PartialEq + Debug,
239{
240    // Morphism LHS, fixed RHS:
241    for b in items_b {
242        for [a, da] in cartesian_power(items_a) {
243            assert_eq!(
244                func.call(Merge::merge_owned(a.clone(), da.clone()), b.clone()),
245                Merge::merge_owned(
246                    func.call(a.clone(), b.clone()),
247                    func.call(da.clone(), b.clone())
248                ),
249                "Left arg not a morphism: `f(a ⊔ da, b) != f(a, b) ⊔ f(da, b)`
250                \n`a = {:?}`, `da = {:?}`, `b = {:?}`",
251                a,
252                da,
253                b,
254            );
255        }
256    }
257    // Fixed LHS, morphism RHS:
258    for a in items_a {
259        for [b, db] in cartesian_power(items_b) {
260            assert_eq!(
261                func.call(a.clone(), Merge::merge_owned(b.clone(), db.clone())),
262                Merge::merge_owned(
263                    func.call(a.clone(), b.clone()),
264                    func.call(a.clone(), db.clone())
265                ),
266                "Right arg not a morphism: `f(a, b ⊔ db) != f(a, b) ⊔ f(a, db)`
267                \n`a = {:?}`, `b = {:?}`, `db = {:?}`",
268                a,
269                b,
270                db,
271            );
272        }
273    }
274}
275
276/// Returns an iterator of `N`-length arrays containing all permutations of `items` (with
277/// replacement).
278///
279/// I.e. the `N`th cartesian power of `items`. I.e. the cartesian
280/// product of `items` with itself `N` times.
281pub fn cartesian_power<T, const N: usize>(
282    items: &[T],
283) -> impl ExactSizeIterator<Item = [&T; N]> + Clone {
284    struct CartesianPower<'a, T, const N: usize> {
285        items: &'a [T],
286        iters: [std::iter::Peekable<std::slice::Iter<'a, T>>; N],
287    }
288    impl<'a, T, const N: usize> Iterator for CartesianPower<'a, T, N> {
289        type Item = [&'a T; N];
290
291        fn next(&mut self) -> Option<Self::Item> {
292            if self.items.is_empty() {
293                return None;
294            }
295
296            let mut go_next = true;
297            let out = std::array::from_fn::<_, N, _>(|i| {
298                let iter = &mut self.iters[i];
299                let &item = iter.peek().unwrap();
300                if go_next {
301                    iter.next();
302                    if iter.peek().is_none() {
303                        // "Carry" the `go_next` to the next entry.
304                        *iter = self.items.iter().peekable();
305                    } else {
306                        go_next = false;
307                    }
308                }
309                item
310            });
311            if go_next {
312                // This is the last element, after this return `None`.
313                self.items = &[];
314            };
315            Some(out)
316        }
317
318        fn size_hint(&self) -> (usize, Option<usize>) {
319            if self.items.is_empty() {
320                return (0, Some(0));
321            }
322            let mut pow = 1;
323            let mut passed = 0;
324            for iter in self.iters.iter() {
325                passed += pow * (self.items.len() - iter.len());
326                pow *= self.items.len();
327            }
328            let size = pow - passed;
329            (size, Some(size))
330        }
331    }
332    impl<T, const N: usize> ExactSizeIterator for CartesianPower<'_, T, N> {}
333    impl<T, const N: usize> Clone for CartesianPower<'_, T, N> {
334        fn clone(&self) -> Self {
335            Self {
336                items: self.items,
337                iters: self.iters.clone(),
338            }
339        }
340    }
341    let iters = std::array::from_fn::<_, N, _>(|_| items.iter().peekable());
342    CartesianPower { items, iters }
343}
344
345#[test]
346fn test_cartesian_power() {
347    let items = &[1, 2, 3];
348    let mut iter = cartesian_power(items);
349    let mut len = 27;
350    assert_eq!(len, iter.len());
351    for x in items {
352        for y in items {
353            for z in items {
354                len -= 1;
355                assert_eq!(Some([z, y, x]), iter.next());
356                assert_eq!(len, iter.len());
357            }
358        }
359    }
360}
361
362#[test]
363fn test_cartesian_power_zero() {
364    let mut iter = cartesian_power::<_, 0>(&[0, 1, 2]);
365    assert_eq!(1, iter.len());
366    assert_eq!(Some([]), iter.next());
367    assert_eq!(None, iter.next());
368}
369
370#[test]
371fn test_cartesian_power_empty() {
372    let mut iter = cartesian_power::<_, 4>(&[] as &[usize]);
373    assert_eq!(0, iter.len());
374    assert_eq!(None, iter.next());
375}