diff --git a/crates/typst-utils/src/hash.rs b/crates/typst-utils/src/hash.rs index 9687da20b..eaf8a6721 100644 --- a/crates/typst-utils/src/hash.rs +++ b/crates/typst-utils/src/hash.rs @@ -30,9 +30,10 @@ use siphasher::sip128::{Hasher128, SipHasher13}; /// # Usage /// If the value is expected to be cloned, it is best used inside of an `Arc` /// or `Rc` to best re-use the hash once it has been computed. +#[derive(Clone)] pub struct LazyHash { /// The hash for the value. - hash: AtomicU128, + hash: HashLock, /// The underlying value. value: T, } @@ -48,16 +49,7 @@ impl LazyHash { /// Wraps an item without pre-computed hash. #[inline] pub fn new(value: T) -> Self { - Self { hash: AtomicU128::new(0), value } - } - - /// Wrap an item with a pre-computed hash. - /// - /// **Important:** The hash must be correct for the value. This cannot be - /// enforced at compile time, so use with caution. - #[inline] - pub fn reuse(value: T, existing: &LazyHash) -> Self { - LazyHash { hash: AtomicU128::new(existing.load_hash()), value } + Self { hash: HashLock::new(), value } } /// Returns the wrapped value. @@ -67,33 +59,11 @@ impl LazyHash { } } -impl LazyHash { - /// Get the hash, returns zero if not computed yet. - #[inline] - fn load_hash(&self) -> u128 { - // We only need atomicity and no synchronization of other operations, so - // `Relaxed` is fine. - self.hash.load(Ordering::Relaxed) - } -} - impl LazyHash { /// Get the hash or compute it if not set yet. #[inline] fn load_or_compute_hash(&self) -> u128 { - let mut hash = self.load_hash(); - if hash == 0 { - hash = hash_item(&self.value); - self.hash.store(hash, Ordering::Relaxed); - } - hash - } - - /// Reset the hash to zero. - #[inline] - fn reset_hash(&mut self) { - // Because we have a mutable reference, we can skip the atomic. - *self.hash.get_mut() = 0; + self.hash.get_or_insert_with(|| hash_item(&self.value)) } } @@ -140,23 +110,14 @@ impl Deref for LazyHash { } } -impl DerefMut for LazyHash { +impl DerefMut for LazyHash { #[inline] fn deref_mut(&mut self) -> &mut Self::Target { - self.reset_hash(); + self.hash.reset(); &mut self.value } } -impl Clone for LazyHash { - fn clone(&self) -> Self { - Self { - hash: AtomicU128::new(self.load_hash()), - value: self.value.clone(), - } - } -} - impl Debug for LazyHash { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.value.fmt(f) @@ -233,3 +194,51 @@ impl Debug for ManuallyHash { self.value.fmt(f) } } + +/// Storage for lazy hash computation. +pub struct HashLock(AtomicU128); + +impl HashLock { + /// Create a new unset hash cell. + pub const fn new() -> Self { + Self(AtomicU128::new(0)) + } + + /// Get the hash or compute it if not set yet. + #[inline] + pub fn get_or_insert_with(&self, f: impl FnOnce() -> u128) -> u128 { + let mut hash = self.get(); + if hash == 0 { + hash = f(); + self.0.store(hash, Ordering::Relaxed); + } + hash + } + + /// Reset the hash to unset. + #[inline] + pub fn reset(&mut self) { + // Because we have a mutable reference, we can skip the atomic. + *self.0.get_mut() = 0; + } + + /// Get the hash, returns zero if not computed yet. + #[inline] + fn get(&self) -> u128 { + // We only need atomicity and no synchronization of other operations, so + // `Relaxed` is fine. + self.0.load(Ordering::Relaxed) + } +} + +impl Default for HashLock { + fn default() -> Self { + Self::new() + } +} + +impl Clone for HashLock { + fn clone(&self) -> Self { + Self(AtomicU128::new(self.get())) + } +} diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index 8d0cdc15a..6a833b17f 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -15,7 +15,7 @@ mod scalar; pub use self::bitset::{BitSet, SmallBitSet}; pub use self::deferred::Deferred; pub use self::duration::format_duration; -pub use self::hash::{LazyHash, ManuallyHash}; +pub use self::hash::{HashLock, LazyHash, ManuallyHash}; pub use self::pico::{PicoStr, ResolvedPicoStr}; pub use self::round::{round_int_with_precision, round_with_precision}; pub use self::scalar::Scalar;