Skip to main content

kevy_map/
raw_entry.rs

1//! Single-probe entry API à la hashbrown's `RawEntryMut`.
2//!
3//! Motivation: the existing read/insert APIs (`get`, `get_mut`, `insert`,
4//! `remove`) each cost one full probe. Common Store patterns —
5//! "look up, check expiry, conditionally remove, otherwise return the
6//! borrow" — currently do **two** probes because the borrow returned by
7//! `get` cannot survive a subsequent `remove` (mutable-borrow conflict
8//! with the immutable borrow on the value). The raw-entry API folds the
9//! read and the conditional remove into a single probe by consuming the
10//! `RawOccupiedEntryMut` on `remove(self)`, which releases the borrow at
11//! the call site.
12//!
13//! Design notes:
14//!
15//! * `raw_entry_mut` takes `&mut self` (any subsequent insert/remove
16//!   needs exclusive access; matching `Borrow<Q>` lookup matches the
17//!   shape of `get_mut`).
18//! * The `RawOccupiedEntryMut` stores a borrowed `&'a mut KevyMap<K, V>`
19//!   plus the slot index already located by the probe. `get` /
20//!   `get_mut` / `into_mut` reuse that slot; `remove(self)` consumes the
21//!   entry so the map borrow is freed and a `set_meta(DELETED)` write
22//!   can proceed.
23//! * The `RawVacantEntryMut` stores `&'a mut KevyMap<K, V>` and **does
24//!   not** cache the probe's `insert_at`: any subsequent mutation
25//!   (notably `maybe_grow` inside `insert`) can invalidate that slot.
26//!   `insert(self, k, v)` therefore re-runs `insert` from scratch
27//!   (one extra probe in the absent-key insert path; the API-additive
28//!   commit is purely about unblocking the *read or remove* fast path —
29//!   downstream cumulative attacks may later push the cached probe
30//!   through, once the grow-invalidation issue is solved with an
31//!   explicit `reserve` API).
32//!
33//! No `unsafe` is added by this file: every memory touch is delegated
34//! to the existing `pub(crate)` helpers in `map.rs` / `map_keyed.rs`.
35
36use core::borrow::Borrow;
37use core::ptr;
38
39use kevy_hash::KevyHash;
40
41use crate::map::{DELETED, KevyMap, ProbeOutcome};
42
43/// Result of [`KevyMap::raw_entry_mut`].
44///
45/// Mirrors `hashbrown::hash_map::RawEntryMut`: an `Occupied` arm grants
46/// read / mutate / consume access to the existing entry; a `Vacant` arm
47/// can be filled with `insert(k, v)`. The defining property — and the
48/// reason this API exists distinct from `get_mut` — is that
49/// [`RawOccupiedEntryMut::remove`] consumes `self`, which releases the
50/// outstanding borrow on the map and lets the caller perform the
51/// deletion within the same borrow scope.
52///
53/// ```
54/// use kevy_map::{KevyMap, RawEntryMut};
55/// let mut m: KevyMap<u64, u64> = KevyMap::new();
56/// m.insert(1, 0); // 0 marks an expired value
57/// // look up and drop the expired entry with one probe
58/// if let RawEntryMut::Occupied(e) = m.raw_entry_mut(&1) {
59///     if *e.get() == 0 {
60///         e.remove();
61///     }
62/// }
63/// assert!(m.is_empty());
64/// ```
65#[derive(Debug)]
66pub enum RawEntryMut<'a, K, V> {
67    /// The key was present; gives read / mutate / consume access.
68    ///
69    /// ```
70    /// use kevy_map::{KevyMap, RawEntryMut};
71    /// let mut m: KevyMap<u64, u64> = KevyMap::new();
72    /// m.insert(7, 1);
73    /// let RawEntryMut::Occupied(mut e) = m.raw_entry_mut(&7) else { panic!() };
74    /// *e.get_mut() += 1;
75    /// assert_eq!(m.get(&7), Some(&2));
76    /// ```
77    Occupied(RawOccupiedEntryMut<'a, K, V>),
78    /// The key was absent; `insert(k, v)` writes a new entry.
79    ///
80    /// ```
81    /// use kevy_map::{KevyMap, RawEntryMut};
82    /// let mut m: KevyMap<u64, u64> = KevyMap::new();
83    /// let RawEntryMut::Vacant(e) = m.raw_entry_mut(&7) else { panic!() };
84    /// *e.insert(7, 1) += 1;
85    /// assert_eq!(m.get(&7), Some(&2));
86    /// ```
87    Vacant(RawVacantEntryMut<'a, K, V>),
88}
89
90/// Handle to an existing entry, returned by [`RawEntryMut::Occupied`].
91///
92/// ```
93/// use kevy_map::{KevyMap, RawEntryMut};
94/// let mut m: KevyMap<u64, &str> = KevyMap::new();
95/// m.insert(1, "one");
96/// let RawEntryMut::Occupied(e) = m.raw_entry_mut(&1) else { panic!() };
97/// assert_eq!((*e.key(), *e.get()), (1, "one"));
98/// assert_eq!(e.remove(), "one");
99/// assert_eq!(m.get(&1), None);
100/// ```
101#[derive(Debug)]
102pub struct RawOccupiedEntryMut<'a, K, V> {
103    map: &'a mut KevyMap<K, V>,
104    /// Slot index inside `map.slots_ptr` for the located entry.
105    slot: usize,
106}
107
108/// Handle to an absent entry, returned by [`RawEntryMut::Vacant`].
109///
110/// ```
111/// use kevy_map::{KevyMap, RawEntryMut};
112/// let mut m: KevyMap<Vec<u8>, u64> = KevyMap::new();
113/// let RawEntryMut::Vacant(e) = m.raw_entry_mut(b"k".as_slice()) else { panic!() };
114/// e.insert(b"k".to_vec(), 3);
115/// assert_eq!(m.get(b"k".as_slice()), Some(&3));
116/// ```
117#[derive(Debug)]
118pub struct RawVacantEntryMut<'a, K, V> {
119    map: &'a mut KevyMap<K, V>,
120}
121
122impl<K, V> KevyMap<K, V> {
123    /// Look up `key`; return an [`RawEntryMut`] giving single-probe
124    /// access to the located (or vacant) slot.
125    ///
126    /// One full probe. The returned handle borrows `self` mutably; the
127    /// borrow is released only when the handle is dropped (or consumed
128    /// via [`RawOccupiedEntryMut::remove`] / [`RawOccupiedEntryMut::into_mut`]).
129    pub fn raw_entry_mut<Q>(&mut self, key: &Q) -> RawEntryMut<'_, K, V>
130    where
131        K: Borrow<Q> + KevyHash + Eq,
132        Q: KevyHash + Eq + ?Sized,
133    {
134        match self.probe_by_borrow(key) {
135            ProbeOutcome::Found(slot) => {
136                RawEntryMut::Occupied(RawOccupiedEntryMut { map: self, slot })
137            }
138            ProbeOutcome::NotFound { .. } => RawEntryMut::Vacant(RawVacantEntryMut { map: self }),
139        }
140    }
141}
142
143impl<'a, K, V> RawOccupiedEntryMut<'a, K, V> {
144    /// Shared access to the stored value.
145    #[inline]
146    pub fn get(&self) -> &V {
147        // SAFETY: `slot` came from `probe_by_borrow::Found`, which only
148        // returns indices into full slots.
149        let kv = unsafe { (*self.map.slots_ptr.as_ptr().add(self.slot)).assume_init_ref() };
150        &kv.1
151    }
152
153    /// Mutable access to the stored value (borrow tied to `&mut self`).
154    #[inline]
155    pub fn get_mut(&mut self) -> &mut V {
156        // SAFETY: see [`get`].
157        let kv = unsafe { (*self.map.slots_ptr.as_ptr().add(self.slot)).assume_init_mut() };
158        &mut kv.1
159    }
160
161    /// Mutable access to the stored value with the outer map's lifetime,
162    /// consuming the handle. The map borrow returned outlives `self`,
163    /// which is exactly the shape `get`+`get_mut` cannot provide.
164    #[inline]
165    pub fn into_mut(self) -> &'a mut V {
166        // SAFETY: see [`get`]. The returned borrow's lifetime `'a` is
167        // tied to the borrow we were constructed with, which is the
168        // caller's `&mut KevyMap<K, V>` borrow.
169        let kv = unsafe { (*self.map.slots_ptr.as_ptr().add(self.slot)).assume_init_mut() };
170        &mut kv.1
171    }
172
173    /// Shared access to the stored key.
174    #[inline]
175    pub fn key(&self) -> &K {
176        // SAFETY: see [`get`].
177        let kv = unsafe { (*self.map.slots_ptr.as_ptr().add(self.slot)).assume_init_ref() };
178        &kv.0
179    }
180
181    /// Remove the entry; returns the previous value. Consumes `self`,
182    /// releasing the map borrow so the caller can immediately re-probe
183    /// or mutate something else.
184    ///
185    /// This is the load-bearing method — it is what makes the API
186    /// strictly more expressive than `get_mut`+`remove`.
187    pub fn remove(self) -> V {
188        self.map.set_meta(self.slot, DELETED);
189        self.map.occupied -= 1;
190        self.map.deleted += 1;
191        // SAFETY: slot was full; we just marked it DELETED so it won't
192        // be re-read as occupied. `ptr::read` moves the (K, V) out;
193        // dropping `k` here is correct because we don't return it.
194        let (_k, v) =
195            unsafe { ptr::read(self.map.slots_ptr.as_ptr().add(self.slot) as *const (K, V)) };
196        v
197    }
198}
199
200impl<'a, K, V> RawVacantEntryMut<'a, K, V>
201where
202    K: KevyHash + Eq,
203{
204    /// Insert `(key, value)` and return a mutable borrow of the freshly
205    /// inserted value. The borrow is tied to the original map borrow.
206    ///
207    /// Note: this performs a second probe after a possible grow, because
208    /// the slot located by the first probe (the one that produced
209    /// `Vacant`) can be invalidated by `maybe_grow`. The added probe is
210    /// acceptable for the live_entry pattern (the *read* fast path is
211    /// the one we were chasing; the absent-key insert path is the cold
212    /// side, and `live_entry` itself never takes it).
213    pub fn insert(self, key: K, value: V) -> &'a mut V {
214        // Grow if needed (matches `KevyMap::insert`'s preamble).
215        self.map.maybe_grow();
216        // Re-probe by reference, write into the slot, bump occupancy.
217        let hash = key.kevy_hash();
218        let outcome = self.map.probe_by_borrow(&key);
219        let slot = match outcome {
220            ProbeOutcome::NotFound { insert_at, via_tombstone } => {
221                self.map.set_meta(insert_at, crate::map::h2(hash));
222                // SAFETY: insert_at < cap ⇒ slot pointer in-bounds; we
223                // write (K, V) into a previously uninitialised slot.
224                unsafe {
225                    (*self.map.slots_ptr.as_ptr().add(insert_at)).write((key, value));
226                }
227                self.map.occupied += 1;
228                if via_tombstone {
229                    self.map.deleted -= 1;
230                }
231                insert_at
232            }
233            ProbeOutcome::Found(_) => {
234                // Cannot happen: we held the only mutable borrow on the
235                // map between `raw_entry_mut` and here, and the first
236                // probe said Vacant. Treat as logic bug rather than
237                // overwriting (overwriting would silently change the
238                // documented contract).
239                unreachable!("raw vacant insert observed an existing key");
240            }
241        };
242        // SAFETY: slot is the one we just initialised.
243        let kv = unsafe { (*self.map.slots_ptr.as_ptr().add(slot)).assume_init_mut() };
244        &mut kv.1
245    }
246}