Skip to main content

kevy_map/
map_keyed.rs

1//! Insert, grow, look up and remove — the operations that need to compare keys.
2//!
3//! The layout they operate on is documented in [`crate::map`]: `cap` metadata
4//! bytes plus a `GROUP_WIDTH` mirror tail, one byte per slot, `EMPTY` = 0xFF,
5//! `DELETED` = 0x80, and a full slot holding `h2(hash)` — the top seven bits,
6//! never 0x80 or 0xFF. This file is the probe that reads them.
7//!
8//! # The probe
9//!
10//! Probing is by **group of 16, then linear**, not by the more usual
11//! quadratic step:
12//!
13//! ```text
14//!   group_start = hash & mask
15//!   loop:
16//!     load 16 metadata bytes at group_start        (one SIMD word)
17//!     for each byte == h2(hash):  compare the key  (a real candidate)
18//!     if any byte == EMPTY:       stop             (the key is not here)
19//!     group_start = (group_start + 16) & mask      (next group, linear)
20//! ```
21//!
22//! Linear beats quadratic here because the scan is already group-aware: at
23//! this load factor the next group is usually the next cache line, and a
24//! quadratic step throws that away for a collision pattern the h2 filter has
25//! already broken up.
26//!
27//! # Three invariants the probe depends on
28//!
29//! 1. **`EMPTY` terminates, `DELETED` does not.** That is the whole reason
30//!    the two constants differ: a removed slot must stay walkable or every
31//!    key that probed past it becomes unreachable. Removal writes `DELETED`,
32//!    never `EMPTY`.
33//! 2. **The slot is marked `DELETED` *before* the value is moved out.**
34//!    `remove` writes the metadata byte first, then `ptr::read`s the pair.
35//!    The order is what makes the move sound: once the byte says `DELETED`,
36//!    nothing — not a later probe, not `Drop`, not a rehash — will read that
37//!    slot again, so moving the `(K, V)` out cannot become a double drop.
38//!    Reading first and marking second would leave a window in which the slot
39//!    claims to hold a value that has already been moved away.
40//! 3. **The mirror tail is kept in sync.** Every metadata write goes to both
41//!    `i` and its mirror index, so a group load starting near the end of the
42//!    table reads real bytes rather than falling off the allocation.
43//!
44//! Growth lives in `grow.rs`.
45
46use core::borrow::Borrow;
47use core::ptr;
48
49use kevy_hash::KevyHash;
50
51use crate::group::Group;
52use crate::map::{DELETED, EMPTY, GROUP_WIDTH, KevyMap, ProbeOutcome, h2};
53
54impl<K: KevyHash + Eq, V> KevyMap<K, V> {
55    /// Insert `(key, value)`. Returns the old value if `key` was already
56    /// present. Following `std::HashMap` semantics, the existing K is kept on
57    /// overwrite — only V is replaced.
58    /// # Examples
59    ///
60    /// ```
61    /// let mut m = kevy_map::KevyMap::new();
62    /// assert_eq!(m.insert(b"k".to_vec(), 1u32), None, "no previous value");
63    /// assert_eq!(m.insert(b"k".to_vec(), 2), Some(1), "the OLD value comes back");
64    /// assert_eq!(m.get(b"k".as_slice()), Some(&2));
65    /// ```
66    pub fn insert(&mut self, key: K, value: V) -> Option<V> {
67        self.maybe_grow();
68        self.insert_with_room(key, value)
69    }
70
71    /// [`Self::insert`] after the growth check, which the caller has made.
72    #[inline]
73    pub(crate) fn insert_with_room(&mut self, key: K, value: V) -> Option<V> {
74        let hash = key.kevy_hash();
75        match self.probe_with_key(hash, &key) {
76            ProbeOutcome::Found(idx) => {
77                // SAFETY: `idx` came from a probe that found a full metadata byte, so the
78                // slot at `idx` holds an initialised `(K, V)`; only its V is replaced.
79                let v_ptr = unsafe {
80                    let kv: *mut (K, V) = self.slots_ptr.as_ptr().add(idx).cast::<(K, V)>();
81                    ptr::addr_of_mut!((*kv).1)
82                };
83                // SAFETY: `v_ptr` points at that slot's initialised `V`, so the old value
84                // is a valid `V` to move out and the new one is written in its place.
85                let old_v = unsafe { ptr::replace(v_ptr, value) };
86                drop(key);
87                Some(old_v)
88            }
89            ProbeOutcome::NotFound { insert_at, via_tombstone } => {
90                self.set_meta(insert_at, h2(hash));
91                // SAFETY: insert_at < cap ⇒ slot pointer in-bounds; we write
92                // (K, V) into a previously uninitialised slot.
93                unsafe {
94                    (*self.slots_ptr.as_ptr().add(insert_at)).write((key, value));
95                }
96                self.occupied += 1;
97                if via_tombstone {
98                    self.deleted -= 1;
99                }
100                None
101            }
102        }
103    }
104
105    pub(crate) fn maybe_grow(&mut self) {
106        if self.grows_on_insert() {
107            self.grow();
108        }
109    }
110
111    // LOC-WAIVER: per-op probe hot body — deliberate fast/slow loop pair (tombstone-free vs tracking).
112    fn probe_with_key(&self, hash: u64, key: &K) -> ProbeOutcome {
113        if self.cap == 0 {
114            return ProbeOutcome::NotFound { insert_at: 0, via_tombstone: false };
115        }
116        let h2v = h2(hash);
117        let mut group_start = (hash as usize) & self.mask;
118
119        // Fast path: no tombstones in the table ⇒ skip DELETED tracking
120        // entirely. This trims one SIMD `match_byte` (and one branch) from
121        // every group iteration; insert workloads with no deletions hit
122        // this path exclusively.
123        if self.deleted == 0 {
124            loop {
125                // SAFETY: see [insert_known_unique].
126                let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
127                for m in g.match_byte(h2v).iter() {
128                    let slot = (group_start + m) & self.mask;
129                    // SAFETY: matched h2 ⇒ slot is occupied ⇒ initialised.
130                    let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
131                    if &kv.0 == key {
132                        return ProbeOutcome::Found(slot);
133                    }
134                }
135                if let Some(m) = g.match_byte(EMPTY).lowest_set() {
136                    return ProbeOutcome::NotFound {
137                        insert_at: (group_start + m) & self.mask,
138                        via_tombstone: false,
139                    };
140                }
141                group_start = (group_start + GROUP_WIDTH) & self.mask;
142            }
143        }
144
145        // Slow path: tombstones exist; track the first DELETED so insert
146        // can reclaim it instead of growing the tombstone count.
147        let mut first_deleted: Option<usize> = None;
148        loop {
149            // SAFETY: see [insert_known_unique].
150            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
151            for m in g.match_byte(h2v).iter() {
152                let slot = (group_start + m) & self.mask;
153                // SAFETY: matched h2 ⇒ slot is occupied ⇒ initialised.
154                let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
155                if &kv.0 == key {
156                    return ProbeOutcome::Found(slot);
157                }
158            }
159            if first_deleted.is_none()
160                && let Some(m) = g.match_byte(DELETED).lowest_set()
161            {
162                first_deleted = Some((group_start + m) & self.mask);
163            }
164            if let Some(m) = g.match_byte(EMPTY).lowest_set() {
165                let probe_empty = (group_start + m) & self.mask;
166                return ProbeOutcome::NotFound {
167                    insert_at: first_deleted.unwrap_or(probe_empty),
168                    via_tombstone: first_deleted.is_some(),
169                };
170            }
171            group_start = (group_start + GROUP_WIDTH) & self.mask;
172        }
173    }
174}
175
176impl<K, V> KevyMap<K, V> {
177    /// Borrow the value for `key`, or `None` if absent.
178    /// # Examples
179    ///
180    /// ```
181    /// let mut m = kevy_map::KevyMap::new();
182    /// m.insert(b"k".to_vec(), 1u32);
183    /// // Borrowed lookup: a `&[u8]` finds a `Vec<u8>` key without allocating.
184    /// assert_eq!(m.get(b"k".as_slice()), Some(&1));
185    /// assert_eq!(m.get(b"nope".as_slice()), None);
186    /// ```
187    pub fn get<Q>(&self, key: &Q) -> Option<&V>
188    where
189        K: Borrow<Q>,
190        Q: KevyHash + Eq + ?Sized,
191    {
192        let idx = self.find_by_borrow(key)?;
193        // SAFETY: find_by_borrow only returns indices into full slots.
194        let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_ref() };
195        Some(&kv.1)
196    }
197
198    /// Mutably borrow the value for `key`, or `None` if absent.
199    /// # Examples
200    ///
201    /// ```
202    /// let mut m = kevy_map::KevyMap::new();
203    /// m.insert(b"k".to_vec(), 1u32);
204    /// if let Some(v) = m.get_mut(b"k".as_slice()) { *v += 41; }
205    /// assert_eq!(m.get(b"k".as_slice()), Some(&42));
206    /// ```
207    pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
208    where
209        K: Borrow<Q>,
210        Q: KevyHash + Eq + ?Sized,
211    {
212        let idx = self.find_by_borrow(key)?;
213        // SAFETY: full slot.
214        let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_mut() };
215        Some(&mut kv.1)
216    }
217
218    /// Whether `key` is present in the map.
219    /// # Examples
220    ///
221    /// ```
222    /// let mut m = kevy_map::KevyMap::new();
223    /// m.insert(b"k".to_vec(), ());
224    /// assert!(m.contains_key(b"k".as_slice()));
225    /// assert!(!m.contains_key(b"j".as_slice()));
226    /// ```
227    pub fn contains_key<Q>(&self, key: &Q) -> bool
228    where
229        K: Borrow<Q>,
230        Q: KevyHash + Eq + ?Sized,
231    {
232        self.find_by_borrow(key).is_some()
233    }
234
235    /// Remove `key`'s entry; returns the previous value if present.
236    /// # Examples
237    ///
238    /// ```
239    /// let mut m = kevy_map::KevyMap::new();
240    /// m.insert(b"k".to_vec(), 7u32);
241    /// assert_eq!(m.remove(b"k".as_slice()), Some(7), "the value comes back");
242    /// assert_eq!(m.remove(b"k".as_slice()), None, "absent — None, not a panic");
243    /// assert!(m.is_empty());
244    /// ```
245    pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
246    where
247        K: Borrow<Q>,
248        Q: KevyHash + Eq + ?Sized,
249    {
250        let idx = self.find_by_borrow(key)?;
251        let mark = self.erase_mark(idx);
252        self.set_meta(idx, mark);
253        self.occupied -= 1;
254        if mark == DELETED {
255            self.deleted += 1;
256        }
257        // SAFETY: slot was full, we just marked it DELETED so it won't be
258        // read again; ptr::read moves the (K, V) out.
259        let (_k, v) = unsafe { ptr::read(self.slots_ptr.as_ptr().add(idx) as *const (K, V)) };
260        Some(v)
261    }
262
263    /// `EMPTY` or `DELETED` for a slot being erased.
264    ///
265    /// A tombstone exists to keep a probe going past a hole. When no
266    /// probe could have walked through this position, the hole stops
267    /// nothing and `EMPTY` is the honest mark — which frees the slot for
268    /// reuse and keeps it out of the load count.
269    ///
270    /// Writing `DELETED` unconditionally cost two things. `deleted` only
271    /// returns to zero at a grow, and both the insert probe and
272    /// `raw_entry` branch on `self.deleted == 0` — so ONE `DEL` put the
273    /// table on its slower probe for the rest of the table's life, an
274    /// extra SIMD compare and branch per group forever. And tombstones
275    /// count toward the load threshold, so they drive doubling: the
276    /// steady state under constant-live churn measured 3,048 tombstones
277    /// against a 3,072 headroom, 24 slots from a doubling that the live
278    /// set never asked for.
279    fn erase_mark(&self, idx: usize) -> u8 {
280        // hashbrown's rule, and the reason it is not "does this slot's
281        // group hold an EMPTY": probes here start at `hash & mask` and
282        // are NOT group-aligned, so the sequence that placed a later key
283        // may have entered from any of the `GROUP_WIDTH - 1` positions
284        // before this one. What decides it is whether a run of at least
285        // `GROUP_WIDTH` non-empty slots spans this position — if one
286        // does, some probe could have walked through, and the hole has
287        // to stay a tombstone.
288        //
289        // A simpler condition was tried and it lost a key:
290        // `clone_after_heavy_deletion_keeps_probes_correct` found it
291        // immediately, which is what that test is for.
292        let before = idx.wrapping_sub(GROUP_WIDTH) & self.mask;
293        // SAFETY: both indices are < cap and the metadata array is
294        // `cap + GROUP_WIDTH` bytes, so either group load is in bounds.
295        let (gb, ga) = unsafe {
296            (
297                Group::load(self.metadata_ptr.as_ptr().add(before)),
298                Group::load(self.metadata_ptr.as_ptr().add(idx)),
299            )
300        };
301        let empty_before = gb.match_byte(EMPTY).slot_mask().leading_zeros() as usize;
302        let empty_after = ga.match_byte(EMPTY).slot_mask().trailing_zeros() as usize;
303        if empty_before + empty_after >= GROUP_WIDTH { DELETED } else { EMPTY }
304    }
305
306    pub(crate) fn find_by_borrow<Q>(&self, key: &Q) -> Option<usize>
307    where
308        K: Borrow<Q>,
309        Q: KevyHash + Eq + ?Sized,
310    {
311        if self.cap == 0 {
312            return None;
313        }
314        let hash = key.kevy_hash();
315        let h2v = h2(hash);
316        let mut group_start = (hash as usize) & self.mask;
317        loop {
318            // SAFETY: see [insert_known_unique]; group_start ∈ [0, cap),
319            // metadata length ≥ cap + GROUP_WIDTH.
320            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
321            for m in g.match_byte(h2v).iter() {
322                let slot = (group_start + m) & self.mask;
323                // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
324                let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
325                if kv.0.borrow() == key {
326                    return Some(slot);
327                }
328            }
329            // EMPTY in this group ⇒ key cannot be later in the probe.
330            if !g.match_byte(EMPTY).is_empty() {
331                return None;
332            }
333            group_start = (group_start + GROUP_WIDTH) & self.mask;
334        }
335    }
336
337    /// Full probe: returns `Found(idx)` if `key` is present, else
338    /// `NotFound { insert_at, via_tombstone }` describing the slot a future
339    /// insert would take. Mirrors [`probe_with_key`](Self::probe_with_key)
340    /// but accepts a `Borrow<Q>` key.
341    ///
342    /// Used by the [`raw_entry_mut`](Self::raw_entry_mut) API to fuse a read
343    /// and a possible insert into a single probe.
344    pub(crate) fn probe_by_borrow<Q>(&self, key: &Q) -> ProbeOutcome
345    where
346        K: Borrow<Q>,
347        Q: KevyHash + Eq + ?Sized,
348    {
349        if self.cap == 0 {
350            return ProbeOutcome::NotFound { insert_at: 0, via_tombstone: false };
351        }
352        let hash = key.kevy_hash();
353        let h2v = h2(hash);
354        let group_start = (hash as usize) & self.mask;
355        if self.deleted == 0 {
356            self.probe_by_borrow_fast(key, h2v, group_start)
357        } else {
358            self.probe_by_borrow_slow(key, h2v, group_start)
359        }
360    }
361
362    /// Fast path for `probe_by_borrow`: no tombstones in the table, so we
363    /// can stop tracking DELETED slots entirely.
364    fn probe_by_borrow_fast<Q>(&self, key: &Q, h2v: u8, mut group_start: usize) -> ProbeOutcome
365    where
366        K: Borrow<Q>,
367        Q: KevyHash + Eq + ?Sized,
368    {
369        loop {
370            // SAFETY: see [insert_known_unique].
371            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
372            for m in g.match_byte(h2v).iter() {
373                let slot = (group_start + m) & self.mask;
374                // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
375                let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
376                if kv.0.borrow() == key {
377                    return ProbeOutcome::Found(slot);
378                }
379            }
380            if let Some(m) = g.match_byte(EMPTY).lowest_set() {
381                return ProbeOutcome::NotFound {
382                    insert_at: (group_start + m) & self.mask,
383                    via_tombstone: false,
384                };
385            }
386            group_start = (group_start + GROUP_WIDTH) & self.mask;
387        }
388    }
389
390    /// Slow path for `probe_by_borrow`: tombstones present; remember the
391    /// first DELETED so a later insert can reclaim it.
392    fn probe_by_borrow_slow<Q>(&self, key: &Q, h2v: u8, mut group_start: usize) -> ProbeOutcome
393    where
394        K: Borrow<Q>,
395        Q: KevyHash + Eq + ?Sized,
396    {
397        let mut first_deleted: Option<usize> = None;
398        loop {
399            // SAFETY: see [insert_known_unique].
400            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
401            for m in g.match_byte(h2v).iter() {
402                let slot = (group_start + m) & self.mask;
403                // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
404                let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
405                if kv.0.borrow() == key {
406                    return ProbeOutcome::Found(slot);
407                }
408            }
409            if first_deleted.is_none()
410                && let Some(m) = g.match_byte(DELETED).lowest_set()
411            {
412                first_deleted = Some((group_start + m) & self.mask);
413            }
414            if let Some(m) = g.match_byte(EMPTY).lowest_set() {
415                let probe_empty = (group_start + m) & self.mask;
416                return ProbeOutcome::NotFound {
417                    insert_at: first_deleted.unwrap_or(probe_empty),
418                    via_tombstone: first_deleted.is_some(),
419                };
420            }
421            group_start = (group_start + GROUP_WIDTH) & self.mask;
422        }
423    }
424}