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) as *mut (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 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_map = 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_map 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_map.insert_known_unique(hash, k, v);
89 }
90 }
91 // All occupied entries are now in new_map; the old self has no live slots.
92 self.occupied = 0;
93 self.deleted = 0;
94 std::mem::swap(self, &mut new_map);
95 // new_map (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 fn probe_with_key(&self, hash: u64, key: &K) -> ProbeOutcome {
129 if self.cap == 0 {
130 return ProbeOutcome::NotFound {
131 insert_at: 0,
132 via_tombstone: false,
133 };
134 }
135 let h2v = h2(hash);
136 let mut group_start = (hash as usize) & self.mask;
137
138 // Fast path: no tombstones in the table ⇒ skip DELETED tracking
139 // entirely. This trims one SIMD `match_byte` (and one branch) from
140 // every group iteration; insert workloads with no deletions hit
141 // this path exclusively.
142 if self.deleted == 0 {
143 loop {
144 // SAFETY: see [insert_known_unique].
145 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
146 for m in g.match_byte(h2v).iter() {
147 let slot = (group_start + m) & self.mask;
148 // SAFETY: matched h2 ⇒ slot is occupied ⇒ initialised.
149 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
150 if &kv.0 == key {
151 return ProbeOutcome::Found(slot);
152 }
153 }
154 if let Some(m) = g.match_byte(EMPTY).lowest_set() {
155 return ProbeOutcome::NotFound {
156 insert_at: (group_start + m) & self.mask,
157 via_tombstone: false,
158 };
159 }
160 group_start = (group_start + GROUP_WIDTH) & self.mask;
161 }
162 }
163
164 // Slow path: tombstones exist; track the first DELETED so insert
165 // can reclaim it instead of growing the tombstone count.
166 let mut first_deleted: Option<usize> = None;
167 loop {
168 // SAFETY: see [insert_known_unique].
169 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
170 for m in g.match_byte(h2v).iter() {
171 let slot = (group_start + m) & self.mask;
172 // SAFETY: matched h2 ⇒ slot is occupied ⇒ initialised.
173 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
174 if &kv.0 == key {
175 return ProbeOutcome::Found(slot);
176 }
177 }
178 if first_deleted.is_none()
179 && let Some(m) = g.match_byte(DELETED).lowest_set()
180 {
181 first_deleted = Some((group_start + m) & self.mask);
182 }
183 if let Some(m) = g.match_byte(EMPTY).lowest_set() {
184 let probe_empty = (group_start + m) & self.mask;
185 return ProbeOutcome::NotFound {
186 insert_at: first_deleted.unwrap_or(probe_empty),
187 via_tombstone: first_deleted.is_some(),
188 };
189 }
190 group_start = (group_start + GROUP_WIDTH) & self.mask;
191 }
192 }
193}
194
195impl<K, V> KevyMap<K, V> {
196 /// Borrow the value for `key`, or `None` if absent.
197 pub fn get<Q>(&self, key: &Q) -> Option<&V>
198 where
199 K: Borrow<Q>,
200 Q: KevyHash + Eq + ?Sized,
201 {
202 let idx = self.find_by_borrow(key)?;
203 // SAFETY: find_by_borrow only returns indices into full slots.
204 let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_ref() };
205 Some(&kv.1)
206 }
207
208 /// Mutably borrow the value for `key`, or `None` if absent.
209 pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
210 where
211 K: Borrow<Q>,
212 Q: KevyHash + Eq + ?Sized,
213 {
214 let idx = self.find_by_borrow(key)?;
215 // SAFETY: full slot.
216 let kv = unsafe { (*self.slots_ptr.as_ptr().add(idx)).assume_init_mut() };
217 Some(&mut kv.1)
218 }
219
220 /// Whether `key` is present in the map.
221 pub fn contains_key<Q>(&self, key: &Q) -> bool
222 where
223 K: Borrow<Q>,
224 Q: KevyHash + Eq + ?Sized,
225 {
226 self.find_by_borrow(key).is_some()
227 }
228
229 /// Remove `key`'s entry; returns the previous value if present.
230 pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
231 where
232 K: Borrow<Q>,
233 Q: KevyHash + Eq + ?Sized,
234 {
235 let idx = self.find_by_borrow(key)?;
236 self.set_meta(idx, DELETED);
237 self.occupied -= 1;
238 self.deleted += 1;
239 // SAFETY: slot was full, we just marked it DELETED so it won't be
240 // read again; ptr::read moves the (K, V) out.
241 let (_k, v) = unsafe { ptr::read(self.slots_ptr.as_ptr().add(idx) as *const (K, V)) };
242 Some(v)
243 }
244
245 fn find_by_borrow<Q>(&self, key: &Q) -> Option<usize>
246 where
247 K: Borrow<Q>,
248 Q: KevyHash + Eq + ?Sized,
249 {
250 if self.cap == 0 {
251 return None;
252 }
253 let hash = key.kevy_hash();
254 let h2v = h2(hash);
255 let mut group_start = (hash as usize) & self.mask;
256 loop {
257 // SAFETY: see [insert_known_unique]; group_start ∈ [0, cap),
258 // metadata length ≥ cap + GROUP_WIDTH.
259 let g = unsafe { Group::load(self.metadata_ptr.as_ptr().add(group_start)) };
260 for m in g.match_byte(h2v).iter() {
261 let slot = (group_start + m) & self.mask;
262 // SAFETY: matched h2 ⇒ slot occupied ⇒ initialised.
263 let kv = unsafe { (*self.slots_ptr.as_ptr().add(slot)).assume_init_ref() };
264 if kv.0.borrow() == key {
265 return Some(slot);
266 }
267 }
268 // EMPTY in this group ⇒ key cannot be later in the probe.
269 if !g.match_byte(EMPTY).is_empty() {
270 return None;
271 }
272 group_start = (group_start + GROUP_WIDTH) & self.mask;
273 }
274 }
275}