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 std::borrow::Borrow;
9use std::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    pub fn insert(&mut self, key: K, value: V) -> Option<V> {
21        self.maybe_grow();
22        let hash = key.kevy_hash();
23        match self.probe_with_key(hash, &key) {
24            ProbeOutcome::Found(idx) => {
25                // SAFETY: slot is full ⇒ initialised. We replace only the V
26                // field; the old K is kept (std HashMap semantics).
27                let v_ptr = unsafe {
28                    let kv: *mut (K, V) = self.slots_ptr.as_ptr().add(idx).cast::<(K, V)>();
29                    ptr::addr_of_mut!((*kv).1)
30                };
31                let old_v = unsafe { ptr::replace(v_ptr, value) };
32                drop(key);
33                Some(old_v)
34            }
35            ProbeOutcome::NotFound {
36                insert_at,
37                via_tombstone,
38            } => {
39                self.set_meta(insert_at, h2(hash));
40                // SAFETY: insert_at < cap ⇒ slot pointer in-bounds; we write
41                // (K, V) into a previously uninitialised slot.
42                unsafe {
43                    (*self.slots_ptr.as_ptr().add(insert_at)).write((key, value));
44                }
45                self.occupied += 1;
46                if via_tombstone {
47                    self.deleted -= 1;
48                }
49                None
50            }
51        }
52    }
53
54    pub(crate) fn maybe_grow(&mut self) {
55        if self.cap == 0 || (self.occupied + self.deleted) >= self.threshold() {
56            self.grow();
57        }
58    }
59
60    fn grow(&mut self) {
61        let new_cap = if self.cap == 0 {
62            MIN_CAP
63        } else {
64            self.cap
65                .checked_mul(2)
66                .expect("kevy-map: capacity doubling overflow")
67        };
68        let mut new_table = Self::alloc_table(new_cap);
69        // Move every live entry over. After ptr::read'ing a slot we mark its
70        // metadata DELETED, so any subsequent Drop (incl. panic unwind) won't
71        // double-free; the old allocation will free with all-DELETED metadata.
72        //
73        // Only iterate the real slot range `[0, cap)`; the trailing mirror
74        // bytes are bookkeeping for SIMD-load wraparound, not real slots.
75        // Direct metadata writes are safe here because the old `self` table
76        // is going away (we swap with new_table then drop), so a stale mirror
77        // doesn't matter.
78        let old_cap = self.cap;
79        for i in 0..old_cap {
80            // SAFETY: i < old_cap ⇒ metadata in-bounds.
81            let meta = unsafe { *self.metadata_ptr.as_ptr().add(i) };
82            if meta & 0x80 == 0 {
83                // SAFETY: full slot ⇒ initialised; we mark DELETED immediately
84                // so this byte is never re-read as occupied.
85                let (k, v) = unsafe { ptr::read(self.slots_ptr.as_ptr().add(i) as *const (K, V)) };
86                unsafe { *self.metadata_ptr.as_ptr().add(i) = DELETED };
87                let hash = k.kevy_hash();
88                new_table.insert_known_unique(hash, k, v);
89            }
90        }
91        // All occupied entries are now in new_table; the old self has no live slots.
92        self.occupied = 0;
93        self.deleted = 0;
94        std::mem::swap(self, &mut new_table);
95        // new_table (now the old self) drops; metadata is all DELETED (or EMPTY
96        // for previously-empty slots) ⇒ Drop walks but touches no slots.
97    }
98
99    /// Insert under the assumption that the key isn't already present (used
100    /// by `grow` to repopulate the new table). Skips the duplicate-key
101    /// check. Uses a 16-slot SIMD group scan to find the first EMPTY.
102    fn insert_known_unique(&mut self, hash: u64, k: K, v: V) {
103        let h2v = h2(hash);
104        let mut group_start = (hash as usize) & self.mask;
105        loop {
106            // SAFETY: metadata is `cap + GROUP_WIDTH` bytes; group_start
107            // is in `[0, cap)`; the load reads 16 bytes which lie inside the
108            // buffer thanks to the mirror tail.
109            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
110            if let Some(m) = g.match_byte(EMPTY).lowest_set() {
111                let slot = (group_start + m) & self.mask;
112                self.set_meta(slot, h2v);
113                // SAFETY: slot < cap.
114                unsafe {
115                    (*self.slots_ptr.as_ptr().add(slot)).write((k, v));
116                }
117                self.occupied += 1;
118                return;
119            }
120            // Linear probing by GROUP_WIDTH (tried triangular — at our 7/8
121            // load factor and group-scan-aware probe, linear wins on cache
122            // locality; triangular's anti-clustering only pays off at higher
123            // load factors than we run).
124            group_start = (group_start + GROUP_WIDTH) & self.mask;
125        }
126    }
127
128    // LOC-WAIVER: per-op probe hot body — deliberate fast/slow loop pair (tombstone-free vs tracking).
129    fn probe_with_key(&self, hash: u64, key: &K) -> ProbeOutcome {
130        if self.cap == 0 {
131            return ProbeOutcome::NotFound {
132                insert_at: 0,
133                via_tombstone: false,
134            };
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    pub fn get<Q>(&self, key: &Q) -> Option<&V>
199    where
200        K: Borrow<Q>,
201        Q: KevyHash + Eq + ?Sized,
202    {
203        let idx = self.find_by_borrow(key)?;
204        // SAFETY: find_by_borrow only returns indices into full slots.
205        let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_ref() };
206        Some(&kv.1)
207    }
208
209    /// Mutably borrow the value for `key`, or `None` if absent.
210    pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
211    where
212        K: Borrow<Q>,
213        Q: KevyHash + Eq + ?Sized,
214    {
215        let idx = self.find_by_borrow(key)?;
216        // SAFETY: full slot.
217        let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_mut() };
218        Some(&mut kv.1)
219    }
220
221    /// Whether `key` is present in the map.
222    pub fn contains_key<Q>(&self, key: &Q) -> bool
223    where
224        K: Borrow<Q>,
225        Q: KevyHash + Eq + ?Sized,
226    {
227        self.find_by_borrow(key).is_some()
228    }
229
230    /// Remove `key`'s entry; returns the previous value if present.
231    pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
232    where
233        K: Borrow<Q>,
234        Q: KevyHash + Eq + ?Sized,
235    {
236        let idx = self.find_by_borrow(key)?;
237        self.set_meta(idx, DELETED);
238        self.occupied -= 1;
239        self.deleted += 1;
240        // SAFETY: slot was full, we just marked it DELETED so it won't be
241        // read again; ptr::read moves the (K, V) out.
242        let (_k, v) = unsafe { ptr::read(self.slots_ptr.as_ptr().add(idx) as *const (K, V)) };
243        Some(v)
244    }
245
246    pub(crate) fn find_by_borrow<Q>(&self, key: &Q) -> Option<usize>
247    where
248        K: Borrow<Q>,
249        Q: KevyHash + Eq + ?Sized,
250    {
251        if self.cap == 0 {
252            return None;
253        }
254        let hash = key.kevy_hash();
255        let h2v = h2(hash);
256        let mut group_start = (hash as usize) & self.mask;
257        loop {
258            // SAFETY: see [insert_known_unique]; group_start ∈ [0, cap),
259            // metadata length ≥ cap + GROUP_WIDTH.
260            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
261            for m in g.match_byte(h2v).iter() {
262                let slot = (group_start + m) & self.mask;
263                // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
264                let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
265                if kv.0.borrow() == key {
266                    return Some(slot);
267                }
268            }
269            // EMPTY in this group ⇒ key cannot be later in the probe.
270            if !g.match_byte(EMPTY).is_empty() {
271                return None;
272            }
273            group_start = (group_start + GROUP_WIDTH) & self.mask;
274        }
275    }
276
277    /// Full probe: returns `Found(idx)` if `key` is present, else
278    /// `NotFound { insert_at, via_tombstone }` describing the slot a future
279    /// insert would take. Mirrors [`probe_with_key`](Self::probe_with_key)
280    /// but accepts a `Borrow<Q>` key.
281    ///
282    /// Used by the [`raw_entry_mut`](Self::raw_entry_mut) API to fuse a read
283    /// and a possible insert into a single probe.
284    pub(crate) fn probe_by_borrow<Q>(&self, key: &Q) -> ProbeOutcome
285    where
286        K: Borrow<Q>,
287        Q: KevyHash + Eq + ?Sized,
288    {
289        if self.cap == 0 {
290            return ProbeOutcome::NotFound {
291                insert_at: 0,
292                via_tombstone: false,
293            };
294        }
295        let hash = key.kevy_hash();
296        let h2v = h2(hash);
297        let group_start = (hash as usize) & self.mask;
298        if self.deleted == 0 {
299            self.probe_by_borrow_fast(key, h2v, group_start)
300        } else {
301            self.probe_by_borrow_slow(key, h2v, group_start)
302        }
303    }
304
305    /// Fast path for `probe_by_borrow`: no tombstones in the table, so we
306    /// can stop tracking DELETED slots entirely.
307    fn probe_by_borrow_fast<Q>(&self, key: &Q, h2v: u8, mut group_start: usize) -> ProbeOutcome
308    where
309        K: Borrow<Q>,
310        Q: KevyHash + Eq + ?Sized,
311    {
312        loop {
313            // SAFETY: see [insert_known_unique].
314            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
315            for m in g.match_byte(h2v).iter() {
316                let slot = (group_start + m) & self.mask;
317                // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
318                let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
319                if kv.0.borrow() == key {
320                    return ProbeOutcome::Found(slot);
321                }
322            }
323            if let Some(m) = g.match_byte(EMPTY).lowest_set() {
324                return ProbeOutcome::NotFound {
325                    insert_at: (group_start + m) & self.mask,
326                    via_tombstone: false,
327                };
328            }
329            group_start = (group_start + GROUP_WIDTH) & self.mask;
330        }
331    }
332
333    /// Slow path for `probe_by_borrow`: tombstones present; remember the
334    /// first DELETED so a later insert can reclaim it.
335    fn probe_by_borrow_slow<Q>(&self, key: &Q, h2v: u8, mut group_start: usize) -> ProbeOutcome
336    where
337        K: Borrow<Q>,
338        Q: KevyHash + Eq + ?Sized,
339    {
340        let mut first_deleted: Option<usize> = None;
341        loop {
342            // SAFETY: see [insert_known_unique].
343            let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
344            for m in g.match_byte(h2v).iter() {
345                let slot = (group_start + m) & self.mask;
346                // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
347                let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
348                if kv.0.borrow() == key {
349                    return ProbeOutcome::Found(slot);
350                }
351            }
352            if first_deleted.is_none()
353                && let Some(m) = g.match_byte(DELETED).lowest_set()
354            {
355                first_deleted = Some((group_start + m) & self.mask);
356            }
357            if let Some(m) = g.match_byte(EMPTY).lowest_set() {
358                let probe_empty = (group_start + m) & self.mask;
359                return ProbeOutcome::NotFound {
360                    insert_at: first_deleted.unwrap_or(probe_empty),
361                    via_tombstone: first_deleted.is_some(),
362                };
363            }
364            group_start = (group_start + GROUP_WIDTH) & self.mask;
365        }
366    }
367}