use rustc_hash::FxHashMap;
use std::cell::RefCell;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::rc::Rc;
pub struct ContentKey<T> {
digest: [u8; 32],
_marker: PhantomData<fn() -> T>,
}
impl<T> Clone for ContentKey<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for ContentKey<T> {}
impl<T> PartialEq for ContentKey<T> {
fn eq(&self, other: &Self) -> bool {
self.digest == other.digest
}
}
impl<T> Eq for ContentKey<T> {}
impl<T> Hash for ContentKey<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.digest.hash(state);
}
}
impl<T> std::fmt::Debug for ContentKey<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ContentKey(")?;
for b in &self.digest[..4] {
write!(f, "{b:02x}")?;
}
write!(f, "…)")
}
}
struct Blake3Hasher(blake3::Hasher);
impl Hasher for Blake3Hasher {
fn finish(&self) -> u64 {
let bytes = self.0.finalize();
let mut buf = [0u8; 8];
buf.copy_from_slice(&bytes.as_bytes()[..8]);
u64::from_le_bytes(buf)
}
fn write(&mut self, bytes: &[u8]) {
self.0.update(bytes);
}
}
impl<T: Hash> ContentKey<T> {
#[must_use]
pub fn of(input: &T) -> Self {
let mut hasher = Blake3Hasher(blake3::Hasher::new());
input.hash(&mut hasher);
Self {
digest: *hasher.0.finalize().as_bytes(),
_marker: PhantomData,
}
}
#[must_use]
pub fn digest(&self) -> &[u8; 32] {
&self.digest
}
}
#[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()
}
}
impl<T: Hash, V> ContentMemo<ContentKey<T>, V> {
pub fn get_or_compute_keyed(&self, input: &T, compute: impl FnOnce(&T) -> V) -> Rc<V> {
let key = ContentKey::of(input);
if let Some(hit) = self.map.borrow().get(&key).cloned() {
return hit;
}
let rc = Rc::new(compute(input));
self.map.borrow_mut().insert(key, rc.clone());
rc
}
}
#[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());
}
};
}
#[allow(dead_code)]
fn _content_key_type_seal_doc() {}
#[cfg(test)]
mod tests {
use super::{ContentKey, 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");
}
#[derive(Hash)]
struct Input {
name: String,
n: u32,
}
#[test]
fn content_key_of_is_deterministic() {
let a = Input {
name: "x".into(),
n: 5,
};
let b = Input {
name: "x".into(),
n: 5,
};
assert_eq!(ContentKey::of(&a), ContentKey::of(&b));
assert_eq!(ContentKey::of(&a).digest(), ContentKey::of(&b).digest());
assert_eq!(ContentKey::of(&a), ContentKey::of(&a));
}
#[test]
fn content_key_differs_on_different_content() {
let a = Input {
name: "x".into(),
n: 5,
};
let differ_n = Input {
name: "x".into(),
n: 6,
};
let differ_name = Input {
name: "y".into(),
n: 5,
};
assert_ne!(ContentKey::of(&a), ContentKey::of(&differ_n));
assert_ne!(ContentKey::of(&a), ContentKey::of(&differ_name));
}
#[test]
fn keyed_memo_round_trips_and_does_not_recompute_on_hit() {
let memo: ContentMemo<ContentKey<Input>, String> = ContentMemo::new();
let mut calls = 0;
let input = Input {
name: "hello".into(),
n: 2,
};
let a = memo.get_or_compute_keyed(&input, |i| {
calls += 1;
assert_eq!(i.name, "hello");
assert_eq!(i.n, 2);
format!("{}-{}", i.name, i.n)
});
assert_eq!(&*a, "hello-2");
let same_content = Input {
name: "hello".into(),
n: 2,
};
let b = memo.get_or_compute_keyed(&same_content, |_| {
calls += 1;
"SHOULD-NOT-RUN".to_string()
});
assert_eq!(calls, 1, "structurally-equal input must hit, not recompute");
assert!(std::rc::Rc::ptr_eq(&a, &b), "hit returns the same Rc");
let other = Input {
name: "world".into(),
n: 9,
};
let c = memo.get_or_compute_keyed(&other, |i| {
calls += 1;
format!("{}-{}", i.name, i.n)
});
assert_eq!(calls, 2);
assert_eq!(&*c, "world-9");
assert_eq!(memo.len(), 2);
}
}