1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
/// Cache any expression while staying in expression scope. The expression gets
/// put in a type-erased thread-local box, and it's downcast and cloned every
/// time the expression is called at runtime.
///
/// # Panics
///
/// If the enclosed expression can have multiple possible types and the
/// point-of-use unifies to a different type than the default. I'm not aware of
/// a way to statically detect this so it's just a panic.
///
/// The most obvious way in which this can happen is with integer literals which
/// default to `i32` but can unify seamlessly to any other integer type, but it
/// could also be caused by autoref/autoderef. In either case, rust-analyzer can
/// tell you the type of both the nested expression and the
#[macro_export]
macro_rules! tl_cache {
($data:expr) => {{
use std::any::Any;
thread_local! {
static MY_VALUE: (Box<dyn Any>, &'static str) = {
let data = Box::new($data);
let name = std::any::type_name_of_val(&*data);
(data, name)
}
}
fn failed_downcast_to<T>(found_type: &str) -> T {
let data_str = stringify!($data);
let expected = std::any::type_name::<T>();
panic!("tl_cache called on ambiguous expression!\n{{{data_str}}} defaults to {found_type} but accessed as {expected}")
}
MY_VALUE.with(|(v, name)| {
// the if/else unifies to Option<T> which is the return type of the correct
// downcast_ref.
#[allow(dead_code)]
if false { Some($data) } else { v.downcast_ref().cloned() }
.unwrap_or_else(|| failed_downcast_to(name))
})
}};
}
#[cfg(test)]
mod test {
#[test]
fn tl_cache_correct() {
let my_cached_value = tl_cache!(1);
assert_eq!(my_cached_value, 1)
}
#[test]
#[should_panic = "tl_cache called on ambiguous expression!\n{1} defaults to i32 but accessed as u64"]
fn tl_cache_incorrect() { let _: u64 = tl_cache!(1); }
}