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