Skip to main content

commonware_utils/
cache.rs

1//! A fixed-capacity cache with CLOCK eviction.
2//!
3//! [Clock] is a bounded key-value cache that uses the
4//! [CLOCK](https://en.wikipedia.org/wiki/Page_replacement_algorithm#Clock)
5//! (second-chance) replacement policy, a lightweight approximation of LRU. Each
6//! slot carries a reference bit. Reading an entry sets its bit. When the cache
7//! is full and a new entry must be inserted, a clock hand sweeps the slots:
8//! every slot whose bit is set has it cleared and is skipped (granted a second
9//! chance), and the first slot whose bit is clear is evicted.
10//!
11//! # Why CLOCK Instead of Exact LRU
12//!
13//! Exact LRU must move an entry to the front of a recency list on every read,
14//! which requires exclusive (`&mut`) access. CLOCK only needs to set a reference
15//! bit, which it does through a shared (`&self`) reference via an atomic. This
16//! lets concurrent readers share the cache without serializing on a write lock,
17//! at the cost of approximating (rather than exactly tracking) recency.
18//!
19//! # Allocation Reuse
20//!
21//! Slots are allocated lazily as the cache grows to capacity and are then reused
22//! in place. Eviction, [Clock::remove], and [Clock::retain] never free
23//! a slot's value; they detach the key and keep the slot (and its allocation)
24//! for the next insert. [Clock::get_or_insert_mut] exposes the reused slot
25//! (and its index) so callers holding pooled buffers can overwrite in place
26//! instead of reallocating. The value-returning inserts ([Clock::put],
27//! [Clock::get_or_insert_with]) drop the displaced value as usual.
28//!
29//! # Concurrency
30//!
31//! [Clock] performs no internal locking. [Clock::get] takes `&self`
32//! and is the only lookup that records use, so the cache can be wrapped in a
33//! reader-writer lock and queried concurrently on the hit path. Misses, which
34//! must insert, take `&mut self` and therefore the write lock:
35//!
36//! ```
37//! use commonware_utils::cache::Clock;
38//! use core::num::NonZeroUsize;
39//! use std::sync::RwLock;
40//!
41//! let cache = RwLock::new(Clock::<u64, u64>::new(NonZeroUsize::new(4).unwrap()));
42//!
43//! // Hit path: shared read lock, runs concurrently with other readers.
44//! if cache.read().unwrap().get(&7).is_none() {
45//!     // Miss path: exclusive write lock, computes and inserts the value once.
46//!     cache.write().unwrap().get_or_insert_with(7, || 7 * 7);
47//! }
48//! assert_eq!(cache.read().unwrap().get(&7).copied(), Some(49));
49//! ```
50//!
51//! # Example
52//!
53//! ```
54//! use commonware_utils::cache::Clock;
55//! use core::num::NonZeroUsize;
56//!
57//! let mut cache = Clock::new(NonZeroUsize::new(2).unwrap());
58//!
59//! // Compute an expensive value only on a miss.
60//! let value = *cache.get_or_insert_with(1u64, || 1u64 * 1000);
61//! assert_eq!(value, 1000);
62//!
63//! // A second lookup is served from the cache.
64//! assert_eq!(cache.get(&1).copied(), Some(1000));
65//! ```
66
67#[cfg(not(feature = "std"))]
68use alloc::vec::Vec;
69use core::{
70    hash::Hash,
71    num::NonZeroUsize,
72    sync::atomic::{AtomicBool, Ordering},
73};
74use hashbrown::HashMap;
75
76type Hasher = ahash::RandomState;
77
78/// A single cache slot.
79///
80/// A slot is live when its key is present in the index, and free otherwise.
81/// Free slots keep their (now stale) `key` and `value` until the slot is reused,
82/// with `live` cleared so [Clock::get_at] cannot resolve them.
83struct Slot<K, V> {
84    key: K,
85    value: V,
86    referenced: AtomicBool,
87    live: bool,
88}
89
90/// A fixed-capacity key-value cache that evicts entries using the CLOCK
91/// (second-chance) replacement policy.
92///
93/// See the [module documentation](self) for the policy, allocation reuse, and
94/// concurrency details.
95pub struct Clock<K, V> {
96    /// Maps each live key to the index of its slot in `slots`.
97    ///
98    /// `index.len() + free.len() == slots.len()` always holds.
99    index: HashMap<K, usize, Hasher>,
100    /// Backing storage for slots, grown lazily up to `capacity` and then reused.
101    slots: Vec<Slot<K, V>>,
102    /// Slots detached from the index and available for reuse. Populated by
103    /// [Self::remove] and [Self::retain]; eviction reuses its victim slot
104    /// directly, so it never adds here.
105    free: Vec<usize>,
106    /// The clock hand: the next slot the evictor will examine.
107    hand: usize,
108    /// The maximum number of entries the cache will hold.
109    capacity: usize,
110}
111
112impl<K: Hash + Eq + Clone, V> Clock<K, V> {
113    /// Creates a cache that holds at most `capacity` entries.
114    pub fn new(capacity: NonZeroUsize) -> Self {
115        let capacity = capacity.get();
116        Self {
117            index: HashMap::with_capacity_and_hasher(capacity, Hasher::default()),
118            slots: Vec::with_capacity(capacity),
119            free: Vec::new(),
120            hand: 0,
121            capacity,
122        }
123    }
124
125    /// Returns the maximum number of entries the cache can hold.
126    #[inline]
127    pub const fn capacity(&self) -> usize {
128        self.capacity
129    }
130
131    /// Returns the number of entries currently in the cache.
132    #[inline]
133    pub fn len(&self) -> usize {
134        self.index.len()
135    }
136
137    /// Returns `true` if the cache holds no entries.
138    #[inline]
139    pub fn is_empty(&self) -> bool {
140        self.index.is_empty()
141    }
142
143    /// Returns `true` if `key` is in the cache without recording use.
144    #[inline]
145    pub fn contains(&self, key: &K) -> bool {
146        self.index.contains_key(key)
147    }
148
149    /// Returns a reference to the value for `key` without recording use.
150    ///
151    /// Unlike [Self::get], this does not set the entry's reference bit, so it
152    /// does not protect the entry from the next eviction sweep.
153    #[inline]
154    pub fn peek(&self, key: &K) -> Option<&V> {
155        let &slot = self.index.get(key)?;
156        Some(&self.slots[slot].value)
157    }
158
159    /// Returns a reference to the value for `key`, recording use.
160    ///
161    /// Recording use sets the entry's reference bit so the next eviction sweep
162    /// grants it a second chance. This takes `&self` so it can be called
163    /// concurrently behind a shared lock.
164    #[inline]
165    pub fn get(&self, key: &K) -> Option<&V> {
166        let &index = self.index.get(key)?;
167        let slot = &self.slots[index];
168
169        // Skip the store when the bit is already set. Under concurrent &self
170        // readers an unconditional store would dirty this cache line on every
171        // hit and bounce it between cores
172        if !slot.referenced.load(Ordering::Relaxed) {
173            slot.referenced.store(true, Ordering::Relaxed);
174        }
175        Some(&slot.value)
176    }
177
178    /// Returns a reference to the value in `slot` if that slot currently holds
179    /// `key` as a live entry, recording use.
180    ///
181    /// This is the read half of an external slot index: callers that recorded a
182    /// key's slot (via [Self::get_or_insert_mut]) can resolve it with a
183    /// key compare instead of a hash lookup. Any stale index entry reads as a
184    /// miss rather than a wrong value: a slot reused for another key fails the
185    /// key compare, a slot freed by [Self::remove] or [Self::retain] is not
186    /// live, and an out-of-range slot does not exist. External indexes
187    /// therefore need no maintenance beyond tolerating misses.
188    #[inline]
189    pub fn get_at(&self, slot: usize, key: &K) -> Option<&V> {
190        let slot = self.slots.get(slot)?;
191        if !slot.live || slot.key != *key {
192            return None;
193        }
194
195        // Skip the store when the bit is already set (see [Self::get]).
196        if !slot.referenced.load(Ordering::Relaxed) {
197            slot.referenced.store(true, Ordering::Relaxed);
198        }
199        Some(&slot.value)
200    }
201
202    /// Returns a mutable reference to the value for `key`, recording use.
203    #[inline]
204    pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
205        let &index = self.index.get(key)?;
206        let slot = &mut self.slots[index];
207        slot.referenced.store(true, Ordering::Relaxed);
208        Some(&mut slot.value)
209    }
210
211    /// Inserts `value` for `key`.
212    ///
213    /// If `key` was already present, replaces and returns the previous value,
214    /// recording use. If inserting a new entry exceeds the capacity, the CLOCK
215    /// evictor reclaims a slot first.
216    pub fn put(&mut self, key: K, value: V) -> Option<V> {
217        if let Some(&index) = self.index.get(&key) {
218            let slot = &mut self.slots[index];
219            slot.referenced.store(true, Ordering::Relaxed);
220            return Some(core::mem::replace(&mut slot.value, value));
221        }
222        self.insert_value(key, value);
223        None
224    }
225
226    /// Returns the value for `key`, computing and inserting it with `f` on a
227    /// miss.
228    ///
229    /// On a hit, `f` is not called. On a miss, `f` is called, its result is
230    /// inserted (evicting an entry if the cache is full), and a reference to the
231    /// stored value is returned.
232    pub fn get_or_insert_with<F: FnOnce() -> V>(&mut self, key: K, f: F) -> &V {
233        let slot = match self.index.get(&key) {
234            Some(&slot) => {
235                self.slots[slot].referenced.store(true, Ordering::Relaxed);
236                slot
237            }
238            None => self.insert_value(key, f()),
239        };
240        &self.slots[slot].value
241    }
242
243    /// Returns the value for `key`, computing and inserting it with a fallible
244    /// `f` on a miss.
245    ///
246    /// On a hit, `f` is not called. On a miss, `f` is called; if it returns an
247    /// error the error is propagated and nothing is inserted, so failures are
248    /// not cached.
249    pub fn try_get_or_insert_with<F: FnOnce() -> Result<V, E>, E>(
250        &mut self,
251        key: K,
252        f: F,
253    ) -> Result<&V, E> {
254        let slot = match self.index.get(&key) {
255            Some(&slot) => {
256                self.slots[slot].referenced.store(true, Ordering::Relaxed);
257                slot
258            }
259            None => self.insert_value(key, f()?),
260        };
261        Ok(&self.slots[slot].value)
262    }
263
264    /// Returns the slot index and a mutable reference to the slot for `key`,
265    /// reusing an existing allocation where possible.
266    ///
267    /// On a hit, records use and returns the current value. On a miss into a
268    /// reused slot (a freed slot or an eviction victim), the returned reference
269    /// is the reused slot's stale value, which the caller is expected to
270    /// overwrite. Only when the cache grows is `make` called to produce a fresh
271    /// value. This lets callers holding pooled buffers overwrite in place
272    /// rather than allocating on every insert.
273    ///
274    /// The slot index identifies the entry until it is evicted or removed, so
275    /// callers can record it in an external index and resolve later reads with
276    /// [Self::get_at] instead of a hash lookup.
277    pub fn get_or_insert_mut<F: FnOnce() -> V>(&mut self, key: K, make: F) -> (usize, &mut V) {
278        let slot = match self.index.get(&key) {
279            Some(&slot) => {
280                self.slots[slot].referenced.store(true, Ordering::Relaxed);
281                slot
282            }
283            None => match self.take_slot() {
284                Some(slot) => {
285                    self.slots[slot].key = key.clone();
286                    self.slots[slot].referenced.store(true, Ordering::Relaxed);
287                    self.slots[slot].live = true;
288                    self.index.insert(key, slot);
289                    slot
290                }
291                None => {
292                    let value = make();
293                    self.grow(key, value)
294                }
295            },
296        };
297        (slot, &mut self.slots[slot].value)
298    }
299
300    /// Removes `key`, returning whether it was present.
301    ///
302    /// The slot and its allocation are retained for reuse, so the value is not
303    /// returned.
304    pub fn remove(&mut self, key: &K) -> bool {
305        match self.index.remove(key) {
306            Some(slot) => {
307                self.slots[slot].referenced.store(false, Ordering::Relaxed);
308                self.slots[slot].live = false;
309                self.free.push(slot);
310                true
311            }
312            None => false,
313        }
314    }
315
316    /// Retains only the entries for which `keep` returns `true`.
317    ///
318    /// Dropped entries' slots and allocations are retained for reuse.
319    pub fn retain<F: FnMut(&K, &V) -> bool>(&mut self, mut keep: F) {
320        let Self {
321            index, slots, free, ..
322        } = self;
323        index.retain(|key, &mut slot| {
324            let keep = keep(key, &slots[slot].value);
325            if !keep {
326                slots[slot].referenced.store(false, Ordering::Relaxed);
327                slots[slot].live = false;
328                free.push(slot);
329            }
330            keep
331        });
332    }
333
334    /// Removes all entries, dropping their values and retaining the allocated
335    /// capacity of the index and slot vector.
336    pub fn clear(&mut self) {
337        self.index.clear();
338        self.slots.clear();
339        self.free.clear();
340        self.hand = 0;
341    }
342
343    /// Pushes a brand new slot holding `(key, value)` and returns its index.
344    ///
345    /// Only called while the cache is below capacity.
346    fn grow(&mut self, key: K, value: V) -> usize {
347        let slot = self.slots.len();
348        self.index.insert(key.clone(), slot);
349        self.slots.push(Slot {
350            key,
351            value,
352            referenced: AtomicBool::new(true),
353            live: true,
354        });
355        slot
356    }
357
358    /// Inserts `value` for a `key` known to be absent, returning its slot.
359    fn insert_value(&mut self, key: K, value: V) -> usize {
360        match self.take_slot() {
361            Some(slot) => {
362                self.slots[slot].key = key.clone();
363                self.slots[slot].value = value;
364                self.slots[slot].referenced.store(true, Ordering::Relaxed);
365                self.slots[slot].live = true;
366                self.index.insert(key, slot);
367                slot
368            }
369            None => self.grow(key, value),
370        }
371    }
372
373    /// Selects a slot to receive a new entry, detaching it from the index.
374    ///
375    /// Returns a freed slot if any exist, otherwise an eviction victim chosen by
376    /// the clock sweep. Returns `None` if the cache is below capacity and should
377    /// grow instead. The returned slot keeps its stale value for reuse.
378    fn take_slot(&mut self) -> Option<usize> {
379        if let Some(slot) = self.free.pop() {
380            return Some(slot);
381        }
382        if self.slots.len() < self.capacity {
383            return None;
384        }
385
386        let len = self.slots.len();
387        while self.slots[self.hand].referenced.load(Ordering::Relaxed) {
388            self.slots[self.hand]
389                .referenced
390                .store(false, Ordering::Relaxed);
391            self.hand = (self.hand + 1) % len;
392        }
393        let slot = self.hand;
394        self.hand = (self.hand + 1) % len;
395        self.index.remove(&self.slots[slot].key);
396        Some(slot)
397    }
398}
399
400impl<K: Hash + Eq + Clone + Default, V> Clock<K, V> {
401    /// Pre-allocates all slots up to capacity, each holding a value from `make`,
402    /// and leaves them free for reuse.
403    ///
404    /// After this call, the first `capacity` inserts reuse a pre-allocated slot
405    /// instead of growing, so `make` (and any allocation it performs) runs only
406    /// here. Use this to front-load allocation at construction so steady-state
407    /// inserts never allocate. Free slots are seeded with the default key as a
408    /// throwaway placeholder that is overwritten when the slot is first filled.
409    pub fn prefill<F: FnMut() -> V>(&mut self, mut make: F) {
410        let start = self.free.len();
411        while self.slots.len() < self.capacity {
412            let slot = self.slots.len();
413            self.slots.push(Slot {
414                key: K::default(),
415                value: make(),
416                referenced: AtomicBool::new(false),
417                live: false,
418            });
419            self.free.push(slot);
420        }
421        // The free list pops from the back, so reverse the new entries to hand
422        // them out in ascending slot order (matching how growth assigns slots).
423        self.free[start..].reverse();
424    }
425}
426
427impl<K, V> core::fmt::Debug for Clock<K, V> {
428    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
429        f.debug_struct("Clock")
430            .field("len", &self.index.len())
431            .field("capacity", &self.capacity)
432            .finish()
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use crate::NZUsize;
440    use core::cell::Cell;
441    use proptest::prelude::*;
442    use std::{collections::HashMap, rc::Rc, thread};
443
444    impl<K: Hash + Eq + Clone, V> Clock<K, V> {
445        /// Asserts the structural invariants hold (test-only).
446        fn check_invariants(&self) {
447            assert!(self.slots.len() <= self.capacity);
448            assert_eq!(self.index.len() + self.free.len(), self.slots.len());
449            if self.slots.is_empty() {
450                assert_eq!(self.hand, 0);
451            } else {
452                assert!(self.hand < self.slots.len());
453            }
454            // The index is a bijection onto the live slots, each slot's key
455            // round-trips, and live slots are disjoint from free slots.
456            let free: std::collections::HashSet<usize> = self.free.iter().copied().collect();
457            assert_eq!(free.len(), self.free.len(), "duplicate free slot");
458            let mut seen = std::collections::HashSet::new();
459            for (key, &slot) in &self.index {
460                assert!(slot < self.slots.len());
461                assert!(!free.contains(&slot), "slot {slot} both live and free");
462                assert!(seen.insert(slot), "slot {slot} mapped twice");
463                assert!(self.slots[slot].key == *key);
464                assert!(self.slots[slot].live, "indexed slot {slot} not live");
465            }
466            for &slot in &self.free {
467                assert!(!self.slots[slot].live, "free slot {slot} still live");
468            }
469        }
470    }
471
472    #[test]
473    fn test_basic_put_get_peek() {
474        let mut cache = Clock::new(NZUsize!(2));
475        assert!(cache.is_empty());
476        assert_eq!(cache.capacity(), 2);
477
478        assert_eq!(cache.put(1u64, 10u64), None);
479        assert_eq!(cache.put(2, 20), None);
480        assert_eq!(cache.len(), 2);
481
482        assert_eq!(cache.get(&1).copied(), Some(10));
483        assert_eq!(cache.peek(&2).copied(), Some(20));
484        assert!(cache.contains(&1));
485        assert!(!cache.contains(&3));
486        assert_eq!(cache.get(&3), None);
487        cache.check_invariants();
488    }
489
490    #[test]
491    fn test_put_replaces_existing() {
492        let mut cache = Clock::new(NZUsize!(2));
493        assert_eq!(cache.put(1u64, 10u64), None);
494        assert_eq!(cache.put(1, 11), Some(10));
495        assert_eq!(cache.get(&1).copied(), Some(11));
496        assert_eq!(cache.len(), 1);
497        cache.check_invariants();
498    }
499
500    #[test]
501    fn test_capacity_one_eviction() {
502        let mut cache = Clock::new(NZUsize!(1));
503        cache.put(1u64, 10u64);
504        cache.put(2, 20);
505        assert!(!cache.contains(&1));
506        assert_eq!(cache.get(&2).copied(), Some(20));
507        assert_eq!(cache.len(), 1);
508        cache.check_invariants();
509    }
510
511    #[test]
512    fn test_second_chance_protects_referenced_entry() {
513        // New entries are inserted with their reference bit set, so a referenced
514        // entry only beats an unreferenced one once some bits have been cleared.
515        // Capacity 3, slots index by insertion order.
516        let mut cache = Clock::new(NZUsize!(3));
517        cache.put(1u64, 10u64); // slot 0, ref=1
518        cache.put(2, 20); // slot 1, ref=1
519        cache.put(3, 30); // slot 2, ref=1
520
521        // Full and all referenced. Inserting key 4 sweeps slots 0,1,2 clearing
522        // every bit, wraps to slot 0 (now clear), and evicts key 1. Hand -> 1.
523        // State: slot0=key4(1), slot1=key2(0), slot2=key3(0).
524        cache.put(4, 40);
525        assert!(!cache.contains(&1));
526
527        // Reference key 2 (slot 1), leaving key 3 (slot 2) unreferenced.
528        assert_eq!(cache.get(&2).copied(), Some(20));
529
530        // Inserting key 5 sweeps from slot 1: key 2's bit is set, so it is
531        // cleared and skipped; slot 2 (key 3) is unreferenced and evicted.
532        cache.put(5, 50);
533        assert!(cache.contains(&2));
534        assert!(!cache.contains(&3));
535        assert!(cache.contains(&4));
536        assert!(cache.contains(&5));
537        cache.check_invariants();
538    }
539
540    #[test]
541    fn test_all_referenced_evicts_hand_position() {
542        // When every entry has been referenced, the sweep clears all bits and
543        // evicts the slot the hand started on.
544        let mut cache = Clock::new(NZUsize!(3));
545        cache.put(1u64, 10u64);
546        cache.put(2, 20);
547        cache.put(3, 30);
548        assert!(cache.get(&1).is_some());
549        assert!(cache.get(&2).is_some());
550        assert!(cache.get(&3).is_some());
551
552        // Hand is at slot 0 (key 1). Sweep clears all bits, lands back on slot 0.
553        cache.put(4, 40);
554        assert!(!cache.contains(&1));
555        assert!(cache.contains(&2));
556        assert!(cache.contains(&3));
557        assert!(cache.contains(&4));
558        cache.check_invariants();
559    }
560
561    #[test]
562    fn test_get_or_insert_with_calls_f_only_on_miss() {
563        let mut cache = Clock::new(NZUsize!(2));
564        let calls = Cell::new(0);
565        let compute = |k: u64| {
566            calls.set(calls.get() + 1);
567            k * 100
568        };
569
570        assert_eq!(*cache.get_or_insert_with(1, || compute(1)), 100);
571        assert_eq!(calls.get(), 1);
572        // Hit: f is not called.
573        assert_eq!(*cache.get_or_insert_with(1, || compute(1)), 100);
574        assert_eq!(calls.get(), 1);
575        cache.check_invariants();
576    }
577
578    #[test]
579    fn test_try_get_or_insert_with_does_not_cache_errors() {
580        let mut cache = Clock::new(NZUsize!(2));
581
582        let err: Result<&u64, &str> = cache.try_get_or_insert_with(1u64, || Err("bad"));
583        assert_eq!(err, Err("bad"));
584        assert!(!cache.contains(&1));
585
586        let ok: Result<&u64, &str> = cache.try_get_or_insert_with(1, || Ok(10));
587        assert_eq!(ok, Ok(&10));
588        assert!(cache.contains(&1));
589        cache.check_invariants();
590    }
591
592    #[test]
593    fn test_remove_keeps_slot_for_reuse() {
594        // A removed entry frees its slot for reuse without growing the slot
595        // vector or calling the factory again.
596        let makes = Cell::new(0);
597        let mut cache: Clock<u64, u64> = Clock::new(NZUsize!(2));
598        cache.get_or_insert_mut(1, || {
599            makes.set(makes.get() + 1);
600            10
601        });
602        cache.get_or_insert_mut(2, || {
603            makes.set(makes.get() + 1);
604            20
605        });
606        assert_eq!(makes.get(), 2);
607        assert_eq!(cache.slots.len(), 2);
608
609        assert!(cache.remove(&1));
610        assert!(!cache.contains(&1));
611        assert_eq!(cache.len(), 1);
612
613        // Reusing the freed slot does not call the factory or grow.
614        *cache
615            .get_or_insert_mut(3, || {
616                makes.set(makes.get() + 1);
617                30
618            })
619            .1 = 30;
620        assert_eq!(
621            makes.get(),
622            2,
623            "freed slot should be reused, factory not called"
624        );
625        assert_eq!(cache.slots.len(), 2);
626        assert_eq!(cache.get(&3).copied(), Some(30));
627        assert!(!cache.remove(&999));
628        cache.check_invariants();
629    }
630
631    #[test]
632    fn test_retain() {
633        let mut cache = Clock::new(NZUsize!(4));
634        for i in 0..4u64 {
635            cache.put(i, i * 10);
636        }
637        // Keep even keys.
638        cache.retain(|k, _| k % 2 == 0);
639        assert_eq!(cache.len(), 2);
640        assert!(cache.contains(&0));
641        assert!(cache.contains(&2));
642        assert!(!cache.contains(&1));
643        assert!(!cache.contains(&3));
644        // Freed slots are reused for new inserts.
645        cache.put(10, 100);
646        cache.put(12, 120);
647        assert_eq!(cache.slots.len(), 4);
648        assert_eq!(cache.len(), 4);
649        cache.check_invariants();
650    }
651
652    #[test]
653    fn test_get_or_insert_mut_reuses_allocations() {
654        // The factory runs at most `capacity` times no matter how many distinct
655        // keys churn through the cache, proving evicted slots are reused.
656        let makes = Cell::new(0);
657        let mut cache: Clock<u64, u64> = Clock::new(NZUsize!(3));
658        for k in 0..100u64 {
659            let (_, v) = cache.get_or_insert_mut(k, || {
660                makes.set(makes.get() + 1);
661                0
662            });
663            *v = k; // overwrite the (possibly stale) reused slot
664        }
665        assert_eq!(makes.get(), 3, "factory should run only during growth");
666        assert_eq!(cache.slots.len(), 3);
667        assert_eq!(cache.len(), 3);
668        cache.check_invariants();
669    }
670
671    #[test]
672    fn test_prefill_allocates_once_and_reuses() {
673        // prefill runs the factory exactly capacity times; subsequent inserts
674        // reuse pre-allocated slots without growing or calling the factory.
675        let makes = Cell::new(0);
676        let mut cache: Clock<u64, u64> = Clock::new(NZUsize!(3));
677        cache.prefill(|| {
678            makes.set(makes.get() + 1);
679            0
680        });
681        assert_eq!(makes.get(), 3);
682        assert_eq!(cache.slots.len(), 3);
683        assert!(cache.is_empty());
684        cache.check_invariants();
685
686        // Churn many keys; no further factory calls, slot vector stays at capacity.
687        for k in 0..100u64 {
688            *cache
689                .get_or_insert_mut(k, || {
690                    makes.set(makes.get() + 1);
691                    0
692                })
693                .1 = k;
694        }
695        assert_eq!(makes.get(), 3, "prefilled slots must be reused");
696        assert_eq!(cache.slots.len(), 3);
697        assert_eq!(cache.len(), 3);
698        cache.check_invariants();
699    }
700
701    #[test]
702    fn test_clear() {
703        let mut cache = Clock::new(NZUsize!(4));
704        for i in 0..4u64 {
705            cache.put(i, i);
706        }
707        cache.clear();
708        assert!(cache.is_empty());
709        assert_eq!(cache.len(), 0);
710        cache.put(9, 9);
711        assert_eq!(cache.get(&9).copied(), Some(9));
712        cache.check_invariants();
713    }
714
715    #[derive(Clone)]
716    struct Tracked {
717        _counter: Rc<Cell<usize>>,
718    }
719
720    impl Drop for Tracked {
721        fn drop(&mut self) {
722            self._counter.set(self._counter.get() + 1);
723        }
724    }
725
726    #[test]
727    fn test_values_dropped_on_eviction_and_clear() {
728        let drops = Rc::new(Cell::new(0));
729        let mut cache: Clock<u64, Tracked> = Clock::new(NZUsize!(2));
730        for i in 0..2u64 {
731            cache.put(
732                i,
733                Tracked {
734                    _counter: drops.clone(),
735                },
736            );
737        }
738        assert_eq!(drops.get(), 0);
739
740        // Inserting a third entry evicts one (and drops its value).
741        cache.put(
742            2,
743            Tracked {
744                _counter: drops.clone(),
745            },
746        );
747        assert_eq!(drops.get(), 1);
748
749        // Replacing an existing key drops the old value.
750        cache.put(
751            2,
752            Tracked {
753                _counter: drops.clone(),
754            },
755        );
756        assert_eq!(drops.get(), 2);
757
758        // Clearing drops the remaining two values.
759        cache.clear();
760        assert_eq!(drops.get(), 4);
761    }
762
763    #[test]
764    fn test_remove_retains_value_until_reuse() {
765        // remove() does not drop the value; the freed slot keeps it until the
766        // slot is reused by a value-inserting method.
767        let drops = Rc::new(Cell::new(0));
768        let mut cache: Clock<u64, Tracked> = Clock::new(NZUsize!(2));
769        cache.put(
770            1,
771            Tracked {
772                _counter: drops.clone(),
773            },
774        );
775        assert!(cache.remove(&1));
776        assert_eq!(drops.get(), 0, "remove must not drop the value");
777
778        // Reusing the freed slot via a value insert drops the retained value.
779        cache.put(
780            2,
781            Tracked {
782                _counter: drops.clone(),
783            },
784        );
785        assert_eq!(drops.get(), 1);
786    }
787
788    #[test]
789    fn test_get_at_validates_key() {
790        let mut cache = Clock::new(NZUsize!(2));
791        let (slot1, v) = cache.get_or_insert_mut(1u64, || 0u64);
792        *v = 10;
793        let (slot2, v) = cache.get_or_insert_mut(2u64, || 0u64);
794        *v = 20;
795
796        // A recorded slot resolves with a key compare, no hash lookup.
797        assert_eq!(cache.get_at(slot1, &1).copied(), Some(10));
798        assert_eq!(cache.get_at(slot2, &2).copied(), Some(20));
799
800        // The wrong key for a slot and an out-of-range slot both read as misses.
801        assert_eq!(cache.get_at(slot1, &2), None);
802        assert_eq!(cache.get_at(cache.capacity(), &1), None);
803        cache.check_invariants();
804    }
805
806    #[test]
807    fn test_get_at_stale_slot_after_eviction() {
808        // Evicting key 1 reuses its slot for key 3: the stale index entry fails
809        // the key compare while the new key resolves at the same slot.
810        let mut cache = Clock::new(NZUsize!(1));
811        let (slot, v) = cache.get_or_insert_mut(1u64, || 0u64);
812        *v = 10;
813        let (reused, v) = cache.get_or_insert_mut(3u64, || 0u64);
814        *v = 30;
815        assert_eq!(slot, reused);
816        assert_eq!(cache.get_at(slot, &1), None);
817        assert_eq!(cache.get_at(slot, &3).copied(), Some(30));
818        cache.check_invariants();
819    }
820
821    #[test]
822    fn test_get_at_freed_slot_is_a_miss() {
823        // remove() keeps the slot's stale key for allocation reuse, but get_at must not
824        // resolve it: freed entries read as misses without any external index hygiene.
825        let mut cache = Clock::new(NZUsize!(2));
826        let (slot, v) = cache.get_or_insert_mut(1u64, || 0u64);
827        *v = 10;
828        assert!(cache.remove(&1));
829        assert_eq!(cache.get_at(slot, &1), None);
830
831        // Reusing the slot for another key resolves the new key only.
832        let (reused, v) = cache.get_or_insert_mut(2u64, || 0u64);
833        *v = 20;
834        assert_eq!(reused, slot);
835        assert_eq!(cache.get_at(slot, &1), None);
836        assert_eq!(cache.get_at(slot, &2).copied(), Some(20));
837        cache.check_invariants();
838    }
839
840    #[test]
841    fn test_get_at_records_use() {
842        // Mirrors test_peek_does_not_record_use's setup: after it, slot1=key2
843        // and slot2=key3 are unreferenced with the hand at slot1. get_at on
844        // key2 must set its bit so the next insert evicts key3 instead.
845        let mut c = Clock::new(NZUsize!(3));
846        c.put(1u64, 10u64);
847        c.put(2, 20);
848        c.put(3, 30);
849        c.put(4, 40); // evicts key1, clears key2/key3 bits, hand -> slot1
850
851        let slot = *c.index.get(&2).unwrap();
852        assert_eq!(c.get_at(slot, &2).copied(), Some(20));
853        c.put(5, 50);
854        assert!(c.contains(&2), "get_at must protect key2 from eviction");
855        assert!(!c.contains(&3));
856        c.check_invariants();
857    }
858
859    #[test]
860    fn test_get_or_insert_mut_slot_stable_on_hit() {
861        let mut cache = Clock::new(NZUsize!(2));
862        let (slot, v) = cache.get_or_insert_mut(1u64, || 0u64);
863        *v = 10;
864        let (hit_slot, v) = cache.get_or_insert_mut(1u64, || unreachable!());
865        assert_eq!(slot, hit_slot);
866        assert_eq!(*v, 10);
867        cache.check_invariants();
868    }
869
870    #[test]
871    fn test_get_mut() {
872        let mut cache = Clock::new(NZUsize!(2));
873        cache.put(1u64, 10u64);
874        assert_eq!(cache.get_mut(&2), None);
875        *cache.get_mut(&1).unwrap() = 11;
876        assert_eq!(cache.get(&1).copied(), Some(11));
877        cache.check_invariants();
878    }
879
880    #[test]
881    fn test_peek_does_not_record_use() {
882        // After this shared setup (capacity 3): slot1=key2 and slot2=key3 are
883        // both unreferenced and the hand is at slot1. peek(&2) leaves key2
884        // unreferenced, so the next insert evicts it; get(&2) sets its bit, so
885        // key3 is evicted instead. This isolates the one behavioral difference
886        // between peek and get.
887        fn setup() -> Clock<u64, u64> {
888            let mut c = Clock::new(NZUsize!(3));
889            c.put(1, 10);
890            c.put(2, 20);
891            c.put(3, 30);
892            c.put(4, 40); // evicts key1, clears key2/key3 bits, hand -> slot1
893            c
894        }
895
896        // peek does NOT record use: key2 stays evictable.
897        let mut c = setup();
898        assert_eq!(c.peek(&2).copied(), Some(20));
899        c.put(5, 50);
900        assert!(!c.contains(&2), "peek must not protect key2 from eviction");
901        assert!(c.contains(&3));
902
903        // get DOES record use: key2 survives, key3 is evicted instead.
904        let mut c = setup();
905        assert_eq!(c.get(&2).copied(), Some(20));
906        c.put(5, 50);
907        assert!(c.contains(&2), "get must protect key2 from eviction");
908        assert!(!c.contains(&3));
909    }
910
911    #[test]
912    fn test_concurrent_get_is_sound() {
913        // get(&self) records use through an atomic, so many threads can read
914        // concurrently through a shared &Clock with no external lock.
915        let mut cache: Clock<u64, u64> = Clock::new(NZUsize!(64));
916        for i in 0..64u64 {
917            cache.put(i, i * 10);
918        }
919        let cache = &cache;
920        thread::scope(|s| {
921            for _ in 0..4 {
922                s.spawn(move || {
923                    for _ in 0..2_000 {
924                        for i in 0..64u64 {
925                            assert_eq!(cache.get(&i).copied(), Some(i * 10));
926                        }
927                    }
928                });
929            }
930        });
931        for i in 0..64u64 {
932            assert_eq!(cache.get(&i).copied(), Some(i * 10));
933        }
934        cache.check_invariants();
935    }
936
937    #[derive(Clone, Debug)]
938    enum Op {
939        Get(u8),
940        Peek(u8),
941        Put(u8, u16),
942        GetOrInsert(u8, u16),
943        GetOrInsertMut(u8, u16),
944        GetMut(u8, u16),
945        Remove(u8),
946        Retain(u8),
947    }
948
949    fn op_strategy() -> impl Strategy<Value = Op> {
950        prop_oneof![
951            (0u8..16).prop_map(Op::Get),
952            (0u8..16).prop_map(Op::Peek),
953            (0u8..16, any::<u16>()).prop_map(|(k, v)| Op::Put(k, v)),
954            (0u8..16, any::<u16>()).prop_map(|(k, v)| Op::GetOrInsert(k, v)),
955            (0u8..16, any::<u16>()).prop_map(|(k, v)| Op::GetOrInsertMut(k, v)),
956            (0u8..16, any::<u16>()).prop_map(|(k, v)| Op::GetMut(k, v)),
957            (0u8..16).prop_map(Op::Remove),
958            (0u8..16).prop_map(Op::Retain),
959        ]
960    }
961
962    const KEY_SPACE: u8 = 16;
963
964    proptest! {
965        #[test]
966        fn prop_invariants_hold(
967            cap in 1usize..8,
968            prefill in any::<bool>(),
969            ops in proptest::collection::vec(op_strategy(), 0..256),
970        ) {
971            let mut cache: Clock<u8, u16> = Clock::new(NonZeroUsize::new(cap).unwrap());
972            if prefill {
973                cache.prefill(|| 0u16);
974            }
975            // Oracle: last value written for each live key. A key the cache
976            // reports as present must hold its last-written value (no stale or
977            // conjured values); an evicted key is simply absent.
978            let mut model: HashMap<u8, u16> = HashMap::new();
979            for op in ops {
980                match op {
981                    Op::Get(k) => {
982                        let got = cache.get(&k).copied();
983                        prop_assert_eq!(got, cache.peek(&k).copied());
984                    }
985                    Op::Peek(k) => {
986                        let _ = cache.peek(&k);
987                    }
988                    Op::Put(k, v) => {
989                        cache.put(k, v);
990                        model.insert(k, v);
991                        prop_assert_eq!(cache.peek(&k).copied(), Some(v));
992                    }
993                    Op::GetOrInsert(k, v) => {
994                        let stored = *cache.get_or_insert_with(k, || v);
995                        model.insert(k, stored);
996                        prop_assert_eq!(cache.peek(&k).copied(), Some(stored));
997                    }
998                    Op::GetOrInsertMut(k, v) => {
999                        *cache.get_or_insert_mut(k, || v).1 = v;
1000                        model.insert(k, v);
1001                        prop_assert_eq!(cache.peek(&k).copied(), Some(v));
1002                    }
1003                    Op::GetMut(k, v) => {
1004                        if let Some(slot) = cache.get_mut(&k) {
1005                            *slot = v;
1006                            model.insert(k, v);
1007                        }
1008                    }
1009                    Op::Remove(k) => {
1010                        let had = cache.contains(&k);
1011                        prop_assert_eq!(cache.remove(&k), had);
1012                        model.remove(&k);
1013                        prop_assert!(!cache.contains(&k));
1014                    }
1015                    Op::Retain(k) => {
1016                        cache.retain(|key, _| *key < k);
1017                        model.retain(|key, _| *key < k);
1018                        prop_assert!(cache.len() <= usize::from(k).min(cap));
1019                    }
1020                }
1021                prop_assert!(cache.len() <= cap);
1022                // The slot vector never exceeds capacity, proving reuse.
1023                prop_assert!(cache.slots.len() <= cap);
1024                // Every present key holds its last-written value and was logically
1025                // inserted; absent keys are an allowed (evicted) state.
1026                for k in 0..KEY_SPACE {
1027                    let present = cache.contains(&k);
1028                    prop_assert_eq!(present, cache.peek(&k).is_some());
1029                    if present {
1030                        prop_assert_eq!(cache.peek(&k).copied(), model.get(&k).copied());
1031                    }
1032                }
1033                cache.check_invariants();
1034            }
1035        }
1036    }
1037}