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