memo_cache/
lib.rs

1#![no_std]
2
3use core::{borrow::Borrow, cell::Cell};
4
5/// Key equivalence trait, to support `Borrow` types as keys.
6trait Equivalent<K: ?Sized> {
7    /// Returns `true` if two values are equivalent, `false` otherwise.
8    fn equivalent(&self, k: &K) -> bool;
9}
10
11impl<Q: ?Sized, K: ?Sized> Equivalent<K> for Q
12where
13    Q: Eq,
14    K: Borrow<Q>,
15{
16    fn equivalent(&self, k: &K) -> bool {
17        self == k.borrow()
18    }
19}
20
21/// A single key/value slot used in the cache.
22#[derive(Clone, PartialEq)]
23enum KeyValueSlot<K, V> {
24    Used { key: K, value: V, hits: Cell<u8> },
25    Empty,
26}
27
28impl<K, V> KeyValueSlot<K, V> {
29    /// Check a used slot key for equivalence.
30    ///
31    /// Returns `true` for used slots with key equivalence, `false` if otherwise.
32    ///
33    #[cfg_attr(feature = "inline-more", inline)]
34    fn is_key<Q>(&self, k: &Q) -> bool
35    where
36        Q: Equivalent<K> + ?Sized,
37    {
38        if let KeyValueSlot::Used { key, .. } = self {
39            k.equivalent(key)
40        } else {
41            false
42        }
43    }
44
45    /// Get the value of a used slot.
46    #[cfg_attr(feature = "inline-more", inline)]
47    fn get_value(&self) -> Option<&V> {
48        if let KeyValueSlot::Used { value, hits, .. } = self {
49            hits.set(hits.get().saturating_add(1));
50            Some(value)
51        } else {
52            None
53        }
54    }
55
56    /// Get the value of a used slot (for mutation).
57    #[cfg_attr(feature = "inline-more", inline)]
58    fn get_value_mut(&mut self) -> Option<&mut V> {
59        if let KeyValueSlot::Used { value, hits, .. } = self {
60            hits.set(hits.get().saturating_add(1));
61            Some(value)
62        } else {
63            None
64        }
65    }
66
67    /// Get the number of cache hits for this slot.
68    fn hits(&self) -> u8 {
69        if let KeyValueSlot::Used { hits, .. } = self {
70            hits.get()
71        } else {
72            0
73        }
74    }
75
76    /// Update the value of a used slot (will be a no-op for empty slots).
77    #[cfg_attr(feature = "inline-more", inline)]
78    fn update_value(&mut self, v: V) {
79        if let KeyValueSlot::Used { value, .. } = self {
80            *value = v;
81        }
82    }
83}
84
85/// A small, fixed-size, heap-allocated key/value cache with retention management.
86///
87/// # Thread Safety
88///
89/// `MemoCache` is `Send` but not `Sync`. It can be moved between threads but cannot
90/// be shared across threads via shared references (`&MemoCache`).
91///
92/// This is because the cache uses interior mutability (via [`Cell`]) for tracking hit counts
93/// and random number generation, which is not thread-safe.
94///
95/// For concurrent access, wrap it in a synchronization primitive. E.g.:
96///
97/// ```
98/// use std::sync::Mutex;
99/// use memo_cache::MemoCache;
100///
101/// let cache = Mutex::new(MemoCache::<u32, String, 8>::new());
102///
103/// // In thread 1:
104/// cache.lock().unwrap().insert(42, "value".to_string());
105///
106/// // In thread 2:
107/// let value = cache.lock().unwrap().get(&42);
108/// ```
109pub struct MemoCache<K, V, const SIZE: usize> {
110    buffer: [KeyValueSlot<K, V>; SIZE],
111    rng_state: Cell<u32>,
112}
113
114impl<K, V, const SIZE: usize> MemoCache<K, V, SIZE>
115where
116    K: Eq,
117{
118    const SIZE_CHECK: () = assert!(SIZE > 0, "Cache size must be greater than 0");
119
120    /// Create a new cache.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use memo_cache::MemoCache;
126    ///
127    /// let c = MemoCache::<u32, String, 4>::new();
128    /// ```
129    #[cfg_attr(feature = "inline-more", inline)]
130    #[must_use]
131    #[allow(clippy::cast_possible_truncation)] // We assume cache size is limited to values representable by `u32`.
132    pub fn new() -> Self {
133        let () = Self::SIZE_CHECK; // Force evaluation of const assertion.
134
135        Self {
136            buffer: [const { KeyValueSlot::Empty }; SIZE],
137            rng_state: Cell::new(0x9E37_79B9 ^ (SIZE as u32).wrapping_mul(0x85EB_CA6B)), // Mixed primes.
138        }
139    }
140
141    /// Get the (fixed) capacity of the cache in [number of elements].
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use memo_cache::MemoCache;
147    ///
148    /// let c = MemoCache::<u32, String, 8>::new();
149    ///
150    /// assert_eq!(c.capacity(), 8);
151    /// ```
152    #[cfg_attr(feature = "inline-more", inline)]
153    pub const fn capacity(&self) -> usize {
154        SIZE
155    }
156
157    /// Get the size of the cache slot eviction search window.
158    const fn eviction_window_size() -> usize {
159        match SIZE {
160            0..=4 => SIZE,      // Tiny cache.
161            5..=16 => SIZE / 2, // Small-sized cache.
162            17..=64 => 8,       // Medium-sized cache.
163            _ => 16,            // Large cache.
164        }
165    }
166
167    /// Generate a pseudo-random value using Xorshift32 (see: <https://en.wikipedia.org/wiki/Xorshift>).
168    #[cfg_attr(feature = "inline-more", inline)]
169    fn xorshift32(&self) -> u32 {
170        let mut x = self.rng_state.get();
171        x ^= x << 13;
172        x ^= x >> 17;
173        x ^= x << 5;
174        self.rng_state.set(x);
175        x
176    }
177
178    /// Find a cache slot index to evict for replacement.
179    fn find_eviction_slot(&self) -> usize {
180        // Check for empty slots first.
181        for (idx, slot) in self.buffer.iter().enumerate() {
182            if let KeyValueSlot::Empty = slot {
183                return idx;
184            }
185        }
186
187        // The cache is fully occupied, find a slot to evict by scanning a window at a random position.
188        let window_size = Self::eviction_window_size();
189        let start_idx = ((u64::from(self.xorshift32()) * SIZE as u64) >> 32) as usize;
190
191        let mut evict_idx = start_idx;
192        let mut min_hits = u8::MAX;
193
194        for i in 0..window_size {
195            let idx = (start_idx + i) % SIZE;
196            // SAFETY: idx is guaranteed to be < SIZE due to the modulo operation.
197            let hits = unsafe { self.buffer.get_unchecked(idx) }.hits();
198
199            if hits < min_hits {
200                min_hits = hits;
201                evict_idx = idx;
202
203                // Early-out for unhit cache entries.
204                if hits == 0 {
205                    break;
206                }
207            }
208        }
209
210        evict_idx
211    }
212
213    /// Evict a suitable slot and replace it. Returns a reference to the replaced slot value.
214    #[cfg_attr(feature = "inline-more", inline)]
215    fn evict_and_replace(&mut self, k: K, v: V) -> &V {
216        self.decay_hits();
217
218        let idx = self.find_eviction_slot();
219        let s = unsafe { self.buffer.get_unchecked_mut(idx) };
220
221        *s = KeyValueSlot::Used {
222            key: k,
223            value: v,
224            hits: Cell::new(0),
225        };
226
227        // SAFETY: The slot was filled with a key/value above.
228        unsafe { s.get_value().unwrap_unchecked() }
229    }
230
231    /// Decay hit values for all occupied slots if any slot reaches a threshold.
232    fn decay_hits(&mut self) {
233        const DECAY_THRESHOLD: u8 = 192; // 75% of u8::MAX.
234
235        let decay_needed = self
236            .buffer
237            .iter()
238            .any(|s| matches!(s, KeyValueSlot::Used { hits, .. } if hits.get() >= DECAY_THRESHOLD));
239
240        if decay_needed {
241            for s in &mut self.buffer {
242                if let KeyValueSlot::Used { hits, .. } = s {
243                    let current = hits.get();
244                    hits.set(current - (current >> 2)); // Subtract 25%.
245                }
246            }
247        }
248    }
249
250    /// Insert a key/value pair.
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// use memo_cache::MemoCache;
256    ///
257    /// let mut c = MemoCache::<u32, &str, 4>::new();
258    ///
259    /// assert_eq!(c.get(&42), None);
260    ///
261    /// c.insert(42, "The Answer");
262    ///
263    /// assert_eq!(c.get(&42), Some(&"The Answer"));
264    /// ```
265    #[cfg_attr(feature = "inline-more", inline)]
266    pub fn insert(&mut self, k: K, v: V) {
267        match self.buffer.iter_mut().find(|e| e.is_key(&k)) {
268            Some(s) => s.update_value(v),
269            None => {
270                self.evict_and_replace(k, v);
271            }
272        }
273    }
274
275    /// Returns `true` if the cache contains a value for the specified key.
276    ///
277    /// # Examples
278    ///
279    /// ```
280    /// use memo_cache::MemoCache;
281    ///
282    /// let mut c = MemoCache::<u32, &str, 4>::new();
283    ///
284    /// assert_eq!(c.contains_key(&42), false);
285    ///
286    /// c.insert(42, "The Answer");
287    ///
288    /// assert_eq!(c.contains_key(&42), true);
289    /// ```
290    #[cfg_attr(feature = "inline-more", inline)]
291    pub fn contains_key<Q>(&self, k: &Q) -> bool
292    where
293        K: Borrow<Q>,
294        Q: Eq + ?Sized,
295    {
296        self.buffer.iter().any(|e| e.is_key(k))
297    }
298
299    /// Lookup a cache entry by key.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// use memo_cache::MemoCache;
305    ///
306    /// let mut c = MemoCache::<u32, &str, 4>::new();
307    ///
308    /// assert_eq!(c.get(&42), None);
309    ///
310    /// c.insert(42, "The Answer");
311    ///
312    /// assert_eq!(c.get(&42), Some(&"The Answer"));
313    /// ```
314    #[cfg_attr(feature = "inline-more", inline)]
315    pub fn get<Q>(&self, k: &Q) -> Option<&V>
316    where
317        K: Borrow<Q>,
318        Q: Eq + ?Sized,
319    {
320        self.buffer.iter().find(|e| e.is_key(k)).map(|e| {
321            debug_assert!(matches!(e, KeyValueSlot::Used { .. }), "Expected used slot");
322
323            // SAFETY: We assume `is_key` returns true for non-empty slots only.
324            unsafe { e.get_value().unwrap_unchecked() }
325        })
326    }
327
328    /// Lookup a cache entry by key (for mutation).
329    ///
330    /// # Examples
331    ///
332    /// ```
333    /// use memo_cache::MemoCache;
334    ///
335    /// let mut c = MemoCache::<u32, &str, 4>::new();
336    ///
337    /// c.insert(42, "The Answer");
338    ///
339    /// if let Some(v) = c.get_mut(&42) {
340    ///     *v = "Another Answer";
341    /// }
342    ///
343    /// assert_eq!(c.get(&42), Some(&"Another Answer"));
344    /// ```
345    #[cfg_attr(feature = "inline-more", inline)]
346    pub fn get_mut<Q>(&mut self, k: &Q) -> Option<&mut V>
347    where
348        K: Borrow<Q>,
349        Q: Eq + ?Sized,
350    {
351        self.buffer.iter_mut().find(|e| e.is_key(k)).map(|e| {
352            debug_assert!(matches!(e, KeyValueSlot::Used { .. }), "Expected used slot");
353
354            // SAFETY: We assume `is_key` returns true for non-empty slots only.
355            unsafe { e.get_value_mut().unwrap_unchecked() }
356        })
357    }
358
359    /// Get the index for a given key, if found.
360    #[cfg_attr(feature = "inline-more", inline)]
361    fn get_key_index<Q>(&self, k: &Q) -> Option<usize>
362    where
363        K: Borrow<Q>,
364        Q: Eq + ?Sized,
365    {
366        self.buffer.iter().position(|e| e.is_key(k))
367    }
368
369    /// Get a value, or, if it does not exist in the cache, insert it using the value computed by `f`.
370    /// Returns a reference to the found, or newly inserted value associated with the given key.
371    /// If a value is inserted, the key is cloned.
372    ///
373    /// # Examples
374    ///
375    /// ```
376    /// use memo_cache::MemoCache;
377    ///
378    /// let mut c = MemoCache::<u32, &str, 4>::new();
379    ///
380    /// assert_eq!(c.get(&42), None);
381    ///
382    /// let v = c.get_or_insert_with(&42, |_| "The Answer");
383    ///
384    /// assert_eq!(v, &"The Answer");
385    /// assert_eq!(c.get(&42), Some(&"The Answer"));
386    /// ```
387    ///
388    /// # Notes
389    ///
390    /// Because this crate is `no_std`, we have no access to `std::borrow::ToOwned`, which means we cannot create a
391    /// version of `get_or_insert_with` that can create an owned value from a borrowed key.
392    ///
393    #[cfg_attr(feature = "inline-more", inline)]
394    pub fn get_or_insert_with<F>(&mut self, k: &K, f: F) -> &V
395    where
396        K: Clone,
397        F: FnOnce(&K) -> V,
398    {
399        if let Some(i) = self.get_key_index(k) {
400            // SAFETY: The key index was retrieved from a found key.
401            unsafe { self.buffer.get_unchecked(i).get_value().unwrap_unchecked() }
402        } else {
403            self.evict_and_replace(k.clone(), f(k))
404        }
405    }
406
407    /// Get a value, or, if it does not exist in the cache, insert it using the value computed by `f`.
408    /// Returns a result with a reference to the found, or newly inserted value associated with the given key.
409    /// If a value is inserted, the key is cloned.
410    ///
411    /// # Examples
412    ///
413    /// ```
414    /// use memo_cache::MemoCache;
415    ///
416    /// let mut c = MemoCache::<u32, &str, 4>::new();
417    ///
418    /// assert_eq!(c.get(&42), None);
419    ///
420    /// let answer : Result<_, &str> = Ok("The Answer");
421    /// let v = c.get_or_try_insert_with(&42, |_| answer);
422    ///
423    /// assert_eq!(v, Ok(&"The Answer"));
424    /// assert_eq!(c.get(&42), Some(&"The Answer"));
425    ///
426    /// let v = c.get_or_try_insert_with(&17, |_| Err("Dunno"));
427    ///
428    /// assert_eq!(v, Err("Dunno"));
429    /// assert_eq!(c.get(&17), None);
430    /// ```
431    ///
432    /// # Errors
433    ///
434    /// If the function `f` fails, the error of type `E` is returned.
435    ///
436    /// # Notes
437    ///
438    /// Because this crate is `no_std`, we have no access to `std::borrow::ToOwned`, which means we cannot create a
439    /// version of `get_or_try_insert_with` that can create an owned value from a borrowed key.
440    ///
441    #[cfg_attr(feature = "inline-more", inline)]
442    pub fn get_or_try_insert_with<F, E>(&mut self, k: &K, f: F) -> Result<&V, E>
443    where
444        K: Clone,
445        F: FnOnce(&K) -> Result<V, E>,
446    {
447        if let Some(i) = self.get_key_index(k) {
448            // SAFETY: The key index was retrieved from a found key.
449            Ok(unsafe { self.buffer.get_unchecked(i).get_value().unwrap_unchecked() })
450        } else {
451            f(k).map(|v| self.evict_and_replace(k.clone(), v))
452        }
453    }
454
455    /// Clear the cache.
456    ///
457    /// # Examples
458    ///
459    /// ```
460    /// use memo_cache::MemoCache;
461    ///
462    /// let mut c = MemoCache::<u32, &str, 4>::new();
463    ///
464    /// assert_eq!(c.get(&42), None);
465    ///
466    /// c.insert(42, "The Answer");
467    ///
468    /// assert_eq!(c.get(&42), Some(&"The Answer"));
469    ///
470    /// c.clear();
471    ///
472    /// assert_eq!(c.get(&42), None);
473    ///
474    #[cfg_attr(feature = "inline-more", inline)]
475    pub fn clear(&mut self) {
476        self.buffer
477            .iter_mut()
478            .for_each(|e| *e = KeyValueSlot::Empty);
479    }
480}
481
482impl<K, V, const SIZE: usize> Default for MemoCache<K, V, SIZE>
483where
484    K: Eq,
485{
486    fn default() -> Self {
487        Self::new()
488    }
489}
490
491impl<K, V, const SIZE: usize> Clone for MemoCache<K, V, SIZE>
492where
493    K: Eq + Clone,
494    V: Clone,
495{
496    fn clone(&self) -> Self {
497        Self {
498            buffer: self.buffer.clone(),
499            rng_state: self.rng_state.clone(),
500        }
501    }
502}
503
504#[cfg(test)]
505mod tests_internal {
506    use super::*;
507
508    #[test]
509    fn test_new_state() {
510        const SIZE: usize = 8;
511
512        let c = MemoCache::<i32, i32, SIZE>::new();
513
514        // Verify cache size.
515        assert_eq!(c.buffer.len(), SIZE);
516        assert_eq!(c.capacity(), SIZE);
517
518        // All slots should be empty.
519        assert!(c.buffer.iter().all(|s| s == &KeyValueSlot::Empty));
520    }
521
522    // Compile-time assertion: MemoCache should be Send (can move between threads).
523    const _: fn() = || {
524        fn assert_send<T: Send>() {}
525        assert_send::<MemoCache<i32, i32, 4>>();
526    };
527
528    // MemoCache should NOT be Sync (cannot share across threads).
529    //
530    // The following compile-time test should fail:
531    //
532    // const _: fn() = || {
533    //     fn assert_sync<T: Sync>() {}
534    //     assert_sync::<MemoCache<i32, i32, 4>>();
535    // };
536}