1use std::any::Any;
24use std::collections::HashMap;
25use std::hash::Hash;
26
27#[derive(Debug)]
33pub struct Evicted<K, V> {
34 pub key: K,
36 pub value: V,
38}
39
40impl<K, V> Evicted<K, V> {
41 const fn new(key: K, value: V) -> Self {
43 Self { key, value }
44 }
45}
46
47#[derive(Debug)]
49struct CacheEntry<K, V> {
50 key: K,
51 value: V,
52 ref_bit: bool,
54}
55
56impl<K, V> CacheEntry<K, V> {
57 const fn new(key: K, value: V) -> Self {
58 Self {
59 key,
60 value,
61 ref_bit: false, }
63 }
64}
65
66#[derive(Debug, Clone)]
68struct GhostNode<K> {
69 key: K,
70 prev: Option<usize>,
71 next: Option<usize>,
72}
73
74#[derive(Debug)]
76struct GhostList<K> {
77 entries: Vec<Option<GhostNode<K>>>,
78 head: Option<usize>, tail: Option<usize>, free_slots: Vec<usize>,
81 size: usize,
82}
83
84impl<K: Clone> GhostList<K> {
85 fn new(capacity: usize) -> Self {
86 Self {
87 entries: vec![None; capacity],
88 head: None,
89 tail: None,
90 free_slots: (0..capacity).rev().collect(),
91 size: 0,
92 }
93 }
94
95 fn insert_at_tail(&mut self, key: K) -> Option<(usize, Option<K>)> {
98 let evicted_key = if self.free_slots.is_empty() {
100 self.remove_lru()
101 } else {
102 None
103 };
104
105 let slot = self.free_slots.pop()?;
106 let new_node = GhostNode {
107 key,
108 prev: self.tail,
109 next: None,
110 };
111
112 if let Some(old_tail) = self.tail {
113 if let Some(ref mut old_tail_node) = self.entries[old_tail] {
114 old_tail_node.next = Some(slot);
115 }
116 } else {
117 self.head = Some(slot);
118 }
119
120 self.tail = Some(slot);
121 self.entries[slot] = Some(new_node);
122 self.size += 1;
123
124 Some((slot, evicted_key))
125 }
126
127 fn remove_lru(&mut self) -> Option<K> {
129 let head_slot = self.head?;
130 let head_node = self.entries[head_slot].take()?;
131
132 self.free_slots.push(head_slot);
133 self.size -= 1;
134
135 if self.size == 0 {
136 self.head = None;
137 self.tail = None;
138 } else {
139 self.head = head_node.next;
140 if let Some(new_head) = self.head {
141 if let Some(ref mut new_head_node) = self.entries[new_head] {
142 new_head_node.prev = None;
143 }
144 }
145 }
146
147 Some(head_node.key)
148 }
149
150 fn remove(&mut self, slot: usize) -> bool {
152 let Some(node) = self.entries[slot].take() else {
153 return false;
154 };
155
156 self.free_slots.push(slot);
157 self.size -= 1;
158
159 if self.size == 0 {
160 self.head = None;
161 self.tail = None;
162 } else {
163 if let Some(prev_slot) = node.prev {
164 if let Some(ref mut prev_node) = self.entries[prev_slot] {
165 prev_node.next = node.next;
166 }
167 } else {
168 self.head = node.next;
169 }
170
171 if let Some(next_slot) = node.next {
172 if let Some(ref mut next_node) = self.entries[next_slot] {
173 next_node.prev = node.prev;
174 }
175 } else {
176 self.tail = node.prev;
177 }
178 }
179
180 true
181 }
182
183 const fn len(&self) -> usize {
184 self.size
185 }
186}
187
188#[derive(Debug)]
190struct ClockList<K, V> {
191 entries: Vec<Option<CacheEntry<K, V>>>,
192 hand: usize, free_slots: Vec<usize>,
194 size: usize,
195}
196
197impl<K: Clone, V> ClockList<K, V> {
198 fn new(capacity: usize) -> Self {
199 let mut entries = Vec::with_capacity(capacity);
200 for _ in 0..capacity {
201 entries.push(None);
202 }
203 Self {
204 entries,
205 hand: 0,
206 free_slots: (0..capacity).rev().collect(),
207 size: 0,
208 }
209 }
210
211 fn insert_at_tail(&mut self, key: K, value: V) -> Option<usize> {
213 let slot = self.free_slots.pop()?;
214 self.entries[slot] = Some(CacheEntry::new(key, value));
215 self.size += 1;
216 Some(slot)
217 }
218
219 fn get_head_page(&mut self) -> Option<&mut CacheEntry<K, V>> {
221 let start_hand = self.hand;
223 loop {
224 if self.size == 0 {
225 return None;
226 }
227
228 if self.entries[self.hand].is_some() {
229 return self.entries[self.hand].as_mut();
230 }
231
232 self.advance_hand();
233
234 if self.hand == start_hand {
236 return None;
237 }
238 }
239 }
240
241 fn remove_head_page(&mut self) -> Option<CacheEntry<K, V>> {
243 let entry = self.entries[self.hand].take()?;
244 self.free_slots.push(self.hand);
245 self.size -= 1;
246 self.advance_hand();
247 Some(entry)
248 }
249
250 const fn advance_hand(&mut self) {
251 self.hand = (self.hand + 1) % self.entries.len();
252 }
253
254 fn get_mut(&mut self, slot: usize) -> Option<&mut CacheEntry<K, V>> {
255 self.entries.get_mut(slot)?.as_mut()
256 }
257
258 const fn len(&self) -> usize {
259 self.size
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265enum Location {
266 T1(usize),
267 T2(usize),
268 B1(usize),
269 B2(usize),
270}
271
272pub struct CarCache<K, V> {
274 c: usize,
276 p: usize,
278
279 t1: ClockList<K, V>,
281 t2: ClockList<K, V>,
283 b1: GhostList<K>,
285 b2: GhostList<K>,
287
288 index: HashMap<K, Location>,
290}
291
292impl<K, V> CarCache<K, V>
293where
294 K: Eq + Hash + Clone,
295{
296 #[must_use]
298 pub fn new(capacity: usize) -> Self {
299 Self {
300 c: capacity,
301 p: 0,
302 t1: ClockList::new(capacity),
303 t2: ClockList::new(capacity),
304 b1: GhostList::new(capacity),
305 b2: GhostList::new(capacity),
306 index: HashMap::new(),
307 }
308 }
309
310 pub fn get(&mut self, key: &K) -> Option<&V> {
313 match self.index.get(key) {
314 Some(Location::T1(slot)) => {
315 if let Some(entry) = self.t1.get_mut(*slot) {
317 entry.ref_bit = true; Some(&entry.value)
319 } else {
320 None
321 }
322 }
323 Some(Location::T2(slot)) => {
324 if let Some(entry) = self.t2.get_mut(*slot) {
326 entry.ref_bit = true; Some(&entry.value)
328 } else {
329 None
330 }
331 }
332 _ => None, }
334 }
335
336 pub fn put(&mut self, key: K, value: V) -> Option<Evicted<K, V>> {
340 if let Some(location) = self.index.get(&key).copied() {
342 match location {
343 Location::T1(slot) | Location::T2(slot) => {
344 let entry = if matches!(location, Location::T1(_)) {
346 self.t1.get_mut(slot)
347 } else {
348 self.t2.get_mut(slot)
349 };
350
351 if let Some(entry) = entry {
352 entry.value = value;
353 }
354
355 return None;
357 }
358 _ => {
359 }
361 }
362 }
363
364 let mut evicted = None;
365 if self.t1.len() + self.t2.len() == self.c {
368 evicted = self.replace();
370
371 if !self.is_in_b1_or_b2(&key) && (self.t1.len() + self.b1.len() == self.c) {
373 if let Some(discarded_key) = self.b1.remove_lru() {
375 self.index.remove(&discarded_key);
376 }
377 }
378 else if !self.is_in_b1_or_b2(&key)
380 && (self.t1.len() + self.t2.len() + self.b1.len() + self.b2.len() == 2 * self.c)
381 {
382 if let Some(discarded_key) = self.b2.remove_lru() {
384 self.index.remove(&discarded_key);
385 }
386 }
387 }
388
389 match self.index.get(&key).copied() {
390 Some(Location::B1(slot)) => {
391 let delta = if self.b1.len() > 0 {
394 1.max(self.b2.len() / self.b1.len())
395 } else {
396 1
397 };
398 self.p = (self.p + delta).min(self.c);
399
400 self.b1.remove(slot);
402
403 if let Some(t2_slot) = self.t2.insert_at_tail(key.clone(), value) {
405 self.index.insert(key, Location::T2(t2_slot));
406 }
408 }
409 Some(Location::B2(slot)) => {
410 let delta = if self.b2.len() > 0 {
413 1.max(self.b1.len() / self.b2.len())
414 } else {
415 1
416 };
417 self.p = self.p.saturating_sub(delta);
418
419 self.b2.remove(slot);
421
422 if let Some(t2_slot) = self.t2.insert_at_tail(key.clone(), value) {
424 self.index.insert(key, Location::T2(t2_slot));
425 }
426 }
427 None => {
428 if let Some(t1_slot) = self.t1.insert_at_tail(key.clone(), value) {
431 self.index.insert(key, Location::T1(t1_slot));
432 }
433 }
434 _ => {
435 }
437 }
438 evicted.map(|e| Evicted::new(e.key, e.value))
439 }
440
441 fn replace(&mut self) -> Option<CacheEntry<K, V>> {
443 loop {
445 if self.t1.len() >= 1.max(self.p) {
447 if let Some(found) = self.try_replace_from_t1() {
448 return Some(found);
449 }
450 self.t1.advance_hand();
451 } else {
452 if let Some(found) = self.try_replace_from_t2() {
454 return Some(found);
455 }
456 self.t2.advance_hand();
457 }
458 }
459 }
461
462 fn try_replace_from_t1(&mut self) -> Option<CacheEntry<K, V>> {
464 if let Some(head_entry) = self.t1.get_head_page() {
465 #[allow(clippy::bool_comparison)] if head_entry.ref_bit == false {
469 if let Some(entry) = self.t1.remove_head_page() {
472 if let Some((b1_slot, evicted_key)) = self.b1.insert_at_tail(entry.key.clone())
473 {
474 if let Some(evicted) = evicted_key {
476 self.index.remove(&evicted);
477 }
478 self.index.insert(entry.key.clone(), Location::B1(b1_slot));
479 } else {
480 self.index.remove(&entry.key);
481 }
482 return Some(entry);
483 }
484 } else {
485 head_entry.ref_bit = false; if let Some(entry) = self.t1.remove_head_page() {
488 if let Some(t2_slot) = self.t2.insert_at_tail(entry.key.clone(), entry.value) {
489 self.index.insert(entry.key, Location::T2(t2_slot));
490 }
491 }
492 }
493 }
494 None
495 }
496
497 fn try_replace_from_t2(&mut self) -> Option<CacheEntry<K, V>> {
499 if let Some(head_entry) = self.t2.get_head_page() {
500 #[allow(clippy::bool_comparison)] if head_entry.ref_bit == false {
504 if let Some(entry) = self.t2.remove_head_page() {
507 if let Some((b2_slot, evicted_key)) = self.b2.insert_at_tail(entry.key.clone())
508 {
509 if let Some(evicted) = evicted_key {
511 self.index.remove(&evicted);
512 }
513 self.index.insert(entry.key.clone(), Location::B2(b2_slot));
514 } else {
515 self.index.remove(&entry.key);
516 }
517 return Some(entry);
518 }
519 } else {
520 head_entry.ref_bit = false; if let Some(entry) = self.t2.remove_head_page() {
523 if let Some(t2_slot) = self.t2.insert_at_tail(entry.key.clone(), entry.value) {
524 self.index.insert(entry.key, Location::T2(t2_slot));
525 }
526 }
527 }
528 }
529 None
530 }
531
532 fn is_in_b1_or_b2(&self, key: &K) -> bool {
534 matches!(self.index.get(key), Some(Location::B1(_) | Location::B2(_)))
535 }
536
537 #[must_use]
539 pub const fn len(&self) -> usize {
540 self.t1.len() + self.t2.len()
541 }
542
543 #[must_use]
545 pub const fn is_empty(&self) -> bool {
546 self.len() == 0
547 }
548
549 #[must_use]
551 pub const fn capacity(&self) -> usize {
552 self.c
553 }
554
555 #[must_use]
557 pub const fn adaptation_parameter(&self) -> usize {
558 self.p
559 }
560}
561
562pub(crate) type TypeErasedCarCache<K> = CarCache<K, Box<dyn Any + Send + Sync>>;
563
564impl<K> TypeErasedCarCache<K>
565where
566 K: Eq + Hash + Clone,
567{
568 pub(crate) fn get_typed<T: 'static + Send + Sync>(&mut self, key: &K) -> Option<&T> {
569 self.get(key)?.downcast_ref::<T>()
570 }
571
572 pub(crate) fn put_typed<T: 'static + Send + Sync>(&mut self, key: K, value: T) -> Option<K> {
576 let evicted = self.put(key, Box::new(value) as Box<dyn Any + Send + Sync>);
577 evicted.map(|e| e.key)
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use std::sync::Arc;
584
585 use super::*;
586
587 #[derive(Debug, Clone)]
588 #[allow(dead_code)]
589 struct TypeA {
590 id: String,
591 }
592
593 #[derive(Debug, Clone)]
594 #[allow(dead_code)]
595 struct TypeB {
596 id: String,
597 }
598
599 fn fill_cache_with_invariant_check<K, V>(
600 cache: &mut CarCache<K, V>,
601 items: impl Iterator<Item = (K, V)>,
602 ) where
603 K: Eq + std::hash::Hash + Clone,
604 {
605 for (key, value) in items {
606 cache.put(key, value);
607 assert_car_invariants(cache);
608 }
609 }
610
611 fn access_items_with_invariant_check<K, V>(
612 cache: &mut CarCache<K, V>,
613 keys: impl Iterator<Item = K>,
614 ) where
615 K: Eq + std::hash::Hash + Clone,
616 {
617 for key in keys {
618 cache.get(&key);
619 assert_car_invariants(cache);
620 }
621 }
622
623 fn assert_car_invariants<K, V>(cache: &CarCache<K, V>)
624 where
625 K: Eq + std::hash::Hash + Clone,
626 {
627 let c = cache.capacity();
628 let t1_size = cache.t1.len();
629 let t2_size = cache.t2.len();
630 let b1_size = cache.b1.len();
631 let b2_size = cache.b2.len();
632 let p = cache.adaptation_parameter();
633
634 let state_info = format!(
635 "Cache state: T1={}, T2={}, B1={}, B2={}, c={}, p={}",
636 t1_size, t2_size, b1_size, b2_size, c, p
637 );
638
639 assert!(
641 t1_size + t2_size <= c,
642 "I1 violated: |T1| + |T2| = {} > c = {}. {}",
643 t1_size + t2_size,
644 c,
645 state_info
646 );
647
648 assert!(
650 t1_size + b1_size <= c,
651 "I2 violated: |T1| + |B1| = {} > c = {}. {}",
652 t1_size + b1_size,
653 c,
654 state_info
655 );
656
657 assert!(
659 t2_size + b2_size <= 2 * c,
660 "I3 violated: |T2| + |B2| = {} > 2c = {}. {}",
661 t2_size + b2_size,
662 2 * c,
663 state_info
664 );
665
666 assert!(
668 t1_size + t2_size + b1_size + b2_size <= 2 * c,
669 "I4 violated: |T1| + |T2| + |B1| + |B2| = {} > 2c = {}. {}",
670 t1_size + t2_size + b1_size + b2_size,
671 2 * c,
672 state_info
673 );
674
675 if t1_size + t2_size < c {
677 assert!(
678 b1_size == 0 && b2_size == 0,
679 "I5 violated: |T1| + |T2| = {} < c = {} but B1 or B2 not empty. {}",
680 t1_size + t2_size,
681 c,
682 state_info
683 );
684 }
685
686 if t1_size + b1_size + t2_size + b2_size >= c {
688 assert!(
689 t1_size + t2_size == c,
690 "I6 violated: total directory size {} ≥ c = {} but |T1| + |T2| = {} ≠ c. {}",
691 t1_size + b1_size + t2_size + b2_size,
692 c,
693 t1_size + t2_size,
694 state_info
695 );
696 }
697
698 if t1_size + t2_size == c {
700 assert_eq!(
701 cache.len(),
702 c,
703 "I7: Cache should remain at capacity once full. {}",
704 state_info
705 );
706 }
707
708 assert!(
709 p <= c,
710 "Adaptation parameter p={} should not exceed capacity c={}. {}",
711 p,
712 c,
713 state_info
714 );
715 assert_eq!(
716 cache.len(),
717 t1_size + t2_size,
718 "Cache length mismatch. {}",
719 state_info
720 );
721 }
722
723 fn create_eviction_pressure(cache: &mut CarCache<String, i32>, rounds: i32) {
724 for round in 0..rounds {
725 cache.put(format!("b1_source_{}", round), round + 100);
726 assert_car_invariants(cache);
727
728 cache.put(format!("b2_source_{}", round), round + 200);
729 cache.get(&format!("b2_source_{}", round));
730 assert_car_invariants(cache);
731
732 cache.put(format!("pressure_{}", round), round + 300);
733 assert_car_invariants(cache);
734 }
735 }
736
737 fn promote_all_to_t2(cache: &mut CarCache<i32, i32>, range: std::ops::Range<i32>) {
738 for i in range.clone() {
739 cache.put(i, i);
740 cache.get(&i);
741 assert_car_invariants(cache);
742 }
743 }
744
745 fn create_t1_t2_mix(cache: &mut CarCache<String, i32>, prefix: &str, count: i32) {
746 fill_cache_with_invariant_check(
747 cache,
748 (0..count).map(|i| (format!("{}_{}", prefix, i), i)),
749 );
750 access_items_with_invariant_check(
751 cache,
752 (0..count / 2).map(|i| format!("{}_{}", prefix, i)),
753 );
754 }
755
756 fn verify_directory_state<K, V>(cache: &CarCache<K, V>) -> (usize, usize, usize, usize, usize)
757 where
758 K: Eq + std::hash::Hash + Clone,
759 {
760 let t1_size = cache.t1.len();
761 let t2_size = cache.t2.len();
762 let b1_size = cache.b1.len();
763 let b2_size = cache.b2.len();
764 let total = t1_size + t2_size + b1_size + b2_size;
765
766 (t1_size, t2_size, b1_size, b2_size, total)
767 }
768
769 fn create_ghost_hits(
770 cache: &mut CarCache<String, i32>,
771 prefix: &str,
772 range: std::ops::Range<i32>,
773 value_offset: i32,
774 ) {
775 for i in range {
776 cache.put(format!("{}_{}", prefix, i), i + value_offset);
777 assert_car_invariants(cache);
778 }
779 }
780
781 #[test]
782 fn test_ghost_list_basic_operations() {
783 let mut ghost_list = GhostList::new(3);
784
785 assert_eq!(ghost_list.len(), 0);
786 assert_eq!(ghost_list.remove_lru(), None);
787
788 let (_slot1, _) = ghost_list.insert_at_tail("a").unwrap();
789 assert_eq!(ghost_list.len(), 1);
790
791 let (slot2, _) = ghost_list.insert_at_tail("b").unwrap();
792 assert_eq!(ghost_list.len(), 2);
793
794 assert_eq!(ghost_list.remove_lru(), Some("a"));
795 assert_eq!(ghost_list.len(), 1);
796
797 assert!(ghost_list.remove(slot2));
798 assert_eq!(ghost_list.len(), 0);
799 }
800
801 #[test]
802 fn test_clock_list_basic_operations() {
803 let mut clock_list = ClockList::new(3);
804
805 assert_eq!(clock_list.len(), 0);
806 assert!(clock_list.get_head_page().is_none());
807
808 let slot1 = clock_list.insert_at_tail("a", 1).unwrap();
809 assert_eq!(clock_list.len(), 1);
810
811 let slot2 = clock_list.insert_at_tail("b", 2).unwrap();
812 assert_eq!(clock_list.len(), 2);
813
814 assert_eq!(clock_list.get_mut(slot1).unwrap().value, 1);
815 assert_eq!(clock_list.get_mut(slot2).unwrap().value, 2);
816
817 let entry = clock_list.get_mut(slot1).unwrap();
818 assert_eq!(entry.ref_bit, false);
819 }
820
821 #[test]
822 fn test_adaptation_parameter_increase_on_b1_hit() {
823 let mut cache = CarCache::new(4);
824
825 cache.put("a", 1);
826 cache.put("b", 2);
827 cache.put("c", 3);
828
829 let initial_p = cache.adaptation_parameter();
830 cache.get(&"a");
831
832 cache.put("e", 5);
833 cache.put("f", 6);
834
835 cache.put("c", 10);
836
837 assert!(cache.adaptation_parameter() > initial_p);
838 assert!(cache.adaptation_parameter() <= cache.capacity());
839 }
840
841 #[test]
842 fn test_adaptation_parameter_decrease_on_b2_hit() {
843 let mut cache = CarCache::new(4);
844
845 cache.put("a", 1);
846 cache.put("b", 2);
847 cache.put("c", 3);
848
849 cache.get(&"a");
850
851 cache.put("e", 5);
852 cache.put("f", 6);
853
854 cache.put("c", 10);
855
856 let p_before = cache.adaptation_parameter();
857
858 cache.put("f", 6);
859 cache.get(&"f");
860 cache.put("g", 7);
861 cache.get(&"g");
862 cache.put("x", 7);
863 cache.put("y", 7);
864 cache.put("z", 7);
865
866 cache.put("a", 10);
867
868 assert!(cache.adaptation_parameter() < p_before);
869 }
870
871 #[test]
872 fn test_clock_algorithm_reference_bit_behavior() {
873 let mut cache = CarCache::new(3);
874
875 cache.put("a", 1);
876 cache.put("b", 2);
877 cache.put("c", 3);
878
879 cache.get(&"a");
880
881 cache.put("d", 4);
882 cache.put("e", 5);
883
884 assert!(cache.get(&"a").is_some());
885 assert!(cache.len() <= 3);
886 }
887
888 #[test]
889 fn test_ghost_list_lru_behavior() {
890 let mut ghost_list = GhostList::new(3);
891
892 let _ = ghost_list.insert_at_tail("first");
893 let _ = ghost_list.insert_at_tail("second");
894 let _ = ghost_list.insert_at_tail("third");
895
896 assert_eq!(ghost_list.remove_lru(), Some("first"));
897 assert_eq!(ghost_list.remove_lru(), Some("second"));
898 assert_eq!(ghost_list.remove_lru(), Some("third"));
899 assert_eq!(ghost_list.remove_lru(), None);
900 }
901
902 #[test]
903 fn test_directory_replacement_constraints() {
904 let mut cache = CarCache::new(3);
905
906 cache.put("a", 1);
907 cache.put("b", 2);
908 cache.get(&"a");
909 cache.put("c", 3);
910 cache.get(&"c");
911 cache.put("d", 4);
912 cache.put("e", 5);
913
914 assert_eq!(cache.t1.len(), 1);
915 assert_eq!(cache.t2.len(), 2);
916 }
917
918 #[test]
919 fn test_large_cache_reference_bit_behavior() {
920 let mut cache = CarCache::new(1000);
921
922 for i in 0..800 {
923 cache.put(format!("frequent_{}", i), i);
924 cache.get(&format!("frequent_{}", i)); }
926
927 for i in 0..200 {
928 cache.put(format!("rare_{}", i), i);
929 }
930
931 for i in 0..400 {
932 cache.put(format!("new_{}", i), i);
933 }
934
935 let frequent_survivors = (0..800)
936 .filter(|&i| cache.get(&format!("frequent_{}", i)).is_some())
937 .count();
938
939 let rare_survivors = (0..200)
940 .filter(|&i| cache.get(&format!("rare_{}", i)).is_some())
941 .count();
942
943 assert!(frequent_survivors as f64 / 800.0 >= rare_survivors as f64 / 200.0);
944 }
945
946 #[test]
947 fn test_large_cache_scan_resistance() {
948 let mut cache = CarCache::new(1000);
949
950 let working_set: Vec<String> = (0..200).map(|i| format!("working_{}", i)).collect();
951 for key in &working_set {
952 cache.put(key.clone(), 1);
953 cache.get(key);
954 }
955
956 for i in 0..800 {
957 cache.put(format!("filler_{}", i), i);
958 }
959
960 for i in 0..500 {
961 cache.put(format!("scan_{}", i), i);
962 }
963
964 let survivors = working_set
965 .iter()
966 .filter(|key| cache.get(key).is_some())
967 .count();
968
969 assert_eq!(survivors, 200);
970 assert_eq!(cache.len(), cache.capacity());
971 assert!(cache.adaptation_parameter() <= cache.capacity());
972 }
973
974 #[test]
975 fn test_cache_adaptation_bounds() {
976 let mut cache = CarCache::new(10);
977 let mut p_values = Vec::new();
978
979 let working_set = (0..15).map(|i| format!("item_{}", i)).collect::<Vec<_>>();
980
981 for i in 0..8 {
982 cache.put(working_set[i].clone(), i);
983 }
984
985 for i in 0..4 {
986 cache.get(&working_set[i]);
987 }
988
989 p_values.push(cache.adaptation_parameter());
990 for cycle in 0..3 {
991 for (round, item) in working_set.iter().enumerate() {
992 cache.put(item.clone(), cycle * 100 + round);
993
994 let p_after = cache.adaptation_parameter();
995 p_values.push(p_after);
996
997 assert!(
998 p_after <= cache.capacity(),
999 "Adaptation parameter {} exceeds capacity {} at cycle {} round {}",
1000 p_after,
1001 cache.capacity(),
1002 cycle,
1003 round
1004 );
1005
1006 if round % 3 == 0 && round > 0 {
1007 cache.get(&working_set[round - 1]);
1008 }
1009 }
1010 }
1011
1012 for (i, &p) in p_values.iter().enumerate() {
1013 assert!(
1014 p <= cache.capacity(),
1015 "p={} > c={} at step {}",
1016 p,
1017 cache.capacity(),
1018 i
1019 );
1020 }
1021
1022 let p_changed = p_values.iter().any(|&p| p != p_values[0]);
1023 assert!(
1024 p_changed,
1025 "NOTE: Adaptation parameter remained at {} (may need different workload)",
1026 p_values[0]
1027 );
1028 assert_eq!(cache.adaptation_parameter(), 5);
1029 }
1030
1031 #[test]
1032 fn test_put_return_values_eviction() {
1033 let mut cache = CarCache::new(3);
1034
1035 assert!(cache.put("a", 1).is_none());
1036 assert!(cache.put("b", 2).is_none());
1037 assert!(cache.put("c", 3).is_none());
1038
1039 let evicted = cache.put("d", 4);
1041 assert!(evicted.is_some());
1042 let evicted = evicted.unwrap();
1043 assert_eq!(evicted.key, "a");
1044 assert_eq!(evicted.value, 1);
1045
1046 let evicted = cache.put("e", 5);
1047 assert!(evicted.is_some());
1048 let evicted = evicted.unwrap();
1049 assert_eq!(evicted.key, "b");
1050 assert_eq!(evicted.value, 2);
1051
1052 assert_eq!(cache.get(&"a"), None);
1053 assert_eq!(cache.get(&"b"), None);
1054 assert_eq!(cache.get(&"c"), Some(&3));
1055 assert_eq!(cache.get(&"d"), Some(&4));
1056 assert_eq!(cache.get(&"e"), Some(&5));
1057 }
1058
1059 #[test]
1060 fn test_put_return_values_t1_t2_eviction() {
1061 let mut cache = CarCache::new(4);
1062
1063 assert!(cache.put("t1_a", 1).is_none());
1064 assert!(cache.put("t1_b", 2).is_none());
1065
1066 cache.get(&"t1_a");
1067 cache.get(&"t1_b");
1068
1069 assert!(cache.put("t1_c", 3).is_none());
1070 assert!(cache.put("t1_d", 4).is_none());
1071
1072 let evicted = cache.put("new1", 10);
1073 assert!(evicted.is_some());
1074 assert_eq!(evicted.unwrap().value, 3);
1075 }
1076
1077 #[test]
1078 fn test_car_invariants_i3_stress() {
1079 let mut cache = CarCache::new(5);
1080
1081 promote_all_to_t2(&mut cache, 0..5);
1082
1083 for i in 5..20 {
1084 cache.put(i, i);
1085 assert_car_invariants(&cache);
1086 cache.get(&i);
1087 assert_car_invariants(&cache);
1088 }
1089
1090 fill_cache_with_invariant_check(&mut cache, (0..5).map(|i| (i, i + 100)));
1091
1092 let (_, t2_size, _, b2_size, _) = verify_directory_state(&cache);
1093 assert!(
1094 t2_size + b2_size > 0,
1095 "Should have some T2/B2 entries to test I3"
1096 );
1097 }
1098
1099 #[test]
1100 fn test_car_invariants_i4_maximum_directory() {
1101 let mut cache = CarCache::new(8);
1102
1103 create_t1_t2_mix(&mut cache, "t1", 8);
1104 create_eviction_pressure(&mut cache, 10);
1105 create_ghost_hits(&mut cache, "t1", 0..4, 1000);
1106
1107 let (_, _, _, _, total) = verify_directory_state(&cache);
1108 let max_allowed = 2 * cache.capacity();
1109
1110 assert!(
1111 total >= cache.capacity(),
1112 "Directory should be substantial for meaningful I4 test"
1113 );
1114 assert!(
1115 total <= max_allowed,
1116 "I4: Directory size {} should not exceed 2c={}",
1117 total,
1118 max_allowed
1119 );
1120 }
1121
1122 #[test]
1123 fn test_car_invariant_i6_directory_full_cache_full() {
1124 let mut cache = CarCache::new(6);
1125
1126 create_t1_t2_mix(&mut cache, "initial", 6);
1127
1128 for i in 6..15 {
1129 cache.put(format!("evict_{}", i), i);
1130 assert_car_invariants(&cache);
1131
1132 if i % 2 == 0 {
1133 cache.get(&format!("evict_{}", i));
1134 assert_car_invariants(&cache);
1135 }
1136 }
1137
1138 create_ghost_hits(&mut cache, "initial", 0..3, 1000);
1139
1140 let (t1_size, t2_size, _b1_size, _b2_size, total_dir) = verify_directory_state(&cache);
1141
1142 if total_dir >= cache.capacity() {
1143 assert_eq!(
1144 t1_size + t2_size,
1145 cache.capacity(),
1146 "I6: When directory size {} ≥ c={}, cache should be full but |T1|+|T2|={}",
1147 total_dir,
1148 cache.capacity(),
1149 t1_size + t2_size
1150 );
1151 } else {
1152 panic!(
1153 "Test setup failed: Directory size {} should be ≥ c={}",
1154 total_dir,
1155 cache.capacity()
1156 );
1157 }
1158 }
1159
1160 #[test]
1161 fn test_car_invariant_i7_cache_remains_full() {
1162 let mut cache = CarCache::new(8);
1163
1164 for i in 0..8 {
1165 cache.put(format!("fill_{}", i), i);
1166 assert_car_invariants(&cache);
1167 }
1168
1169 assert_eq!(cache.len(), cache.capacity(), "Cache should be at capacity");
1170
1171 for round in 0..20 {
1172 cache.put(format!("new_{}", round), round + 100);
1173 assert_car_invariants(&cache);
1174 assert_eq!(
1175 cache.len(),
1176 cache.capacity(),
1177 "I7: Cache should remain full after adding new item in round {}",
1178 round
1179 );
1180
1181 cache.get(&format!("new_{}", round));
1182 assert_car_invariants(&cache);
1183 assert_eq!(
1184 cache.len(),
1185 cache.capacity(),
1186 "I7: Cache should remain full after accessing item in round {}",
1187 round
1188 );
1189
1190 cache.put(format!("new_{}", round), round + 200);
1191 assert_car_invariants(&cache);
1192 assert_eq!(
1193 cache.len(),
1194 cache.capacity(),
1195 "I7: Cache should remain full after updating item in round {}",
1196 round
1197 );
1198
1199 if round > 5 {
1200 cache.put(format!("fill_{}", round % 8), round + 300);
1201 assert_car_invariants(&cache);
1202 assert_eq!(
1203 cache.len(),
1204 cache.capacity(),
1205 "I7: Cache should remain full after B1/B2 hit in round {}",
1206 round
1207 );
1208 }
1209 }
1210 }
1211
1212 #[test]
1213 fn test_put_typed_works_across_types() {
1214 let mut cache: TypeErasedCarCache<String> = CarCache::new(2);
1215
1216 let evicted_key = cache.put_typed("key1".to_string(), Arc::new(TypeA { id: "1".into() }));
1217 assert!(evicted_key.is_none());
1218
1219 let evicted_key = cache.put_typed("key2".to_string(), Arc::new(TypeA { id: "2".into() }));
1220 assert!(evicted_key.is_none());
1221
1222 let evicted_key = cache.put_typed("key3".to_string(), Arc::new(TypeB { id: "3".into() }));
1223
1224 assert!(evicted_key.is_some(),);
1225
1226 let evicted_key = evicted_key.unwrap();
1227 assert!(evicted_key == "key1" || evicted_key == "key2",);
1228
1229 let key_in_cache = cache.get_typed::<Arc<TypeA>>(&evicted_key).is_some();
1230 assert!(!key_in_cache,);
1231 }
1232}