use rustc_hash::FxHashMap;
use std::cell::RefCell;
use std::hash::Hash;
use std::rc::Rc;
#[derive(Debug)]
pub struct ContentMemo<K: Eq + Hash, V> {
map: RefCell<FxHashMap<K, Rc<V>>>,
}
impl<K: Eq + Hash, V> Default for ContentMemo<K, V> {
fn default() -> Self {
Self {
map: RefCell::new(FxHashMap::default()),
}
}
}
impl<K: Eq + Hash + Clone, V> ContentMemo<K, V> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn get_or_compute(&self, key: K, compute: impl FnOnce() -> V) -> Rc<V> {
if let Some(hit) = self.map.borrow().get(&key).cloned() {
return hit;
}
let rc = Rc::new(compute());
self.map.borrow_mut().insert(key, rc.clone());
rc
}
pub fn clear(&self) {
self.map.borrow_mut().clear();
}
#[must_use]
pub fn len(&self) -> usize {
self.map.borrow().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.map.borrow().is_empty()
}
}
#[macro_export]
macro_rules! thread_local_content_memo {
(
$(#[$meta:meta])*
$STATIC:ident : $K:ty => $V:ty ;
fn $get:ident ;
fn $clear:ident ;
) => {
thread_local! {
$(#[$meta])*
static $STATIC: $crate::memo::ContentMemo<$K, $V> =
$crate::memo::ContentMemo::new();
}
#[allow(dead_code)]
fn $get(key: $K, compute: impl FnOnce() -> $V) -> ::std::rc::Rc<$V> {
$STATIC.with(|m| m.get_or_compute(key, compute))
}
#[allow(dead_code)]
fn $clear() {
$STATIC.with(|m| m.clear());
}
};
}
#[cfg(test)]
mod tests {
use super::ContentMemo;
#[test]
fn hit_returns_same_rc_and_does_not_recompute() {
let memo: ContentMemo<u32, String> = ContentMemo::new();
let mut calls = 0;
let a = memo.get_or_compute(7, || {
calls += 1;
"seven".to_string()
});
let b = memo.get_or_compute(7, || {
calls += 1;
"SHOULD-NOT-RUN".to_string()
});
assert_eq!(calls, 1, "second get for the same key must not recompute");
assert!(std::rc::Rc::ptr_eq(&a, &b), "same key returns the same Rc");
assert_eq!(&*a, "seven");
}
#[test]
fn distinct_keys_are_independent() {
let memo: ContentMemo<u32, u32> = ContentMemo::new();
let a = memo.get_or_compute(1, || 10);
let b = memo.get_or_compute(2, || 20);
assert_eq!((*a, *b), (10, 20));
assert_eq!(memo.len(), 2);
assert!(!std::rc::Rc::ptr_eq(&a, &b));
}
#[test]
fn clear_drops_entries_so_recompute_happens() {
let memo: ContentMemo<u32, u32> = ContentMemo::new();
let mut calls = 0;
let _ = memo.get_or_compute(1, || {
calls += 1;
1
});
assert_eq!(memo.len(), 1);
memo.clear();
assert!(memo.is_empty());
let _ = memo.get_or_compute(1, || {
calls += 1;
1
});
assert_eq!(calls, 2, "after clear, the key recomputes");
}
crate::thread_local_content_memo! {
TEST_MEMO: u32 => String;
fn tl_get;
fn tl_clear;
}
#[test]
fn macro_accessor_memoizes_and_clears() {
tl_clear();
let a = tl_get(3, || "three".to_string());
let b = tl_get(3, || "nope".to_string());
assert!(std::rc::Rc::ptr_eq(&a, &b));
assert_eq!(&*a, "three");
tl_clear();
let c = tl_get(3, || "again".to_string());
assert_eq!(&*c, "again", "after clear the key recomputes");
}
}