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}