Skip to main content

commonware_utils/
handles.rs

1//! An open-addressing multimap from 64-bit hashes to caller-owned handles.
2//!
3//! [Table] never stores keys. Each entry holds a handle, its probe displacement, and a one-byte
4//! fingerprint of its hash, packed into one slot that costs the handle plus four bytes and that a
5//! probe reads with a single access. The caller keeps keys elsewhere, resolving each candidate
6//! handle to its key to confirm a lookup and mapping existing handles back to their hashes when
7//! the table grows. A lookup is therefore a candidate generator that yields every entry inserted
8//! under the requested hash, plus any entry whose different hash shares the same home slot and
9//! fingerprint.
10//!
11//! The low bits of a hash select its home slot and the top byte is its fingerprint, so hashes must
12//! be well distributed in both (use a keyed hasher for adversarial keys).
13//!
14//! ```text
15//! hash  |  fingerprint: top byte  |              ...              | home: low bits |
16//!
17//! slot  |  displacement: 24 bits  | fingerprint: 8 bits |          handle          |
18//!       |<---------------- meta: u32 ------------------>|<---------- H ---------->|
19//! ```
20//!
21//! # Robin Hood Ordering
22//!
23//! Within each run of occupied slots, entries are sorted by home slot. All entries with a given
24//! home are therefore adjacent, each exactly its displacement away from that home, so a lookup
25//! stops as soon as it reaches an empty slot or an entry closer to its own home.
26//!
27//! ```text
28//! slot            4       5       6       7       8       9
29//!              +-------+-------+-------+-------+-------+-------+
30//! home         |   4   |   4   |   5   |   7   |   7   | empty |
31//! displacement |   0   |   1   |   1   |   0   |   1   |       |
32//!              +-------+-------+-------+-------+-------+-------+
33//!
34//! get(hash with home 5) walks from slot 5. Slot 5 holds a home-4 entry at displacement 1,
35//! which is not at displacement 0 and so is not a candidate. Slot 6 holds a home-5 entry at
36//! displacement 1, yielded if its fingerprint matches. Slot 7 holds an entry at displacement 0,
37//! closer to its home than the walk's distance of 2, so the walk stops.
38//!
39//! remove(the entry at slot 5) shifts slot 6 back into slot 5 (displacement 1 -> 0) and stops at
40//! slot 7, whose entry is already at its home.
41//! ```
42//!
43//! Removal uses backward-shift deletion, so the following entries of the run, up to the next
44//! entry already at its home, move back one slot. No tombstones exist, so removals never
45//! lengthen probes or force a rebuild, and the allocation depends only on the number of entries
46//! that have been live at once. The table never shrinks.
47//!
48//! Entries that share a hash occupy one contiguous run, so inserting or removing one of them
49//! costs time linear in the run's length.
50
51use crate::prefetch;
52#[cfg(not(feature = "std"))]
53use alloc::{vec, vec::Vec};
54use core::iter;
55
56/// The bit at which a hash's fingerprint starts.
57const FINGERPRINT_SHIFT: u32 = 56;
58
59/// Metadata of an unoccupied slot.
60///
61/// Its displacement (0xFF_FFFF) exceeds [MAX_DISPLACEMENT], so no occupied slot can equal it.
62const EMPTY: u32 = u32::MAX;
63
64/// Largest displacement an entry may have.
65///
66/// Entries sharing a home slot sit at consecutive displacements, so this also bounds how many
67/// entries may share the low bits of their hashes.
68const MAX_DISPLACEMENT: u32 = (1 << 24) - 2;
69
70/// One unit of displacement in slot metadata.
71const DISPLACEMENT_UNIT: u32 = 1 << 8;
72
73/// Slot count after the first allocation.
74const MIN_SLOTS: usize = 16;
75
76/// Returns the slot metadata of an entry for `hash` at `displacement` from its home.
77#[inline]
78const fn slot_meta(hash: u64, displacement: u32) -> u32 {
79    (displacement << 8) | (hash >> FINGERPRINT_SHIFT) as u32
80}
81
82/// Returns which of `partitions` groups `hash` falls in.
83///
84/// The group comes from the bits just below the fingerprint, which a [Table] of fewer than 2^40
85/// slots reads for neither the home slot nor the fingerprint. Keys split among tables by group
86/// therefore stay spread over every slot and fingerprint of their own table.
87///
88/// # Panics
89///
90/// Panics if `partitions` is not a power of two or exceeds 2^16.
91#[inline]
92pub const fn partition(hash: u64, partitions: usize) -> usize {
93    assert!(
94        partitions.is_power_of_two() && partitions <= 1 << 16,
95        "partitions must be a power of two no larger than 2^16"
96    );
97    (hash >> (FINGERPRINT_SHIFT - partitions.trailing_zeros())) as usize & (partitions - 1)
98}
99
100/// One slot of a [Table]: an entry's metadata and handle, packed so a probe reads one place.
101#[repr(C, packed)]
102struct Slot<H> {
103    /// Displacement (high 24 bits) and fingerprint (low byte), or EMPTY when unoccupied.
104    meta: u32,
105    /// The handle, meaningful only when the slot is occupied.
106    handle: H,
107}
108
109impl<H: Copy> Clone for Slot<H> {
110    fn clone(&self) -> Self {
111        *self
112    }
113}
114
115impl<H: Copy> Copy for Slot<H> {}
116
117/// An open-addressing multimap from 64-bit hashes to caller-owned handles.
118///
119/// See the [module documentation](self) for the lookup contract.
120///
121/// # Example
122///
123/// ```
124/// use commonware_utils::handles::Table;
125///
126/// // FNV-1a stands in for a keyed hasher.
127/// fn hash(key: &str) -> u64 {
128///     key.bytes().fold(0xcbf2_9ce4_8422_2325, |h, b| {
129///         (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3)
130///     })
131/// }
132///
133/// let keys = ["apple", "banana", "cherry"];
134/// let mut table = Table::new();
135/// for (handle, key) in keys.iter().enumerate() {
136///     table.insert(hash(key), handle, |handle| hash(keys[handle]));
137/// }
138///
139/// // Confirm each candidate against the key it resolves to.
140/// let find = |table: &Table<usize>, key| table.get(hash(key)).find(|&handle| keys[handle] == key);
141/// assert_eq!(find(&table, "banana"), Some(1));
142/// assert!(table.remove(hash("banana"), 1));
143/// assert_eq!(find(&table, "banana"), None);
144/// ```
145pub struct Table<H> {
146    /// The slots: a power of two of them, or none before the first allocation.
147    slots: Vec<Slot<H>>,
148    len: usize,
149}
150
151impl<H> core::fmt::Debug for Table<H> {
152    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153        f.debug_struct("Table")
154            .field("len", &self.len)
155            .field("slots", &self.slots.len())
156            .finish()
157    }
158}
159
160impl<H: Copy + Eq + Default> Table<H> {
161    /// Creates an empty table without allocating.
162    pub const fn new() -> Self {
163        Self {
164            slots: Vec::new(),
165            len: 0,
166        }
167    }
168
169    /// Creates an empty table that fits `entries` entries without growing.
170    ///
171    /// # Panics
172    ///
173    /// Panics if the required slot count overflows `usize`.
174    pub fn with_capacity(entries: usize) -> Self {
175        if entries == 0 {
176            return Self::new();
177        }
178        Self {
179            slots: Self::empty_slots(Self::slots_for(entries)),
180            len: 0,
181        }
182    }
183
184    /// Returns the slot count that fits `entries` entries without growing.
185    ///
186    /// # Panics
187    ///
188    /// Panics if the slot count overflows `usize`.
189    fn slots_for(entries: usize) -> usize {
190        entries
191            .checked_mul(4)
192            .and_then(|scaled| scaled.div_ceil(3).checked_next_power_of_two())
193            .expect("capacity overflow")
194            .max(MIN_SLOTS)
195    }
196
197    /// Returns `count` unoccupied slots.
198    fn empty_slots(count: usize) -> Vec<Slot<H>> {
199        vec![
200            Slot {
201                meta: EMPTY,
202                handle: H::default(),
203            };
204            count
205        ]
206    }
207
208    /// Returns the number of entries.
209    pub const fn len(&self) -> usize {
210        self.len
211    }
212
213    /// Returns whether the table has no entries.
214    pub const fn is_empty(&self) -> bool {
215        self.len == 0
216    }
217
218    /// Returns the number of entries that fit before the next growth.
219    pub const fn capacity(&self) -> usize {
220        self.slots.len() * 3 / 4
221    }
222
223    /// Grows the table, if needed, so that inserting `additional` more entries never grows it.
224    /// `rehash` is as for [Table::insert].
225    ///
226    /// # Panics
227    ///
228    /// Panics if the required entry or slot count overflows `usize`, or if the slots need more
229    /// than `isize::MAX` bytes.
230    pub fn reserve(&mut self, additional: usize, mut rehash: impl FnMut(H) -> u64) {
231        let entries = self.len.checked_add(additional).expect("capacity overflow");
232        if entries > self.capacity() {
233            self.rebuild(Self::slots_for(entries), &mut rehash);
234        }
235    }
236
237    /// Returns the handles whose home slot and fingerprint match `hash`, in probe order.
238    ///
239    /// Every handle inserted under `hash` is yielded. Callers confirm each candidate against the
240    /// key it resolves to, since entries with a different hash that shares the home slot and
241    /// fingerprint are yielded too.
242    #[inline]
243    pub fn get(&self, hash: u64) -> impl Iterator<Item = H> + '_ {
244        self.probe(hash).map(|slot| self.slots[slot].handle)
245    }
246
247    /// Hints the CPU to load the home slot of `hash`, so that a following lookup, insertion, or
248    /// removal under `hash` waits less on memory.
249    #[inline]
250    pub fn prefetch(&self, hash: u64) {
251        if !prefetch::ENABLED {
252            return;
253        }
254        let mask = self.slots.len().wrapping_sub(1);
255        if let Some(slot) = self.slots.get(hash as usize & mask) {
256            prefetch::prefetch(slot);
257        }
258    }
259
260    /// Inserts `handle` under `hash`.
261    ///
262    /// Duplicate hashes and duplicate handles are allowed. `rehash` must map every handle already
263    /// in the table to the hash it was inserted under, and it is called only when the table grows.
264    ///
265    /// # Panics
266    ///
267    /// Panics if an entry would end up more than 2^24 - 2 slots from its home, which takes a run
268    /// of 2^24 occupied slots (for example, 2^24 entries with identical hashes).
269    #[inline]
270    pub fn insert(&mut self, hash: u64, handle: H, mut rehash: impl FnMut(H) -> u64) {
271        if self.len == self.capacity() {
272            self.grow(&mut rehash);
273        }
274        self.place(hash, handle);
275        self.len += 1;
276    }
277
278    /// Returns the first handle among the candidates for `hash` that `matches` accepts, or inserts
279    /// `handle` under `hash` and returns it, in one walk of the run. The second value reports
280    /// whether `handle` was inserted.
281    ///
282    /// A full table grows first, even if a candidate is then accepted, so the candidates are those
283    /// [Table::get] yields after any growth, in the same order. `rehash` is as for [Table::insert].
284    ///
285    /// # Panics
286    ///
287    /// Panics if an entry would end up more than 2^24 - 2 slots from its home, or if growing the
288    /// table would need more than `isize::MAX` bytes of slots.
289    #[inline]
290    pub fn find_or_insert(
291        &mut self,
292        hash: u64,
293        matches: impl FnMut(H) -> bool,
294        handle: H,
295        mut rehash: impl FnMut(H) -> u64,
296    ) -> (H, bool) {
297        if self.len == self.capacity() {
298            self.grow(&mut rehash);
299        }
300        let (target, displacement) = match self.walk(hash, matches) {
301            Ok(found) => return (found, false),
302            Err(stop) => stop,
303        };
304        self.place_at(target, displacement, hash, handle);
305        self.len += 1;
306        (handle, true)
307    }
308
309    /// Removes one entry holding `handle` whose home slot and fingerprint match `hash`, returning
310    /// whether one existed.
311    ///
312    /// `hash` must be the hash the entry was inserted under. Like [Table::get], matching is by
313    /// home slot and fingerprint, so a different hash sharing both would remove the entry too.
314    #[inline]
315    pub fn remove(&mut self, hash: u64, handle: H) -> bool {
316        let Some(mut hole) = self.probe(hash).find(|&slot| {
317            let candidate = self.slots[slot].handle;
318            candidate == handle
319        }) else {
320            return false;
321        };
322
323        // Shift the rest of the run back one slot, stopping before an entry already at home.
324        let mask = self.slots.len() - 1;
325        loop {
326            let next = (hole + 1) & mask;
327            let Slot { meta, handle } = self.slots[next];
328            if meta == EMPTY || meta < DISPLACEMENT_UNIT {
329                break;
330            }
331            self.slots[hole] = Slot {
332                meta: meta - DISPLACEMENT_UNIT,
333                handle,
334            };
335            hole = next;
336        }
337        self.slots[hole].meta = EMPTY;
338        self.len -= 1;
339        true
340    }
341
342    /// Returns the slots holding entries with the home slot and fingerprint of `hash`.
343    ///
344    /// Such entries sit exactly at their probe distance. The walk ends at an empty slot or at an
345    /// entry closer to its home, one of which exists within [MAX_DISPLACEMENT] + 1 steps.
346    #[inline]
347    fn probe(&self, hash: u64) -> impl Iterator<Item = usize> + '_ {
348        let mask = self.slots.len().wrapping_sub(1);
349        let mut slot = hash as usize & mask;
350        let mut displacement = 0;
351        iter::from_fn(move || {
352            loop {
353                let meta = self.slots.get(slot)?.meta;
354                if meta == EMPTY || meta >> 8 < displacement {
355                    return None;
356                }
357                let current = slot;
358                let matched = meta == slot_meta(hash, displacement);
359                slot = (slot + 1) & mask;
360                displacement += 1;
361                if matched {
362                    return Some(current);
363                }
364            }
365        })
366    }
367
368    /// Walks the run from the home slot of `hash`, handing each candidate for `hash` to
369    /// `matches` until one is accepted.
370    ///
371    /// Returns the accepted handle, or else the slot an entry for `hash` would be placed in and
372    /// its displacement: the first empty slot or the first entry closer to its own home.
373    ///
374    /// Requires at least one slot.
375    #[inline]
376    fn walk(&self, hash: u64, mut matches: impl FnMut(H) -> bool) -> Result<H, (usize, u32)> {
377        let mask = self.slots.len() - 1;
378        let mut slot = hash as usize & mask;
379        let mut displacement = 0;
380        loop {
381            let Slot { meta, handle } = self.slots[slot];
382            if meta == EMPTY || meta >> 8 < displacement {
383                return Err((slot, displacement));
384            }
385            if meta == slot_meta(hash, displacement) && matches(handle) {
386                return Ok(handle);
387            }
388            assert!(
389                displacement < MAX_DISPLACEMENT,
390                "the run from the home slot of hash {hash:#x} must end within the maximum displacement"
391            );
392            slot = (slot + 1) & mask;
393            displacement += 1;
394        }
395    }
396
397    /// Places an entry in Robin Hood order without changing `len`.
398    ///
399    /// The entry goes before the first entry closer to its home (or into the first empty slot),
400    /// and the rest of the run shifts forward one slot.
401    ///
402    /// Requires an empty slot.
403    #[inline]
404    fn place(&mut self, hash: u64, handle: H) {
405        let (target, displacement) = match self.walk(hash, |_| false) {
406            Ok(_) => unreachable!("a walk that accepts no candidate must stop at a slot"),
407            Err(stop) => stop,
408        };
409        self.place_at(target, displacement, hash, handle);
410    }
411
412    /// Places an entry for `hash` at `target`, `displacement` slots from its home, and shifts the
413    /// rest of the run forward one slot, without changing `len`.
414    ///
415    /// Requires an empty slot and the `target` and `displacement` at which a walk for `hash`
416    /// stopped.
417    #[inline]
418    fn place_at(&mut self, target: usize, displacement: u32, hash: u64, handle: H) {
419        // Find the end of the run, checking that every entry can shift.
420        let mask = self.slots.len() - 1;
421        let mut slot = target;
422        loop {
423            let meta = self.slots[slot].meta;
424            if meta == EMPTY {
425                break;
426            }
427            assert!(
428                meta >> 8 < MAX_DISPLACEMENT,
429                "shifting the run from slot {target} must not move an entry past the maximum displacement"
430            );
431            slot = (slot + 1) & mask;
432        }
433
434        // Shift the run forward into the empty slot and fill the insertion point.
435        while slot != target {
436            let previous = slot.wrapping_sub(1) & mask;
437            let Slot { meta, handle } = self.slots[previous];
438            self.slots[slot] = Slot {
439                meta: meta + DISPLACEMENT_UNIT,
440                handle,
441            };
442            slot = previous;
443        }
444        self.slots[target] = Slot {
445            meta: slot_meta(hash, displacement),
446            handle,
447        };
448    }
449
450    /// Rebuilds the table with twice as many slots.
451    fn grow(&mut self, rehash: &mut impl FnMut(H) -> u64) {
452        self.rebuild((self.slots.len() * 2).max(MIN_SLOTS), rehash);
453    }
454
455    /// Rebuilds the table with `slots` slots, placing every entry under the hash `rehash` gives
456    /// its handle.
457    fn rebuild(&mut self, slots: usize, rehash: &mut impl FnMut(H) -> u64) {
458        let mut table = Self {
459            slots: Self::empty_slots(slots),
460            len: self.len,
461        };
462        for Slot { meta, handle } in self.slots.iter().copied() {
463            if meta != EMPTY {
464                table.place(rehash(handle), handle);
465            }
466        }
467        *self = table;
468    }
469}
470
471impl<H: Copy + Eq + Default> Default for Table<H> {
472    fn default() -> Self {
473        Self::new()
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480    use crate::{TestRng, test_rng};
481    use core::cell::Cell;
482    use rand::{Rng as _, RngExt as _};
483    use std::collections::BTreeMap;
484
485    impl<H: Copy + Eq + Default> Table<H> {
486        /// Returns the metadata of every slot.
487        fn metas(&self) -> Vec<u32> {
488            self.slots.iter().map(|slot| slot.meta).collect()
489        }
490
491        /// Asserts slot metadata, Robin Hood order, and `len` agree with each handle's hash.
492        fn check_invariants(&self, hash_of: impl Fn(H) -> u64) {
493            let slots = self.slots.len();
494            assert_eq!(self.capacity(), slots * 3 / 4);
495            assert!(self.len <= self.capacity());
496            if slots == 0 {
497                return;
498            }
499            assert!(slots.is_power_of_two() && slots >= MIN_SLOTS);
500            let mask = slots - 1;
501            let mut occupied = 0;
502            for (slot, Slot { meta, handle }) in self.slots.iter().copied().enumerate() {
503                if meta == EMPTY {
504                    continue;
505                }
506                occupied += 1;
507                let hash = hash_of(handle);
508                let displacement = slot.wrapping_sub(hash as usize) & mask;
509                assert!(displacement <= MAX_DISPLACEMENT as usize);
510                assert_eq!(meta, slot_meta(hash, displacement as u32));
511
512                // Each entry is at most one slot further from its home than its predecessor.
513                let next = self.slots[(slot + 1) & mask].meta;
514                assert!(next == EMPTY || next >> 8 <= (meta >> 8) + 1);
515            }
516            assert_eq!(occupied, self.len);
517        }
518    }
519
520    /// Spreads `i` across all 64 bits.
521    const fn spread(i: u64) -> u64 {
522        i.wrapping_mul(0x9E37_79B9_7F4A_7C15)
523    }
524
525    fn sorted<H: Ord>(handles: impl Iterator<Item = H>) -> Vec<H> {
526        let mut handles: Vec<H> = handles.collect();
527        handles.sort();
528        handles
529    }
530
531    #[test]
532    fn test_find_or_insert_finds_or_places() {
533        // Handles 0 and 1 share a hash. A walk that rejects every candidate inserts, and one that
534        // accepts a candidate returns it without inserting.
535        let hashes = [spread(1), spread(1), spread(2)];
536        let rehash = |handle: usize| hashes[handle];
537        let mut table = Table::new();
538        assert_eq!(
539            table.find_or_insert(hashes[0], |_| false, 0, rehash),
540            (0, true)
541        );
542        assert_eq!(
543            table.find_or_insert(hashes[0], |handle| handle == 0, 7, rehash),
544            (0, false)
545        );
546        assert_eq!(
547            table.find_or_insert(hashes[1], |handle| handle == 1, 1, rehash),
548            (1, true)
549        );
550        assert_eq!(table.get(hashes[0]).collect::<Vec<_>>(), vec![0, 1]);
551
552        // A walk that accepts every candidate stops at the first one get yields.
553        let mut offered = Vec::new();
554        let accept = |handle| {
555            offered.push(handle);
556            true
557        };
558        assert_eq!(
559            table.find_or_insert(hashes[0], accept, 7, rehash),
560            (0, false)
561        );
562        assert_eq!(offered, vec![0]);
563
564        // A hash with no candidates inserts its handle.
565        assert_eq!(
566            table.find_or_insert(hashes[2], |handle| handle == 2, 2, rehash),
567            (2, true)
568        );
569        assert_eq!(table.len(), 3);
570        table.check_invariants(rehash);
571    }
572
573    #[test]
574    fn test_find_or_insert_grows_and_prefetch_is_harmless() {
575        // Every handle goes in through find_or_insert, growing the table several times, and is
576        // then found again. Prefetching a hash, on an empty table or before each insert, changes
577        // nothing.
578        let mut rng = test_rng();
579        let hashes: Vec<u64> = (0..1000).map(|_| rng.random()).collect();
580        let rehash = |handle: usize| hashes[handle];
581        Table::<usize>::new().prefetch(hashes[0]);
582        let mut table = Table::new();
583        for (handle, &hash) in hashes.iter().enumerate() {
584            table.prefetch(hash);
585            assert_eq!(
586                table.find_or_insert(hash, |candidate| candidate == handle, handle, rehash),
587                (handle, true)
588            );
589            assert_eq!(
590                table.find_or_insert(hash, |candidate| candidate == handle, usize::MAX, rehash),
591                (handle, false)
592            );
593        }
594        assert_eq!(table.len(), hashes.len());
595        table.check_invariants(rehash);
596        for (handle, &hash) in hashes.iter().enumerate() {
597            assert!(table.get(hash).any(|candidate| candidate == handle));
598        }
599    }
600
601    #[test]
602    fn test_find_or_insert_grows_a_full_table_before_walking() {
603        // Twelve handles fill a 16-slot table. All share a fingerprint and home slot 15, so each
604        // is a candidate for each of these hashes, and doubling the table moves the odd ones to
605        // home slot 31.
606        let hashes: Vec<u64> = (0..=12)
607            .map(|handle| 0xAB00_0000_0000_000F | ((handle % 2) << 4))
608            .collect();
609        let rehash = |handle: usize| hashes[handle];
610        let full = || {
611            let mut table = Table::new();
612            for (handle, &hash) in hashes.iter().enumerate().take(12) {
613                table.insert(hash, handle, rehash);
614            }
615            assert_eq!(table.len(), table.capacity());
616            table
617        };
618
619        // Whether the walk would accept odd handle 1 or even handle 2, the full table grows first,
620        // so the walk is offered only even handles. Handle 1 is then no candidate, and handle 12 is
621        // inserted.
622        for (accept, want) in [(1, (12, true)), (2, (2, false))] {
623            let mut table = full();
624            let mut offered = Vec::new();
625            let found = table.find_or_insert(
626                hashes[12],
627                |handle| {
628                    offered.push(handle);
629                    handle == accept
630                },
631                12,
632                rehash,
633            );
634            assert_eq!(found, want);
635            assert_eq!(table.capacity(), 24);
636            assert!(!offered.is_empty() && offered.iter().all(|handle| handle % 2 == 0));
637            table.check_invariants(rehash);
638        }
639    }
640
641    #[test]
642    fn test_find_or_insert_offers_the_candidates_of_get() {
643        // The first two hashes share a home slot and fingerprint, the third only the fingerprint,
644        // and the fourth only the home slot, so the run from slot 5 holds non-candidates for each.
645        // Every seeded hash is inserted again under a new handle, along with 50 fresh ones.
646        let mut rng = test_rng();
647        let mut hashes = vec![
648            0xAB00_0000_0000_0005,
649            0xAB00_0100_0000_0005,
650            0xAB00_0000_0000_0006,
651            0xCD00_0000_0000_0005,
652        ];
653        hashes.extend((0..200).map(|_| rng.next_u64()));
654        let seeded = hashes.len();
655        hashes.extend_from_within(..seeded);
656        hashes.extend((0..50).map(|_| rng.next_u64()));
657        let rehash = |handle: usize| hashes[handle];
658
659        // Seed the table, then reserve room for the rest so no walk grows it.
660        let mut table = Table::new();
661        for (handle, &hash) in hashes.iter().enumerate().take(seeded) {
662            table.insert(hash, handle, rehash);
663        }
664        table.reserve(hashes.len() - seeded, rehash);
665
666        // Each walk accepts no candidate, so it offers exactly the candidates get yields, in order,
667        // and inserts its handle as the last candidate for its hash.
668        for (handle, &hash) in hashes.iter().enumerate().skip(seeded) {
669            let before: Vec<usize> = table.get(hash).collect();
670            let mut offered = Vec::new();
671            let matches = |candidate: usize| {
672                offered.push(candidate);
673                false
674            };
675            assert_eq!(
676                table.find_or_insert(hash, matches, handle, rehash),
677                (handle, true)
678            );
679            assert_eq!(
680                offered, before,
681                "the walk for hash {hash:#x} must offer the candidates get yields, in order"
682            );
683            let after: Vec<usize> = table.get(hash).collect();
684            assert_eq!(
685                after,
686                [before, vec![handle]].concat(),
687                "the handle inserted under hash {hash:#x} must follow its existing candidates"
688            );
689        }
690        table.check_invariants(rehash);
691    }
692
693    #[test]
694    fn test_basic_insert_get_remove() {
695        // Handles 0 and 1 share a hash, and handle 4 is never inserted.
696        let hashes = [spread(1), spread(1), spread(2), spread(3), spread(4)];
697        let rehash = |handle: usize| hashes[handle];
698        let mut table = Table::new();
699        assert!(table.is_empty());
700        assert_eq!(table.slots.capacity(), 0);
701        assert_eq!(table.get(hashes[0]).next(), None);
702        assert!(!table.remove(hashes[0], 0));
703
704        for (handle, &hash) in hashes.iter().enumerate().take(4) {
705            table.insert(hash, handle, rehash);
706        }
707        assert_eq!(table.len(), 4);
708        assert_eq!(sorted(table.get(hashes[0])), [0, 1]);
709        assert_eq!(sorted(table.get(hashes[2])), [2]);
710        assert_eq!(sorted(table.get(hashes[3])), [3]);
711        assert_eq!(table.get(hashes[4]).next(), None);
712
713        // Removing one duplicate leaves the other.
714        assert!(table.remove(hashes[0], 0));
715        assert_eq!(sorted(table.get(hashes[0])), [1]);
716        assert_eq!(table.len(), 3);
717
718        // Removing absent handles changes nothing.
719        let metas = table.metas();
720        assert!(!table.remove(hashes[0], 0));
721        assert!(!table.remove(hashes[4], 4));
722        assert_eq!(table.metas(), metas);
723        assert_eq!(table.len(), 3);
724
725        // The same handle may be inserted twice under one hash, and is removed once at a time.
726        table.insert(hashes[1], 1, rehash);
727        assert_eq!(sorted(table.get(hashes[1])), [1, 1]);
728        assert!(table.remove(hashes[1], 1));
729        assert_eq!(sorted(table.get(hashes[1])), [1]);
730        table.check_invariants(rehash);
731    }
732
733    #[test]
734    fn test_fingerprint_false_candidates() {
735        // Hash 1 shares the home slot and fingerprint of hash 0 but differs in its middle bits.
736        // Hash 2 shares only the fingerprint, and hash 3 only the home slot.
737        let keys = ["a", "b", "c", "d"];
738        let hashes = [
739            0xAB00_0000_0000_0005,
740            0xAB00_0100_0000_0005,
741            0xAB00_0000_0000_0006,
742            0xCD00_0000_0000_0005,
743        ];
744        let rehash = |handle: usize| hashes[handle];
745        let mut table = Table::new();
746        for (handle, &hash) in hashes.iter().enumerate() {
747            table.insert(hash, handle, rehash);
748        }
749
750        // The table is a candidate generator, so callers resolve each handle to confirm its key.
751        assert_eq!(sorted(table.get(hashes[0])), [0, 1]);
752        let confirmed: Vec<_> = table.get(hashes[0]).filter(|&h| keys[h] == "a").collect();
753        assert_eq!(confirmed, [0]);
754        assert_eq!(sorted(table.get(hashes[2])), [2]);
755        assert_eq!(sorted(table.get(hashes[3])), [3]);
756        table.check_invariants(rehash);
757    }
758
759    #[test]
760    fn test_growth_rehashes_every_entry() {
761        let mut rng = test_rng();
762        let mut hashes = Vec::new();
763        let calls = Cell::new(0);
764        let mut table = Table::new();
765        let mut growths = 0;
766        for handle in 0..5_000 {
767            hashes.push(rng.next_u64());
768            let (slots, len, before) = (table.slots.len(), table.len(), calls.get());
769            table.insert(hashes[handle], handle, |handle| {
770                calls.set(calls.get() + 1);
771                hashes[handle]
772            });
773            assert_eq!(table.len(), handle + 1);
774            if table.slots.len() == slots {
775                assert_eq!(calls.get(), before);
776                continue;
777            }
778
779            // Growth doubles once, rehashing every existing entry exactly once.
780            growths += 1;
781            assert_eq!(table.slots.len(), (slots * 2).max(MIN_SLOTS));
782            assert_eq!(len, slots * 3 / 4);
783            assert_eq!(calls.get() - before, len);
784            table.check_invariants(|handle| hashes[handle]);
785            for (handle, &hash) in hashes.iter().enumerate() {
786                assert!(table.get(hash).any(|h| h == handle));
787            }
788        }
789        assert_eq!(growths, 10);
790        assert_eq!(table.slots.len(), 8_192);
791    }
792
793    #[test]
794    fn test_shared_hash_run() {
795        // Every entry shares one hash, so all sit in one run at consecutive displacements, and
796        // the run survives growth and removal from its middle.
797        const ENTRIES: usize = 2_000;
798        let mut table = Table::new();
799        for handle in 0..ENTRIES {
800            table.insert(spread(1), handle, |_| spread(1));
801        }
802        assert_eq!(table.slots.len(), 4_096);
803        assert_eq!(table.get(spread(1)).count(), ENTRIES);
804        table.check_invariants(|_| spread(1));
805
806        for handle in (0..ENTRIES).step_by(2) {
807            assert!(table.remove(spread(1), handle));
808        }
809        assert_eq!(
810            sorted(table.get(spread(1))),
811            sorted((1..ENTRIES).step_by(2))
812        );
813        table.check_invariants(|_| spread(1));
814    }
815
816    #[test]
817    fn test_matches_multimap_model() {
818        const HANDLES: usize = 1_024;
819        const HASHES: usize = 512;
820        const OPERATIONS: usize = 50_000;
821        let mut rng = TestRng::new(5);
822
823        // Paired hashes share their low 24 bits (one home at every reachable size), and
824        // fingerprints take four values, so false candidates are common. Handles map to hashes
825        // many-to-one, and a handle may be inserted repeatedly.
826        let mut pool: Vec<u64> = Vec::with_capacity(HASHES);
827        for i in 0..HASHES {
828            let fingerprint = rng.random_range(0..4u64) << 56;
829            let middle = rng.next_u64() & 0x00FF_FFFF_FF00_0000;
830            let low = if i % 2 == 1 {
831                pool[i - 1]
832            } else {
833                rng.next_u64()
834            } & 0xFF_FFFF;
835            pool.push(fingerprint | middle | low);
836        }
837        let hash_of: Vec<u64> = (0..HANDLES)
838            .map(|_| pool[rng.random_range(0..pool.len())])
839            .collect();
840        let rehash_calls = Cell::new(0);
841        let rehash = |handle: usize| {
842            rehash_calls.set(rehash_calls.get() + 1);
843            hash_of[handle]
844        };
845
846        let mut model: BTreeMap<u64, Vec<usize>> = BTreeMap::new();
847        let mut table = Table::new();
848        let (mut removed, mut false_candidates, mut max_len) = (0, 0, 0);
849        for op in 0..OPERATIONS {
850            // Alternate phases that favor insertion and removal.
851            let inserts = if (op / 5_000) % 2 == 0 { 6 } else { 3 };
852            let roll = rng.random_range(0..10);
853            let (slots, calls) = (table.slots.len(), rehash_calls.get());
854            let hash = if roll < inserts {
855                let handle = rng.random_range(0..HANDLES);
856                table.insert(hash_of[handle], handle, rehash);
857                model.entry(hash_of[handle]).or_default().push(handle);
858                hash_of[handle]
859            } else if roll < 9 {
860                let handle = rng.random_range(0..HANDLES);
861                let hash = hash_of[handle];
862                let handles = model.get_mut(&hash);
863                let position = handles
864                    .as_ref()
865                    .and_then(|handles| handles.iter().position(|&h| h == handle));
866                assert_eq!(table.remove(hash, handle), position.is_some());
867                if let (Some(handles), Some(position)) = (handles, position) {
868                    handles.swap_remove(position);
869                    if handles.is_empty() {
870                        model.remove(&hash);
871                    }
872                    removed += 1;
873                }
874                hash
875            } else if rng.random_bool(0.5) {
876                pool[rng.random_range(0..pool.len())]
877            } else {
878                rng.next_u64()
879            };
880            if table.slots.len() == slots {
881                assert_eq!(rehash_calls.get(), calls);
882            }
883            assert_eq!(table.len(), model.values().map(Vec::len).sum::<usize>());
884            max_len = max_len.max(table.len());
885
886            // Candidates are exactly the entries sharing the home slot and fingerprint.
887            let mask = table.slots.len().wrapping_sub(1);
888            let expected = sorted(
889                model
890                    .iter()
891                    .filter(|&(&other, _)| {
892                        other >> 56 == hash >> 56 && (other ^ hash) & mask as u64 == 0
893                    })
894                    .flat_map(|(_, handles)| handles.iter().copied()),
895            );
896            let actual = sorted(table.get(hash));
897            assert_eq!(actual, expected);
898            false_candidates += actual.iter().filter(|&&h| hash_of[h] != hash).count();
899
900            if op % 1_000 == 0 {
901                table.check_invariants(|handle| hash_of[handle]);
902            }
903        }
904        table.check_invariants(|handle| hash_of[handle]);
905        assert!(removed > 10_000 && false_candidates > 1_000 && max_len > 2_000);
906    }
907
908    #[test]
909    fn test_with_capacity_never_rehashes() {
910        let table = Table::<u64>::with_capacity(0);
911        assert_eq!(table.slots.capacity(), 0);
912
913        let mut rng = test_rng();
914        for entries in [1, 12, 13, 100, 384, 385, 1_000] {
915            let mut table = Table::with_capacity(entries);
916            let slots = table.slots.len();
917            assert!(table.capacity() >= entries);
918            assert!(slots == MIN_SLOTS || slots / 2 * 3 / 4 < entries);
919            for handle in 0..entries {
920                table.insert(rng.next_u64(), handle, |_| {
921                    unreachable!("rehash without growth")
922                });
923            }
924            assert_eq!(table.slots.len(), slots);
925            assert_eq!(table.len(), entries);
926        }
927    }
928
929    #[test]
930    fn test_reserve_grows_once_and_keeps_entries() {
931        let mut rng = test_rng();
932        let hashes: Vec<u64> = (0..1_000).map(|_| rng.next_u64()).collect();
933        let rehash = |handle: usize| hashes[handle];
934        let calls = Cell::new(0);
935        let counted = |handle: usize| {
936            calls.set(calls.get() + 1);
937            hashes[handle]
938        };
939
940        // Reserving nothing on an empty table allocates nothing.
941        let mut table = Table::new();
942        table.reserve(0, rehash);
943        assert_eq!(table.slots.capacity(), 0);
944
945        // A reserve that fits in the current capacity changes nothing.
946        for (handle, &hash) in hashes.iter().enumerate().take(600) {
947            table.insert(hash, handle, rehash);
948        }
949        let slots = table.slots.len();
950        table.reserve(1, counted);
951        assert_eq!(table.slots.len(), slots);
952        assert_eq!(calls.get(), 0);
953
954        // A larger reserve rebuilds the table once, rehashing each held entry once, so the reserved
955        // inserts never rehash. The held entries outnumber the reserved ones, so the inserts fit
956        // only if the reserve counts the held entries.
957        table.reserve(hashes.len() - 600, counted);
958        assert!(table.capacity() >= hashes.len());
959        assert_eq!(calls.get(), 600);
960        let slots = table.slots.len();
961        for (handle, &hash) in hashes.iter().enumerate().skip(600) {
962            table.insert(hash, handle, |_| {
963                unreachable!("reserved inserts must not rehash")
964            });
965        }
966        assert_eq!(table.slots.len(), slots);
967        assert_eq!(table.len(), hashes.len());
968        table.check_invariants(rehash);
969        for (handle, &hash) in hashes.iter().enumerate() {
970            assert!(table.get(hash).any(|candidate| candidate == handle));
971        }
972    }
973
974    #[test]
975    fn test_partition_reads_neither_home_nor_fingerprint_bits() {
976        // The group of 256 comes from the byte just below the fingerprint.
977        assert_eq!(partition(0xAB << (FINGERPRINT_SHIFT - 8), 256), 0xAB);
978
979        // Flipping the fingerprint or the low 40 bits, which hold the home slot of any table
980        // below 2^40 slots, never moves a hash to another group.
981        let mut rng = test_rng();
982        for _ in 0..1_000 {
983            let hash: u64 = rng.random();
984            for partitions in [1, 2, 256, 1 << 16] {
985                let group = partition(hash, partitions);
986                assert!(
987                    group < partitions,
988                    "group must be below the partition count"
989                );
990                assert_eq!(
991                    partition(hash ^ (0xFF << FINGERPRINT_SHIFT), partitions),
992                    group
993                );
994                assert_eq!(partition(hash ^ ((1 << 40) - 1), partitions), group);
995            }
996        }
997    }
998
999    #[test]
1000    #[should_panic(expected = "partitions must be a power of two")]
1001    fn test_partition_rejects_a_count_that_is_not_a_power_of_two() {
1002        partition(0, 3);
1003    }
1004}