Skip to main content

memo_cache/
lib.rs

1#![no_std]
2
3use core::{borrow::Borrow, cell::Cell, fmt, mem};
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/// Hit count threshold at which all hit counts are decayed (75% of `u8::MAX`).
22const DECAY_THRESHOLD: u8 = 192;
23
24/// A single key/value slot used in the cache.
25#[derive(Clone, PartialEq)]
26enum KeyValueSlot<K, V> {
27    Used { key: K, value: V, hits: Cell<u8> },
28    Empty,
29}
30
31impl<K, V> KeyValueSlot<K, V> {
32    /// Check a used slot key for equivalence.
33    ///
34    /// Returns `true` for used slots with key equivalence, `false` if otherwise.
35    ///
36    #[cfg_attr(feature = "inline-more", inline)]
37    fn is_key<Q>(&self, k: &Q) -> bool
38    where
39        Q: Equivalent<K> + ?Sized,
40    {
41        if let KeyValueSlot::Used { key, .. } = self {
42            k.equivalent(key)
43        } else {
44            false
45        }
46    }
47
48    /// Get the value of a used slot, counting the access as a hit and raising the decay
49    /// flag when the hit count crosses the decay threshold.
50    ///
51    /// NOTE: Hit counts increment by one, so crossing the threshold passes through it
52    ///       exactly once; comparing for equality keeps the hot path free of stores.
53    #[cfg_attr(feature = "inline-more", inline)]
54    fn get_value(&self, decay_pending: &Cell<bool>) -> Option<&V> {
55        if let KeyValueSlot::Used { value, hits, .. } = self {
56            let h = hits.get().saturating_add(1);
57            hits.set(h);
58            if h == DECAY_THRESHOLD {
59                decay_pending.set(true);
60            }
61            Some(value)
62        } else {
63            None
64        }
65    }
66
67    /// Get the value of a used slot (for mutation), counting the access as a hit and
68    /// raising the decay flag when the hit count crosses the decay threshold.
69    #[cfg_attr(feature = "inline-more", inline)]
70    fn get_value_mut(&mut self, decay_pending: &Cell<bool>) -> Option<&mut V> {
71        if let KeyValueSlot::Used { value, hits, .. } = self {
72            let h = hits.get().saturating_add(1);
73            hits.set(h);
74            if h == DECAY_THRESHOLD {
75                decay_pending.set(true);
76            }
77            Some(value)
78        } else {
79            None
80        }
81    }
82
83    /// Get the number of cache hits for this slot.
84    fn hits(&self) -> u8 {
85        if let KeyValueSlot::Used { hits, .. } = self {
86            hits.get()
87        } else {
88            0
89        }
90    }
91
92    /// Update the value of a used slot, returning the previous value (will be a no-op
93    /// returning `None` for empty slots).
94    #[cfg_attr(feature = "inline-more", inline)]
95    fn update_value(&mut self, v: V) -> Option<V> {
96        if let KeyValueSlot::Used { value, .. } = self {
97            Some(mem::replace(value, v))
98        } else {
99            None
100        }
101    }
102}
103
104/// A small, fixed-size key/value cache with retention management.
105///
106/// The key/value slots are stored inline (as an array inside the struct), so the cache
107/// lives wherever you place it; no heap allocation is required or performed.
108///
109/// # Thread Safety
110///
111/// `MemoCache` is `Send` but not `Sync`. It can be moved between threads but cannot
112/// be shared across threads via shared references (`&MemoCache`).
113///
114/// This is because the cache uses interior mutability (via [`Cell`]) for tracking hit counts
115/// and random number generation, which is not thread-safe.
116///
117/// For concurrent access, wrap it in a synchronization primitive. E.g.:
118///
119/// ```
120/// use std::sync::Mutex;
121/// use memo_cache::MemoCache;
122///
123/// let cache = Mutex::new(MemoCache::<u32, String, 8>::new());
124///
125/// // In thread 1:
126/// cache.lock().unwrap().insert(42, "value".to_string());
127///
128/// // In thread 2:
129/// let value = cache.lock().unwrap().get(&42);
130/// ```
131pub struct MemoCache<K, V, const SIZE: usize> {
132    buffer: [KeyValueSlot<K, V>; SIZE],
133    rng_state: Cell<u32>,
134    /// Number of occupied slots.
135    used: usize,
136    /// Set when some slot's hit count crossed the decay threshold. May overestimate
137    /// (eviction and removal reset or drop slot hit counts without lowering it);
138    /// `decay_hits` verifies and corrects it.
139    decay_pending: Cell<bool>,
140}
141
142impl<K, V, const SIZE: usize> MemoCache<K, V, SIZE>
143where
144    K: Eq,
145{
146    const SIZE_CHECK: () = assert!(SIZE > 0, "Cache size must be greater than 0");
147
148    /// Create a new cache.
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// use memo_cache::MemoCache;
154    ///
155    /// let c = MemoCache::<u32, String, 4>::new();
156    /// ```
157    #[cfg_attr(feature = "inline-more", inline)]
158    #[must_use]
159    #[allow(clippy::cast_possible_truncation)] // We assume cache size is limited to values representable by `u32`.
160    pub fn new() -> Self {
161        let () = Self::SIZE_CHECK; // Force evaluation of const assertion.
162
163        Self {
164            buffer: [const { KeyValueSlot::Empty }; SIZE],
165            rng_state: Cell::new(0x9E37_79B9 ^ (SIZE as u32).wrapping_mul(0x85EB_CA6B)), // Mixed primes.
166            used: 0,
167            decay_pending: Cell::new(false),
168        }
169    }
170
171    /// Get the (fixed) capacity of the cache in [number of elements].
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use memo_cache::MemoCache;
177    ///
178    /// let c = MemoCache::<u32, String, 8>::new();
179    ///
180    /// assert_eq!(c.capacity(), 8);
181    /// ```
182    #[cfg_attr(feature = "inline-more", inline)]
183    pub const fn capacity(&self) -> usize {
184        SIZE
185    }
186
187    /// Get the number of occupied slots in the cache.
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// use memo_cache::MemoCache;
193    ///
194    /// let mut c = MemoCache::<u32, &str, 4>::new();
195    ///
196    /// assert_eq!(c.len(), 0);
197    ///
198    /// c.insert(42, "The Answer");
199    ///
200    /// assert_eq!(c.len(), 1);
201    /// ```
202    #[cfg_attr(feature = "inline-more", inline)]
203    pub const fn len(&self) -> usize {
204        self.used
205    }
206
207    /// Returns `true` if the cache contains no elements.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use memo_cache::MemoCache;
213    ///
214    /// let mut c = MemoCache::<u32, &str, 4>::new();
215    ///
216    /// assert!(c.is_empty());
217    ///
218    /// c.insert(42, "The Answer");
219    ///
220    /// assert!(!c.is_empty());
221    /// ```
222    #[cfg_attr(feature = "inline-more", inline)]
223    pub const fn is_empty(&self) -> bool {
224        self.used == 0
225    }
226
227    /// Get the size of the cache slot eviction search window.
228    const fn eviction_window_size() -> usize {
229        match SIZE {
230            0..=4 => SIZE,      // Tiny cache.
231            5..=16 => SIZE / 2, // Small-sized cache.
232            17..=64 => 8,       // Medium-sized cache.
233            _ => 16,            // Large cache.
234        }
235    }
236
237    /// Generate a pseudo-random value using Xorshift32 (see: <https://en.wikipedia.org/wiki/Xorshift>).
238    #[cfg_attr(feature = "inline-more", inline)]
239    fn xorshift32(&self) -> u32 {
240        let mut x = self.rng_state.get();
241        x ^= x << 13;
242        x ^= x >> 17;
243        x ^= x << 5;
244        self.rng_state.set(x);
245        x
246    }
247
248    /// Find a cache slot index to evict for replacement.
249    ///
250    /// Returns the index of an empty slot if (and only if) the cache is not fully occupied.
251    fn find_eviction_slot(&self) -> usize {
252        // Use an empty slot first, if there is one.
253        if self.used < SIZE {
254            if let Some(idx) = self
255                .buffer
256                .iter()
257                .position(|s| matches!(s, KeyValueSlot::Empty))
258            {
259                return idx;
260            }
261
262            debug_assert!(false, "Occupancy count says an empty slot exists");
263        }
264
265        // The cache is fully occupied, find a slot to evict by scanning a window at a random position.
266        let window_size = Self::eviction_window_size();
267        let start_idx = ((u64::from(self.xorshift32()) * SIZE as u64) >> 32) as usize;
268
269        let mut evict_idx = start_idx;
270        let mut min_hits = u8::MAX;
271
272        for i in 0..window_size {
273            let idx = (start_idx + i) % SIZE;
274            let hits = self.buffer[idx].hits();
275
276            if hits < min_hits {
277                min_hits = hits;
278                evict_idx = idx;
279
280                // Early-out for unhit cache entries.
281                if hits == 0 {
282                    break;
283                }
284            }
285        }
286
287        evict_idx
288    }
289
290    /// Evict a suitable slot and replace it. Returns a reference to the replaced slot value.
291    #[cfg_attr(feature = "inline-more", inline)]
292    fn evict_and_replace(&mut self, k: K, v: V) -> &V {
293        self.decay_hits();
294
295        let idx = self.find_eviction_slot();
296
297        // `find_eviction_slot` fills an empty slot iff the cache is not fully occupied.
298        if self.used < SIZE {
299            self.used += 1;
300        }
301
302        let s = &mut self.buffer[idx];
303
304        *s = KeyValueSlot::Used {
305            key: k,
306            value: v,
307            hits: Cell::new(0),
308        };
309
310        // NOTE: Return the value directly instead of via `get_value`, which would count
311        //       the insertion itself as a hit.
312        match s {
313            KeyValueSlot::Used { value, .. } => value,
314            KeyValueSlot::Empty => unreachable!(), // The slot was filled above.
315        }
316    }
317
318    /// Decay hit values for all occupied slots if any slot reaches the decay threshold.
319    fn decay_hits(&mut self) {
320        // Fast path: the flag is raised whenever a slot hit count crosses the threshold,
321        // so no slot can have reached it while the flag is down.
322        if !self.decay_pending.get() {
323            return;
324        }
325
326        // The flag may overestimate (eviction and removal reset or drop slot hit counts
327        // without lowering it), so compute the true maximum before deciding.
328        let true_max = self
329            .buffer
330            .iter()
331            .map(KeyValueSlot::hits)
332            .max()
333            .unwrap_or(0);
334
335        if true_max >= DECAY_THRESHOLD {
336            for s in &mut self.buffer {
337                if let KeyValueSlot::Used { hits, .. } = s {
338                    let current = hits.get();
339                    // NOTE: Subtracting 25% is a no-op for hit counts 1..=3 (the shift
340                    //       rounds down to zero). This is deliberate: such entries keep a
341                    //       slight edge over never-hit entries, and the imprecision is
342                    //       irrelevant at the decay threshold scale.
343                    hits.set(current - (current >> 2)); // Subtract 25%.
344                }
345            }
346
347            // Saturated hit counts may still be at the threshold after one decay round.
348            self.decay_pending
349                .set(true_max - (true_max >> 2) >= DECAY_THRESHOLD);
350        } else {
351            self.decay_pending.set(false);
352        }
353    }
354
355    /// Insert a key/value pair.
356    ///
357    /// If the key was already present, its value is updated and the previous value is
358    /// returned. Otherwise `None` is returned.
359    ///
360    /// # Notes
361    ///
362    /// Inserting a new key into a full cache evicts another entry to make room; the
363    /// evicted key/value pair is *not* returned.
364    ///
365    /// # Examples
366    ///
367    /// ```
368    /// use memo_cache::MemoCache;
369    ///
370    /// let mut c = MemoCache::<u32, &str, 4>::new();
371    ///
372    /// assert_eq!(c.get(&42), None);
373    ///
374    /// assert_eq!(c.insert(42, "The Answer"), None);
375    ///
376    /// assert_eq!(c.get(&42), Some(&"The Answer"));
377    ///
378    /// assert_eq!(c.insert(42, "Another Answer"), Some("The Answer"));
379    /// ```
380    #[cfg_attr(feature = "inline-more", inline)]
381    pub fn insert(&mut self, k: K, v: V) -> Option<V> {
382        if let Some(s) = self.buffer.iter_mut().find(|e| e.is_key(&k)) {
383            s.update_value(v)
384        } else {
385            self.evict_and_replace(k, v);
386            None
387        }
388    }
389
390    /// Returns `true` if the cache contains a value for the specified key.
391    ///
392    /// # Examples
393    ///
394    /// ```
395    /// use memo_cache::MemoCache;
396    ///
397    /// let mut c = MemoCache::<u32, &str, 4>::new();
398    ///
399    /// assert_eq!(c.contains_key(&42), false);
400    ///
401    /// c.insert(42, "The Answer");
402    ///
403    /// assert_eq!(c.contains_key(&42), true);
404    /// ```
405    #[cfg_attr(feature = "inline-more", inline)]
406    pub fn contains_key<Q>(&self, k: &Q) -> bool
407    where
408        K: Borrow<Q>,
409        Q: Eq + ?Sized,
410    {
411        self.buffer.iter().any(|e| e.is_key(k))
412    }
413
414    /// Lookup a cache entry by key.
415    ///
416    /// # Examples
417    ///
418    /// ```
419    /// use memo_cache::MemoCache;
420    ///
421    /// let mut c = MemoCache::<u32, &str, 4>::new();
422    ///
423    /// assert_eq!(c.get(&42), None);
424    ///
425    /// c.insert(42, "The Answer");
426    ///
427    /// assert_eq!(c.get(&42), Some(&"The Answer"));
428    /// ```
429    #[cfg_attr(feature = "inline-more", inline)]
430    pub fn get<Q>(&self, k: &Q) -> Option<&V>
431    where
432        K: Borrow<Q>,
433        Q: Eq + ?Sized,
434    {
435        // NOTE: `is_key` only matches used slots, so `get_value` returns `Some` here.
436        self.buffer
437            .iter()
438            .find(|e| e.is_key(k))
439            .and_then(|e| e.get_value(&self.decay_pending))
440    }
441
442    /// Lookup a cache entry by key (for mutation).
443    ///
444    /// # Examples
445    ///
446    /// ```
447    /// use memo_cache::MemoCache;
448    ///
449    /// let mut c = MemoCache::<u32, &str, 4>::new();
450    ///
451    /// c.insert(42, "The Answer");
452    ///
453    /// if let Some(v) = c.get_mut(&42) {
454    ///     *v = "Another Answer";
455    /// }
456    ///
457    /// assert_eq!(c.get(&42), Some(&"Another Answer"));
458    /// ```
459    #[cfg_attr(feature = "inline-more", inline)]
460    pub fn get_mut<Q>(&mut self, k: &Q) -> Option<&mut V>
461    where
462        K: Borrow<Q>,
463        Q: Eq + ?Sized,
464    {
465        // NOTE: `is_key` only matches used slots, so `get_value_mut` returns `Some` here.
466        let decay_pending = &self.decay_pending;
467        self.buffer
468            .iter_mut()
469            .find(|e| e.is_key(k))
470            .and_then(|e| e.get_value_mut(decay_pending))
471    }
472
473    /// Remove a key from the cache, returning the value if the key was present.
474    ///
475    /// The freed slot is reused by subsequent insertions.
476    ///
477    /// # Examples
478    ///
479    /// ```
480    /// use memo_cache::MemoCache;
481    ///
482    /// let mut c = MemoCache::<u32, &str, 4>::new();
483    ///
484    /// c.insert(42, "The Answer");
485    ///
486    /// assert_eq!(c.remove(&42), Some("The Answer"));
487    /// assert_eq!(c.remove(&42), None);
488    /// assert_eq!(c.get(&42), None);
489    /// ```
490    #[cfg_attr(feature = "inline-more", inline)]
491    pub fn remove<Q>(&mut self, k: &Q) -> Option<V>
492    where
493        K: Borrow<Q>,
494        Q: Eq + ?Sized,
495    {
496        let used = &mut self.used;
497        self.buffer.iter_mut().find(|e| e.is_key(k)).map(|s| {
498            *used -= 1;
499            match mem::replace(s, KeyValueSlot::Empty) {
500                KeyValueSlot::Used { value, .. } => value,
501                KeyValueSlot::Empty => unreachable!(), // The slot was found by key.
502            }
503        })
504    }
505
506    /// Get the index for a given key, if found.
507    #[cfg_attr(feature = "inline-more", inline)]
508    fn get_key_index<Q>(&self, k: &Q) -> Option<usize>
509    where
510        K: Borrow<Q>,
511        Q: Eq + ?Sized,
512    {
513        self.buffer.iter().position(|e| e.is_key(k))
514    }
515
516    /// Get a value, or, if it does not exist in the cache, insert it using the value computed by `f`.
517    /// Returns a reference to the found, or newly inserted value associated with the given key.
518    /// If a value is inserted, the key is cloned.
519    ///
520    /// # Examples
521    ///
522    /// ```
523    /// use memo_cache::MemoCache;
524    ///
525    /// let mut c = MemoCache::<u32, &str, 4>::new();
526    ///
527    /// assert_eq!(c.get(&42), None);
528    ///
529    /// let v = c.get_or_insert_with(&42, |_| "The Answer");
530    ///
531    /// assert_eq!(v, &"The Answer");
532    /// assert_eq!(c.get(&42), Some(&"The Answer"));
533    /// ```
534    ///
535    /// # Notes
536    ///
537    /// Because this crate is `no_std`, we have no access to `std::borrow::ToOwned`, which means we cannot create a
538    /// version of `get_or_insert_with` that can create an owned value from a borrowed key.
539    ///
540    #[cfg_attr(feature = "inline-more", inline)]
541    pub fn get_or_insert_with<F>(&mut self, k: &K, f: F) -> &V
542    where
543        K: Clone,
544        F: FnOnce(&K) -> V,
545    {
546        if let Some(i) = self.get_key_index(k) {
547            // NOTE: The index was found by key, so the slot is used and holds a value.
548            self.buffer[i]
549                .get_value(&self.decay_pending)
550                .expect("Slot found by key must be used")
551        } else {
552            self.evict_and_replace(k.clone(), f(k))
553        }
554    }
555
556    /// Get a value, or, if it does not exist in the cache, insert it using the value computed by `f`.
557    /// Returns a result with a reference to the found, or newly inserted value associated with the given key.
558    /// If a value is inserted, the key is cloned.
559    ///
560    /// # Examples
561    ///
562    /// ```
563    /// use memo_cache::MemoCache;
564    ///
565    /// let mut c = MemoCache::<u32, &str, 4>::new();
566    ///
567    /// assert_eq!(c.get(&42), None);
568    ///
569    /// let answer : Result<_, &str> = Ok("The Answer");
570    /// let v = c.get_or_try_insert_with(&42, |_| answer);
571    ///
572    /// assert_eq!(v, Ok(&"The Answer"));
573    /// assert_eq!(c.get(&42), Some(&"The Answer"));
574    ///
575    /// let v = c.get_or_try_insert_with(&17, |_| Err("Dunno"));
576    ///
577    /// assert_eq!(v, Err("Dunno"));
578    /// assert_eq!(c.get(&17), None);
579    /// ```
580    ///
581    /// # Errors
582    ///
583    /// If the function `f` fails, the error of type `E` is returned.
584    ///
585    /// # Notes
586    ///
587    /// Because this crate is `no_std`, we have no access to `std::borrow::ToOwned`, which means we cannot create a
588    /// version of `get_or_try_insert_with` that can create an owned value from a borrowed key.
589    ///
590    #[cfg_attr(feature = "inline-more", inline)]
591    pub fn get_or_try_insert_with<F, E>(&mut self, k: &K, f: F) -> Result<&V, E>
592    where
593        K: Clone,
594        F: FnOnce(&K) -> Result<V, E>,
595    {
596        if let Some(i) = self.get_key_index(k) {
597            // NOTE: The index was found by key, so the slot is used and holds a value.
598            Ok(self.buffer[i]
599                .get_value(&self.decay_pending)
600                .expect("Slot found by key must be used"))
601        } else {
602            f(k).map(|v| self.evict_and_replace(k.clone(), v))
603        }
604    }
605
606    /// Get an iterator over the key/value pairs of the cache, in unspecified order.
607    ///
608    /// Iterating does not affect the hit counts used by the eviction policy.
609    ///
610    /// # Examples
611    ///
612    /// ```
613    /// use memo_cache::MemoCache;
614    ///
615    /// let mut c = MemoCache::<u32, &str, 4>::new();
616    ///
617    /// c.insert(1, "one");
618    /// c.insert(2, "two");
619    ///
620    /// let mut entries = c.iter().collect::<Vec<_>>();
621    /// entries.sort();
622    ///
623    /// assert_eq!(entries, [(&1, &"one"), (&2, &"two")]);
624    /// ```
625    #[cfg_attr(feature = "inline-more", inline)]
626    pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
627        self.buffer.iter().filter_map(|s| match s {
628            KeyValueSlot::Used { key, value, .. } => Some((key, value)),
629            KeyValueSlot::Empty => None,
630        })
631    }
632
633    /// Clear the cache.
634    ///
635    /// # Examples
636    ///
637    /// ```
638    /// use memo_cache::MemoCache;
639    ///
640    /// let mut c = MemoCache::<u32, &str, 4>::new();
641    ///
642    /// assert_eq!(c.get(&42), None);
643    ///
644    /// c.insert(42, "The Answer");
645    ///
646    /// assert_eq!(c.get(&42), Some(&"The Answer"));
647    ///
648    /// c.clear();
649    ///
650    /// assert_eq!(c.get(&42), None);
651    ///
652    #[cfg_attr(feature = "inline-more", inline)]
653    pub fn clear(&mut self) {
654        self.buffer
655            .iter_mut()
656            .for_each(|e| *e = KeyValueSlot::Empty);
657        self.used = 0;
658        self.decay_pending.set(false);
659    }
660}
661
662impl<K, V, const SIZE: usize> Default for MemoCache<K, V, SIZE>
663where
664    K: Eq,
665{
666    fn default() -> Self {
667        Self::new()
668    }
669}
670
671impl<K, V, const SIZE: usize> fmt::Debug for MemoCache<K, V, SIZE>
672where
673    K: Eq + fmt::Debug,
674    V: fmt::Debug,
675{
676    /// Format the cache entries as a map, in unspecified order.
677    ///
678    /// # Examples
679    ///
680    /// ```
681    /// use memo_cache::MemoCache;
682    ///
683    /// let mut c = MemoCache::<u32, &str, 4>::new();
684    ///
685    /// c.insert(42, "The Answer");
686    ///
687    /// assert_eq!(format!("{c:?}"), r#"{42: "The Answer"}"#);
688    /// ```
689    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690        f.debug_map().entries(self.iter()).finish()
691    }
692}
693
694impl<K, V, const SIZE: usize> Clone for MemoCache<K, V, SIZE>
695where
696    K: Eq + Clone,
697    V: Clone,
698{
699    fn clone(&self) -> Self {
700        // Remix the RNG state so the clone does not replay the original's eviction
701        // randomness.
702        let mut rng_state = self.rng_state.get() ^ 0x5851_F42D; // Mixing prime.
703        if rng_state == 0 {
704            rng_state = 0x9E37_79B9; // The Xorshift32 state must be nonzero.
705        }
706
707        Self {
708            buffer: self.buffer.clone(),
709            rng_state: Cell::new(rng_state),
710            used: self.used,
711            decay_pending: self.decay_pending.clone(),
712        }
713    }
714}
715
716#[cfg(test)]
717mod tests_internal {
718    use super::*;
719
720    #[test]
721    fn test_new_state() {
722        const SIZE: usize = 8;
723
724        let c = MemoCache::<i32, i32, SIZE>::new();
725
726        // Verify cache size.
727        assert_eq!(c.buffer.len(), SIZE);
728        assert_eq!(c.capacity(), SIZE);
729
730        // All slots should be empty.
731        assert!(c.buffer.iter().all(|s| s == &KeyValueSlot::Empty));
732    }
733
734    // Compile-time assertion: MemoCache should be Send (can move between threads).
735    const _: fn() = || {
736        fn assert_send<T: Send>() {}
737        assert_send::<MemoCache<i32, i32, 4>>();
738    };
739
740    // MemoCache should NOT be Sync (cannot share across threads).
741    //
742    // The following compile-time test should fail:
743    //
744    // const _: fn() = || {
745    //     fn assert_sync<T: Sync>() {}
746    //     assert_sync::<MemoCache<i32, i32, 4>>();
747    // };
748}