Skip to main content

kevy_map/
map_keyed.rs

1//! Key-trait-bound `KevyMap` operations: insert/grow/lookup/remove.
2//!
3//! Split out of [`crate::map`] for file-size hygiene. The raw / non-keyed
4//! impl block (allocation, metadata bookkeeping, iter, Drop, trait impls)
5//! stays in `map.rs`; everything that needs `K: KevyHash + Eq` or
6//! `K: Borrow<Q>, Q: KevyHash + Eq` lives here.
7
8use core::borrow::Borrow;
9use core::ptr;
10
11use kevy_hash::KevyHash;
12
13use crate::group::Group;
14use crate::map::{DELETED, EMPTY, GROUP_WIDTH, KevyMap, MIN_CAP, ProbeOutcome, h2};
15
16impl<K: KevyHash + Eq, V> KevyMap<K, V> {
17    /// Insert `(key, value)`. Returns the old value if `key` was already
18    /// present. Following `std::HashMap` semantics, the existing K is kept on
19    /// overwrite — only V is replaced.
20    /// # Examples
21    ///
22    /// ```
23    /// let mut m = kevy_map::KevyMap::new();
24    /// assert_eq!(m.insert(b"k".to_vec(), 1u32), None, "no previous value");
25    /// assert_eq!(m.insert(b"k".to_vec(), 2), Some(1), "the OLD value comes back");
26    /// assert_eq!(m.get(b"k".as_slice()), Some(&2));
27    /// ```
28    pub fn insert(&mut self, key: K, value: V) -> Option<V> {
29        self.maybe_grow();
30        let hash = key.kevy_hash();
31        match self.probe_with_key(hash, &key) {
32            ProbeOutcome::Found(idx) => {
33                // SAFETY: slot is full ⇒ initialised. We replace only the V
34                // field; the old K is kept (std HashMap semantics).
35                let v_ptr = unsafe {
36                    let kv: *mut (K, V) = self.slots_ptr.as_ptr().add(idx).cast::<(K, V)>();
37                    ptr::addr_of_mut!((*kv).1)
38                };
39                let old_v = unsafe { ptr::replace(v_ptr, value) };
40                drop(key);
41                Some(old_v)
42            }
43            ProbeOutcome::NotFound { insert_at, via_tombstone } => {
44                self.set_meta(insert_at, h2(hash));
45                // SAFETY: insert_at < cap ⇒ slot pointer in-bounds; we write
46                // (K, V) into a previously uninitialised slot.
47                unsafe {
48                    (*self.slots_ptr.as_ptr().add(insert_at)).write((key, value));
49                }
50                self.occupied += 1;
51                if via_tombstone {
52                    self.deleted -= 1;
53                }
54                None
55            }
56        }
57    }
58
59    pub(crate) fn maybe_grow(&mut self) {
60        if self.cap == 0 || (self.occupied + self.deleted) >= self.threshold() {
61            self.grow();
62        }
63    }
64
65    fn grow(&mut self) {
66        let new_cap = if self.cap == 0 {
67            MIN_CAP
68        } else {
69            self.cap.checked_mul(2).expect("kevy-map: capacity doubling overflow")
70        };
71        let mut new_table = Self::alloc_table(new_cap);
72        // Move every live entry over. After ptr::read'ing a slot we mark its
73        // metadata DELETED, so any subsequent Drop (incl. panic unwind) won't
74        // double-free; the old allocation will free with all-DELETED metadata.
75        //
76        // Only iterate the real slot range `[0, cap)`; the trailing mirror
77        // bytes are bookkeeping for SIMD-load wraparound, not real slots.
78        // Direct metadata writes are safe here because the old `self` table
79        // is going away (we swap with new_table then drop), so a stale mirror
80        // doesn't matter.
81        let old_cap = self.cap;
82        for i in 0..old_cap {
83            // SAFETY: i < old_cap ⇒ metadata in-bounds.
84            let meta = unsafe { *self.metadata_ptr.as_ptr().add(i) };
85            if meta & 0x80 == 0 {
86                // SAFETY: full slot ⇒ initialised; we mark DELETED immediately
87                // so this byte is never re-read as occupied.
88                let (k, v) = unsafe { ptr::read(self.slots_ptr.as_ptr().add(i) as *const (K, V)) };
89                unsafe { *self.metadata_ptr.as_ptr().add(i) = DELETED };
90                let hash = k.kevy_hash();
91                new_table.insert_known_unique(hash, k, v);
92            }
93        }
94        // All occupied entries are now in new_table; the old self has no live slots.
95        self.occupied = 0;
96        self.deleted = 0;
97        core::mem::swap(self, &mut new_table);
98        // new_table (now the old self) drops; metadata is all DELETED (or EMPTY
99        // for previously-empty slots) ⇒ Drop walks but touches no slots.
100    }
101
102    /// Insert under the assumption that the key isn't already present (used
103    /// by `grow` to repopulate the new table). Skips the duplicate-key
104    /// check. Uses a 16-slot SIMD group scan to find the first EMPTY.
105    fn insert_known_unique(&mut self, hash: u64, k: K, v: V) {
106        let h2v = h2(hash);
107        let mut group_start = (hash as usize) & self.mask;
108        loop {
109            // SAFETY: metadata is `cap + GROUP_WIDTH` bytes; group_start
110            // is in `[0, cap)`; the load reads 16 bytes which lie inside the
111            // buffer thanks to the mirror tail.
112            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
113            if let Some(m) = g.match_byte(EMPTY).lowest_set() {
114                let slot = (group_start + m) & self.mask;
115                self.set_meta(slot, h2v);
116                // SAFETY: slot < cap.
117                unsafe {
118                    (*self.slots_ptr.as_ptr().add(slot)).write((k, v));
119                }
120                self.occupied += 1;
121                return;
122            }
123            // Linear probing by GROUP_WIDTH (tried triangular — at our 7/8
124            // load factor and group-scan-aware probe, linear wins on cache
125            // locality; triangular's anti-clustering only pays off at higher
126            // load factors than we run).
127            group_start = (group_start + GROUP_WIDTH) & self.mask;
128        }
129    }
130
131    // LOC-WAIVER: per-op probe hot body — deliberate fast/slow loop pair (tombstone-free vs tracking).
132    fn probe_with_key(&self, hash: u64, key: &K) -> ProbeOutcome {
133        if self.cap == 0 {
134            return ProbeOutcome::NotFound { insert_at: 0, via_tombstone: false };
135        }
136        let h2v = h2(hash);
137        let mut group_start = (hash as usize) & self.mask;
138
139        // Fast path: no tombstones in the table ⇒ skip DELETED tracking
140        // entirely. This trims one SIMD `match_byte` (and one branch) from
141        // every group iteration; insert workloads with no deletions hit
142        // this path exclusively.
143        if self.deleted == 0 {
144            loop {
145                // SAFETY: see [insert_known_unique].
146                let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
147                for m in g.match_byte(h2v).iter() {
148                    let slot = (group_start + m) & self.mask;
149                    // SAFETY: matched h2 ⇒ slot is occupied ⇒ initialised.
150                    let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
151                    if &kv.0 == key {
152                        return ProbeOutcome::Found(slot);
153                    }
154                }
155                if let Some(m) = g.match_byte(EMPTY).lowest_set() {
156                    return ProbeOutcome::NotFound {
157                        insert_at: (group_start + m) & self.mask,
158                        via_tombstone: false,
159                    };
160                }
161                group_start = (group_start + GROUP_WIDTH) & self.mask;
162            }
163        }
164
165        // Slow path: tombstones exist; track the first DELETED so insert
166        // can reclaim it instead of growing the tombstone count.
167        let mut first_deleted: Option<usize> = None;
168        loop {
169            // SAFETY: see [insert_known_unique].
170            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
171            for m in g.match_byte(h2v).iter() {
172                let slot = (group_start + m) & self.mask;
173                // SAFETY: matched h2 ⇒ slot is occupied ⇒ initialised.
174                let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
175                if &kv.0 == key {
176                    return ProbeOutcome::Found(slot);
177                }
178            }
179            if first_deleted.is_none()
180                && let Some(m) = g.match_byte(DELETED).lowest_set()
181            {
182                first_deleted = Some((group_start + m) & self.mask);
183            }
184            if let Some(m) = g.match_byte(EMPTY).lowest_set() {
185                let probe_empty = (group_start + m) & self.mask;
186                return ProbeOutcome::NotFound {
187                    insert_at: first_deleted.unwrap_or(probe_empty),
188                    via_tombstone: first_deleted.is_some(),
189                };
190            }
191            group_start = (group_start + GROUP_WIDTH) & self.mask;
192        }
193    }
194}
195
196impl<K, V> KevyMap<K, V> {
197    /// Borrow the value for `key`, or `None` if absent.
198    /// # Examples
199    ///
200    /// ```
201    /// let mut m = kevy_map::KevyMap::new();
202    /// m.insert(b"k".to_vec(), 1u32);
203    /// // Borrowed lookup: a `&[u8]` finds a `Vec<u8>` key without allocating.
204    /// assert_eq!(m.get(b"k".as_slice()), Some(&1));
205    /// assert_eq!(m.get(b"nope".as_slice()), None);
206    /// ```
207    pub fn get<Q>(&self, key: &Q) -> Option<&V>
208    where
209        K: Borrow<Q>,
210        Q: KevyHash + Eq + ?Sized,
211    {
212        let idx = self.find_by_borrow(key)?;
213        // SAFETY: find_by_borrow only returns indices into full slots.
214        let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_ref() };
215        Some(&kv.1)
216    }
217
218    /// Mutably borrow the value for `key`, or `None` if absent.
219    /// # Examples
220    ///
221    /// ```
222    /// let mut m = kevy_map::KevyMap::new();
223    /// m.insert(b"k".to_vec(), 1u32);
224    /// if let Some(v) = m.get_mut(b"k".as_slice()) { *v += 41; }
225    /// assert_eq!(m.get(b"k".as_slice()), Some(&42));
226    /// ```
227    pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
228    where
229        K: Borrow<Q>,
230        Q: KevyHash + Eq + ?Sized,
231    {
232        let idx = self.find_by_borrow(key)?;
233        // SAFETY: full slot.
234        let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_mut() };
235        Some(&mut kv.1)
236    }
237
238    /// Whether `key` is present in the map.
239    /// # Examples
240    ///
241    /// ```
242    /// let mut m = kevy_map::KevyMap::new();
243    /// m.insert(b"k".to_vec(), ());
244    /// assert!(m.contains_key(b"k".as_slice()));
245    /// assert!(!m.contains_key(b"j".as_slice()));
246    /// ```
247    pub fn contains_key<Q>(&self, key: &Q) -> bool
248    where
249        K: Borrow<Q>,
250        Q: KevyHash + Eq + ?Sized,
251    {
252        self.find_by_borrow(key).is_some()
253    }
254
255    /// Remove `key`'s entry; returns the previous value if present.
256    /// # Examples
257    ///
258    /// ```
259    /// let mut m = kevy_map::KevyMap::new();
260    /// m.insert(b"k".to_vec(), 7u32);
261    /// assert_eq!(m.remove(b"k".as_slice()), Some(7), "the value comes back");
262    /// assert_eq!(m.remove(b"k".as_slice()), None, "absent — None, not a panic");
263    /// assert!(m.is_empty());
264    /// ```
265    pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
266    where
267        K: Borrow<Q>,
268        Q: KevyHash + Eq + ?Sized,
269    {
270        let idx = self.find_by_borrow(key)?;
271        self.set_meta(idx, DELETED);
272        self.occupied -= 1;
273        self.deleted += 1;
274        // SAFETY: slot was full, we just marked it DELETED so it won't be
275        // read again; ptr::read moves the (K, V) out.
276        let (_k, v) = unsafe { ptr::read(self.slots_ptr.as_ptr().add(idx) as *const (K, V)) };
277        Some(v)
278    }
279
280    pub(crate) fn find_by_borrow<Q>(&self, key: &Q) -> Option<usize>
281    where
282        K: Borrow<Q>,
283        Q: KevyHash + Eq + ?Sized,
284    {
285        if self.cap == 0 {
286            return None;
287        }
288        let hash = key.kevy_hash();
289        let h2v = h2(hash);
290        let mut group_start = (hash as usize) & self.mask;
291        loop {
292            // SAFETY: see [insert_known_unique]; group_start ∈ [0, cap),
293            // metadata length ≥ cap + GROUP_WIDTH.
294            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
295            for m in g.match_byte(h2v).iter() {
296                let slot = (group_start + m) & self.mask;
297                // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
298                let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
299                if kv.0.borrow() == key {
300                    return Some(slot);
301                }
302            }
303            // EMPTY in this group ⇒ key cannot be later in the probe.
304            if !g.match_byte(EMPTY).is_empty() {
305                return None;
306            }
307            group_start = (group_start + GROUP_WIDTH) & self.mask;
308        }
309    }
310
311    /// Full probe: returns `Found(idx)` if `key` is present, else
312    /// `NotFound { insert_at, via_tombstone }` describing the slot a future
313    /// insert would take. Mirrors [`probe_with_key`](Self::probe_with_key)
314    /// but accepts a `Borrow<Q>` key.
315    ///
316    /// Used by the [`raw_entry_mut`](Self::raw_entry_mut) API to fuse a read
317    /// and a possible insert into a single probe.
318    pub(crate) fn probe_by_borrow<Q>(&self, key: &Q) -> ProbeOutcome
319    where
320        K: Borrow<Q>,
321        Q: KevyHash + Eq + ?Sized,
322    {
323        if self.cap == 0 {
324            return ProbeOutcome::NotFound { insert_at: 0, via_tombstone: false };
325        }
326        let hash = key.kevy_hash();
327        let h2v = h2(hash);
328        let group_start = (hash as usize) & self.mask;
329        if self.deleted == 0 {
330            self.probe_by_borrow_fast(key, h2v, group_start)
331        } else {
332            self.probe_by_borrow_slow(key, h2v, group_start)
333        }
334    }
335
336    /// Fast path for `probe_by_borrow`: no tombstones in the table, so we
337    /// can stop tracking DELETED slots entirely.
338    fn probe_by_borrow_fast<Q>(&self, key: &Q, h2v: u8, mut group_start: usize) -> ProbeOutcome
339    where
340        K: Borrow<Q>,
341        Q: KevyHash + Eq + ?Sized,
342    {
343        loop {
344            // SAFETY: see [insert_known_unique].
345            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
346            for m in g.match_byte(h2v).iter() {
347                let slot = (group_start + m) & self.mask;
348                // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
349                let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
350                if kv.0.borrow() == key {
351                    return ProbeOutcome::Found(slot);
352                }
353            }
354            if let Some(m) = g.match_byte(EMPTY).lowest_set() {
355                return ProbeOutcome::NotFound {
356                    insert_at: (group_start + m) & self.mask,
357                    via_tombstone: false,
358                };
359            }
360            group_start = (group_start + GROUP_WIDTH) & self.mask;
361        }
362    }
363
364    /// Slow path for `probe_by_borrow`: tombstones present; remember the
365    /// first DELETED so a later insert can reclaim it.
366    fn probe_by_borrow_slow<Q>(&self, key: &Q, h2v: u8, mut group_start: usize) -> ProbeOutcome
367    where
368        K: Borrow<Q>,
369        Q: KevyHash + Eq + ?Sized,
370    {
371        let mut first_deleted: Option<usize> = None;
372        loop {
373            // SAFETY: see [insert_known_unique].
374            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
375            for m in g.match_byte(h2v).iter() {
376                let slot = (group_start + m) & self.mask;
377                // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
378                let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
379                if kv.0.borrow() == key {
380                    return ProbeOutcome::Found(slot);
381                }
382            }
383            if first_deleted.is_none()
384                && let Some(m) = g.match_byte(DELETED).lowest_set()
385            {
386                first_deleted = Some((group_start + m) & self.mask);
387            }
388            if let Some(m) = g.match_byte(EMPTY).lowest_set() {
389                let probe_empty = (group_start + m) & self.mask;
390                return ProbeOutcome::NotFound {
391                    insert_at: first_deleted.unwrap_or(probe_empty),
392                    via_tombstone: first_deleted.is_some(),
393                };
394            }
395            group_start = (group_start + GROUP_WIDTH) & self.mask;
396        }
397    }
398}