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}