1use cranpose_core::collections::map::HashMap;
2use std::hash::Hash;
3use std::num::NonZeroUsize;
4
5struct CacheSlot<K, V> {
10 key: K,
11 value: V,
12 newer: Option<usize>,
13 older: Option<usize>,
14}
15
16pub struct BoundedLruCache<K, V> {
34 index: HashMap<K, usize>,
35 slots: Vec<Option<CacheSlot<K, V>>>,
36 free: Vec<usize>,
37 newest: Option<usize>,
39 oldest: Option<usize>,
41 cap: NonZeroUsize,
42}
43
44impl<K, V> BoundedLruCache<K, V>
45where
46 K: Clone + Eq + Hash,
47{
48 pub fn new(cap: NonZeroUsize) -> Self {
49 Self {
50 index: HashMap::with_capacity(cap.get()),
51 slots: Vec::with_capacity(cap.get()),
52 free: Vec::new(),
53 newest: None,
54 oldest: None,
55 cap,
56 }
57 }
58
59 pub fn with_capacity_at_least_one(cap: usize) -> Self {
60 let cap = NonZeroUsize::new(cap).unwrap_or(NonZeroUsize::MIN);
61 Self::new(cap)
62 }
63
64 pub fn len(&self) -> usize {
65 self.index.len()
66 }
67
68 pub fn is_empty(&self) -> bool {
69 self.index.is_empty()
70 }
71
72 pub fn cap(&self) -> NonZeroUsize {
73 self.cap
74 }
75
76 pub fn contains(&self, key: &K) -> bool {
77 self.index.contains_key(key)
78 }
79
80 pub fn get(&mut self, key: &K) -> Option<&V> {
81 let slot = *self.index.get(key)?;
82 self.promote(slot);
83 Some(&self.slot(slot).value)
84 }
85
86 pub fn peek(&self, key: &K) -> Option<&V> {
87 let slot = *self.index.get(key)?;
88 Some(&self.slot(slot).value)
89 }
90
91 pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
92 let slot = *self.index.get(key)?;
93 self.promote(slot);
94 Some(&mut self.slot_mut(slot).value)
95 }
96
97 pub fn push(&mut self, key: K, value: V) -> Option<(K, V)> {
98 if let Some(&slot) = self.index.get(&key) {
99 self.promote(slot);
100 let old_value = std::mem::replace(&mut self.slot_mut(slot).value, value);
101 return Some((key, old_value));
102 }
103
104 let evicted = if self.index.len() == self.cap.get() {
105 self.pop_lru()
106 } else {
107 None
108 };
109
110 let slot = self.claim_slot(key.clone(), value);
111 self.index.insert(key, slot);
112 self.link_newest(slot);
113 evicted
114 }
115
116 pub fn put(&mut self, key: K, value: V) -> Option<V> {
117 self.push(key, value).map(|(_, value)| value)
118 }
119
120 pub fn pop_lru(&mut self) -> Option<(K, V)> {
121 let slot = self.oldest?;
122 self.unlink(slot);
123 let entry = self.release_slot(slot);
124 self.index.remove(&entry.key);
125 Some((entry.key, entry.value))
126 }
127
128 pub fn pop(&mut self, key: &K) -> Option<V> {
129 let slot = self.index.remove(key)?;
130 self.unlink(slot);
131 Some(self.release_slot(slot).value)
132 }
133
134 pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
136 let mut next = self.newest;
137 std::iter::from_fn(move || {
138 let entry = self.slot(next?);
139 next = entry.older;
140 Some((&entry.key, &entry.value))
141 })
142 }
143
144 fn slot(&self, slot: usize) -> &CacheSlot<K, V> {
146 self.slots[slot]
147 .as_ref()
148 .expect("a linked cache slot is always occupied")
149 }
150
151 fn slot_mut(&mut self, slot: usize) -> &mut CacheSlot<K, V> {
152 self.slots[slot]
153 .as_mut()
154 .expect("a linked cache slot is always occupied")
155 }
156
157 fn promote(&mut self, slot: usize) {
159 if self.newest == Some(slot) {
160 return;
161 }
162 self.unlink(slot);
163 self.link_newest(slot);
164 }
165
166 fn link_newest(&mut self, slot: usize) {
167 let previous_newest = self.newest;
168 {
169 let entry = self.slot_mut(slot);
170 entry.newer = None;
171 entry.older = previous_newest;
172 }
173 if let Some(previous) = previous_newest {
174 self.slot_mut(previous).newer = Some(slot);
175 }
176 self.newest = Some(slot);
177 if self.oldest.is_none() {
178 self.oldest = Some(slot);
179 }
180 }
181
182 fn unlink(&mut self, slot: usize) {
183 let (newer, older) = {
184 let entry = self.slot_mut(slot);
185 (entry.newer.take(), entry.older.take())
186 };
187 match newer {
188 Some(newer) => self.slot_mut(newer).older = older,
189 None => self.newest = older,
190 }
191 match older {
192 Some(older) => self.slot_mut(older).newer = newer,
193 None => self.oldest = newer,
194 }
195 }
196
197 fn claim_slot(&mut self, key: K, value: V) -> usize {
202 let entry = CacheSlot {
203 key,
204 value,
205 newer: None,
206 older: None,
207 };
208 match self.free.pop() {
209 Some(slot) => {
210 self.slots[slot] = Some(entry);
211 slot
212 }
213 None => {
214 self.slots.push(Some(entry));
215 self.slots.len() - 1
216 }
217 }
218 }
219
220 fn release_slot(&mut self, slot: usize) -> CacheSlot<K, V> {
221 let entry = self.slots[slot]
222 .take()
223 .expect("a slot being released is always occupied");
224 self.free.push(slot);
225 entry
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::BoundedLruCache;
232
233 fn cache<K, V>(cap: usize) -> BoundedLruCache<K, V>
234 where
235 K: Clone + Eq + std::hash::Hash,
236 {
237 BoundedLruCache::with_capacity_at_least_one(cap)
238 }
239
240 #[test]
241 fn clamped_constructor_uses_minimum_nonzero_capacity() {
242 let mut cache = BoundedLruCache::with_capacity_at_least_one(0);
243 assert_eq!(cache.cap().get(), 1);
244 assert_eq!(cache.push("a", 1), None);
245 assert_eq!(cache.push("b", 2), Some(("a", 1)));
246 assert_eq!(cache.get(&"b"), Some(&2));
247 }
248
249 #[test]
250 fn get_promotes_entry_and_push_evicts_lru() {
251 let mut cache = cache(2);
252 assert_eq!(cache.push("a", 1), None);
253 assert_eq!(cache.push("b", 2), None);
254
255 assert_eq!(cache.get(&"a"), Some(&1));
256 assert_eq!(cache.push("c", 3), Some(("b", 2)));
257
258 assert!(cache.contains(&"a"));
259 assert!(cache.contains(&"c"));
260 assert!(!cache.contains(&"b"));
261 }
262
263 #[test]
264 fn push_existing_replaces_value_and_keeps_capacity() {
265 let mut cache = cache(2);
266 cache.push("a", 1);
267 cache.push("b", 2);
268
269 assert_eq!(cache.push("a", 3), Some(("a", 1)));
270 assert_eq!(cache.len(), 2);
271 assert_eq!(cache.get(&"a"), Some(&3));
272 }
273
274 #[test]
275 fn pop_removes_requested_entry_and_preserves_lru_order() {
276 let mut cache = cache(3);
277 cache.push("a", 1);
278 cache.push("b", 2);
279 cache.push("c", 3);
280
281 assert_eq!(cache.pop(&"b"), Some(2));
282 assert_eq!(cache.get(&"a"), Some(&1));
283 assert_eq!(cache.pop_lru(), Some(("c", 3)));
284 assert_eq!(cache.len(), 1);
285 }
286
287 #[test]
288 fn a_full_cache_that_only_misses_keeps_evicting_in_order() {
289 let mut cache = cache(4);
293 for step in 0..4 {
294 assert_eq!(cache.push(step, step * 10), None);
295 }
296
297 for step in 4..64 {
298 let evicted = cache.push(step, step * 10);
299 assert_eq!(
300 evicted,
301 Some((step - 4, (step - 4) * 10)),
302 "insert {step} must evict the oldest entry"
303 );
304 assert_eq!(cache.len(), 4);
305 }
306
307 let live: Vec<_> = cache.iter().map(|(key, value)| (*key, *value)).collect();
308 assert_eq!(live, vec![(63, 630), (62, 620), (61, 610), (60, 600)]);
309 }
310
311 #[test]
312 fn reused_slots_do_not_resurrect_the_entries_that_vacated_them() {
313 let mut cache = cache(3);
314 cache.push("a", 1);
315 cache.push("b", 2);
316 cache.push("c", 3);
317
318 assert_eq!(cache.pop(&"b"), Some(2));
319 assert_eq!(cache.push("d", 4), None);
321
322 assert!(!cache.contains(&"b"));
323 assert_eq!(cache.peek(&"d"), Some(&4));
324 assert_eq!(cache.len(), 3);
325 assert_eq!(cache.pop_lru(), Some(("a", 1)));
327 assert_eq!(cache.pop_lru(), Some(("c", 3)));
328 assert_eq!(cache.pop_lru(), Some(("d", 4)));
329 assert_eq!(cache.pop_lru(), None);
330 assert!(cache.is_empty());
331 }
332
333 #[test]
334 fn a_promoted_entry_survives_the_next_eviction() {
335 let mut cache = cache(3);
336 cache.push("a", 1);
337 cache.push("b", 2);
338 cache.push("c", 3);
339
340 assert_eq!(cache.get(&"a"), Some(&1));
341 assert_eq!(cache.get_mut(&"b").map(|value| *value), Some(2));
342
343 assert_eq!(cache.push("d", 4), Some(("c", 3)));
345 assert!(cache.contains(&"a"));
346 assert!(cache.contains(&"b"));
347 }
348
349 #[test]
350 fn peek_reads_without_promoting_entry() {
351 let mut cache = cache(2);
352 cache.push("a", 1);
353 cache.push("b", 2);
354
355 assert_eq!(cache.peek(&"a"), Some(&1));
356 assert_eq!(cache.push("c", 3), Some(("a", 1)));
357 }
358
359 #[test]
360 fn iter_reports_mru_to_lru_entries() {
361 let mut cache = cache(3);
362 cache.push("a", 1);
363 cache.push("b", 2);
364 cache.get(&"a");
365
366 let entries: Vec<_> = cache.iter().map(|(key, value)| (*key, *value)).collect();
367 assert_eq!(entries, vec![("a", 1), ("b", 2)]);
368 }
369}