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) 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}