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