gossip_kv/lattices/
mod.rs

1use std::cmp::Ordering;
2use std::collections::HashSet;
3use std::hash::Hash;
4
5use dfir_rs::lattices::{IsBot, IsTop, LatticeFrom, LatticeOrd, Merge};
6use serde::{Deserialize, Serialize};
7
8/// A bounded set union lattice with a fixed size N.
9///
10/// Once the set reaches size N, it becomes top. The items in the set are no longer tracked to
11/// reclaim associated memory.
12#[derive(Debug, Clone, Eq, Serialize, Deserialize)]
13
14pub struct BoundedSetLattice<T, const N: usize>
15where
16    T: Eq + Hash,
17{
18    // The set of items in the lattice with invariant:
19    // is_top => items.is_empty() ... i.e. the items are dropped when the lattice reaches top.
20    items: HashSet<T>,
21    is_top: bool,
22}
23
24impl<T, const N: usize> LatticeFrom<BoundedSetLattice<T, N>> for BoundedSetLattice<T, N>
25where
26    T: Eq + Hash,
27{
28    fn lattice_from(other: BoundedSetLattice<T, N>) -> Self {
29        other
30    }
31}
32
33impl<T, const N: usize> Default for BoundedSetLattice<T, N>
34where
35    T: Eq + Hash,
36{
37    fn default() -> Self {
38        Self {
39            items: HashSet::new(),
40            is_top: N == 0, // This lattice is effectively a unit lattice `()`, if N == 0
41        }
42    }
43}
44
45impl<T> From<()> for BoundedSetLattice<T, 0>
46where
47    T: Eq + Hash,
48{
49    fn from(_: ()) -> Self {
50        Default::default()
51    }
52}
53
54impl<T> From<BoundedSetLattice<T, 0>> for ()
55where
56    T: Eq + Hash,
57{
58    fn from(_: BoundedSetLattice<T, 0>) -> Self {}
59}
60
61impl<T, const N: usize> BoundedSetLattice<T, N>
62where
63    T: Eq + Hash,
64{
65    pub fn new() -> Self {
66        Default::default()
67    }
68
69    pub fn new_from<U>(items: U) -> Self
70    where
71        U: IntoIterator<Item = T>,
72    {
73        let mut lattice = Self::new();
74        lattice.merge(items);
75        lattice
76    }
77}
78
79impl<T, const N: usize> IsBot for BoundedSetLattice<T, N>
80where
81    T: Eq + Hash,
82{
83    fn is_bot(&self) -> bool {
84        match N {
85            0 => true,
86            _ => self.items.is_empty() && !self.is_top,
87        }
88    }
89}
90
91impl<T, const N: usize> IsTop for BoundedSetLattice<T, N>
92where
93    T: Eq + Hash,
94{
95    fn is_top(&self) -> bool {
96        self.is_top
97    }
98}
99
100impl<T, const N: usize, U> Merge<U> for BoundedSetLattice<T, N>
101where
102    U: IntoIterator<Item = T>,
103    T: Eq + Hash,
104{
105    fn merge(&mut self, other: U) -> bool {
106        if self.is_top {
107            return false;
108        }
109
110        let old_len = self.items.len();
111        self.items.extend(other);
112        let new_len = self.items.len();
113
114        if new_len >= N {
115            self.is_top = true;
116            self.items.clear();
117        }
118
119        new_len != old_len
120    }
121}
122
123impl<T, const N: usize> PartialOrd<Self> for BoundedSetLattice<T, N>
124where
125    T: Eq + Hash,
126{
127    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
128        match (self.is_top, other.is_top) {
129            (true, true) => Some(Ordering::Equal),
130            (true, false) => Some(Ordering::Greater),
131            (false, true) => Some(Ordering::Less),
132            (false, false) => match self.items.len().cmp(&other.items.len()) {
133                Ordering::Greater => {
134                    if other.items.iter().all(|key| self.items.contains(key)) {
135                        Some(Ordering::Greater)
136                    } else {
137                        None
138                    }
139                }
140                Ordering::Less => {
141                    if self.items.iter().all(|key| other.items.contains(key)) {
142                        Some(Ordering::Less)
143                    } else {
144                        None
145                    }
146                }
147                Ordering::Equal => {
148                    if self.items.iter().all(|key| other.items.contains(key)) {
149                        Some(Ordering::Equal)
150                    } else {
151                        None
152                    }
153                }
154            },
155        }
156    }
157}
158
159impl<T, const N: usize> PartialEq<Self> for BoundedSetLattice<T, N>
160where
161    T: Eq + Hash,
162{
163    fn eq(&self, other: &Self) -> bool {
164        match (self.is_top, other.is_top) {
165            (true, true) => true,
166            (true, false) => false,
167            (false, true) => false,
168            (false, false) => self.items == other.items,
169        }
170    }
171}
172
173impl<T, const N: usize> LatticeOrd for BoundedSetLattice<T, N> where T: Eq + Hash {}
174
175impl<T, const N: usize> Merge<BoundedSetLattice<T, N>> for BoundedSetLattice<T, N>
176where
177    T: Eq + Hash,
178{
179    fn merge(&mut self, other: BoundedSetLattice<T, N>) -> bool {
180        match (self.is_top, other.is_top) {
181            (true, _) => false,
182            (false, true) => {
183                self.is_top = true;
184                self.items.clear();
185                true
186            }
187            (false, false) => self.merge(other.items),
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use dfir_rs::lattices::test::check_all;
195
196    use super::*;
197
198    #[test]
199    fn test_0_bounded_set_lattice() {
200        let mut lat: BoundedSetLattice<i32, 0> = ().into();
201        assert!(lat.is_bot() && lat.is_top());
202
203        // Merges should always return false.
204        assert!(!lat.merge([1]));
205
206        // No changes to top/bot status.
207        assert!(lat.is_bot() && lat.is_top());
208    }
209
210    #[test]
211    fn test_1_bounded_set_lattice() {
212        // The bounded lattice with N = 1 is effectively a WithBottom<T> lattice.
213        let mut lat = BoundedSetLattice::<i32, 1>::new();
214        assert!(lat.is_bot() && !lat.is_top());
215        assert!(lat.items.is_empty());
216
217        assert!(lat.merge([1]));
218        assert!(!lat.is_bot() && lat.is_top());
219        assert!(lat.items.is_empty()); // Check that the items were dropped.
220
221        assert!(!lat.merge([2]));
222    }
223
224    #[test]
225    fn test_2_bounded_set_lattice() {
226        let mut a = BoundedSetLattice::<i32, 2>::new();
227        let b: BoundedSetLattice<i32, 2> = BoundedSetLattice::new_from([1, 2]);
228
229        assert!(a.is_bot() && !a.is_top());
230        assert!(!b.is_bot() && b.is_top());
231
232        assert!(a.merge(b));
233        assert!(!a.is_bot() && a.is_top());
234
235        assert!(!a.merge([3]));
236    }
237
238    #[test]
239    fn test_lattice_properties() {
240        check_all(&[
241            Default::default(),
242            BoundedSetLattice::<i32, 3>::new_from([1]),
243            BoundedSetLattice::<i32, 3>::new_from([1, 2]),
244            BoundedSetLattice::<i32, 3>::new_from([1, 2, 3]),
245            BoundedSetLattice::<i32, 3>::new_from([1, 2, 3, 4]),
246        ]);
247    }
248}