1#![cfg_attr(not(test), no_std)]
23#![forbid(unsafe_code)]
24
25const NONE: u16 = u16::MAX;
27
28#[derive(Copy, Clone, PartialEq, Eq, Debug)]
30pub enum SlotState {
31 Empty,
33 Loading,
35 Ready,
37}
38
39#[derive(Copy, Clone, PartialEq, Eq, Debug)]
44pub struct Reservation {
45 slot: u16,
46 key: u16,
47}
48
49impl Reservation {
50 fn new(slot: usize, key: u16) -> Self {
51 Self {
54 slot: slot as u16,
55 key,
56 }
57 }
58
59 pub const fn slot(&self) -> usize {
61 self.slot as usize
62 }
63
64 pub const fn key(&self) -> u16 {
66 self.key
67 }
68}
69
70#[derive(Copy, Clone, PartialEq, Eq, Debug)]
73pub struct Stale;
74
75#[derive(Copy, Clone)]
76struct Slot<V: Copy> {
77 key: u16,
78 value: Option<V>,
79 state: SlotState,
80 last_used: u32,
82 pinned: bool,
84}
85
86pub struct SlotCache<V: Copy, const N: usize, const MAX_KEY: usize> {
88 slots: [Slot<V>; N],
89 key_to_slot: [u16; MAX_KEY],
90 epoch: u32,
91}
92
93impl<V: Copy, const N: usize, const MAX_KEY: usize> Default for SlotCache<V, N, MAX_KEY> {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99impl<V: Copy, const N: usize, const MAX_KEY: usize> SlotCache<V, N, MAX_KEY> {
100 pub const fn new() -> Self {
102 Self {
103 slots: [Slot {
104 key: NONE,
105 value: None,
106 state: SlotState::Empty,
107 last_used: 0,
108 pinned: false,
109 }; N],
110 key_to_slot: [NONE; MAX_KEY],
111 epoch: 0,
112 }
113 }
114
115 pub const fn capacity(&self) -> usize {
117 N
118 }
119
120 pub fn bump_epoch(&mut self) {
125 self.epoch = self.epoch.wrapping_add(1).max(1);
126 }
127
128 pub const fn epoch(&self) -> u32 {
130 self.epoch
131 }
132
133 fn key_ok(key: u16) -> bool {
134 (key as usize) < MAX_KEY && key != NONE
135 }
136
137 pub fn slot_of(&self, key: u16) -> Option<usize> {
140 if !Self::key_ok(key) {
141 return None;
142 }
143 let raw = self.key_to_slot[key as usize];
144 if raw == NONE {
145 return None;
146 }
147 let s = raw as usize;
148 if s < N && self.slots[s].state != SlotState::Empty && self.slots[s].key == key {
149 Some(s)
150 } else {
151 None
152 }
153 }
154
155 pub fn contains_ready(&self, key: u16) -> bool {
157 self.slot_of(key)
158 .is_some_and(|s| self.slots[s].state == SlotState::Ready)
159 }
160
161 pub fn is_loading(&self, key: u16) -> bool {
163 self.slot_of(key)
164 .is_some_and(|s| self.slots[s].state == SlotState::Loading)
165 }
166
167 pub fn state_of(&self, key: u16) -> SlotState {
169 match self.slot_of(key) {
170 Some(s) => self.slots[s].state,
171 None => SlotState::Empty,
172 }
173 }
174
175 pub fn get(&mut self, key: u16) -> Option<&V> {
178 let s = self.slot_of(key)?;
179 if self.slots[s].state != SlotState::Ready {
180 return None;
181 }
182 self.slots[s].last_used = self.epoch;
183 self.slots[s].value.as_ref()
184 }
185
186 pub fn peek(&self, key: u16) -> Option<&V> {
188 let s = self.slot_of(key)?;
189 if self.slots[s].state != SlotState::Ready {
190 return None;
191 }
192 self.slots[s].value.as_ref()
193 }
194
195 pub fn begin_load(&mut self, key: u16) -> Option<Reservation> {
203 if !Self::key_ok(key) {
204 return None;
205 }
206 if let Some(s) = self.slot_of(key) {
207 return Some(Reservation::new(s, key));
208 }
209 let s = self.alloc_slot()?;
210 self.detach(s);
211 self.slots[s] = Slot {
212 key,
213 value: None,
214 state: SlotState::Loading,
215 last_used: self.epoch,
216 pinned: false,
217 };
218 self.key_to_slot[key as usize] = s as u16;
219 Some(Reservation::new(s, key))
220 }
221
222 pub fn finish_load(&mut self, reservation: Reservation, value: V) -> Result<(), Stale> {
227 let slot = reservation.slot();
228 if slot < N
229 && self.slots[slot].state != SlotState::Empty
230 && self.slots[slot].key == reservation.key
231 {
232 self.slots[slot].value = Some(value);
233 self.slots[slot].state = SlotState::Ready;
234 self.slots[slot].last_used = self.epoch;
235 Ok(())
236 } else {
237 Err(Stale)
238 }
239 }
240
241 #[deprecated(note = "renamed to `begin_load`")]
244 pub fn reserve(&mut self, key: u16) -> Option<usize> {
245 self.begin_load(key).map(|r| r.slot())
246 }
247
248 #[deprecated(note = "renamed to `finish_load`, which checks the key still owns the slot")]
253 pub fn mark_ready(&mut self, slot: usize, value: V) {
254 if slot < N && self.slots[slot].state != SlotState::Empty {
255 self.slots[slot].value = Some(value);
256 self.slots[slot].state = SlotState::Ready;
257 self.slots[slot].last_used = self.epoch;
258 }
259 }
260
261 pub fn get_or_insert_with<F>(&mut self, key: u16, build: F) -> Option<&V>
265 where
266 F: FnOnce() -> Option<V>,
267 {
268 if self.contains_ready(key) {
269 return self.get(key);
270 }
271 let r = self.begin_load(key)?;
272 let s = r.slot();
273 match build() {
274 Some(v) => {
275 let _ = self.finish_load(r, v);
277 self.slots[s].value.as_ref()
278 }
279 None => {
280 self.evict_slot(s);
281 None
282 }
283 }
284 }
285
286 pub fn pin(&mut self, key: u16) {
288 if let Some(s) = self.slot_of(key) {
289 self.slots[s].pinned = true;
290 }
291 }
292
293 pub fn unpin_all(&mut self) {
295 for s in self.slots.iter_mut() {
296 s.pinned = false;
297 }
298 }
299
300 pub fn set_pinned(&mut self, keys: &[u16]) {
304 self.unpin_all();
305 for &k in keys {
306 self.pin(k);
307 }
308 }
309
310 pub fn evict(&mut self, key: u16) {
312 if let Some(s) = self.slot_of(key) {
313 self.evict_slot(s);
314 }
315 }
316
317 pub fn evict_ready_outside(&mut self, keep: &[u16]) -> usize {
321 let mut evicted = 0;
322 for s in 0..N {
323 if self.slots[s].state == SlotState::Ready
324 && !self.slots[s].pinned
325 && !keep.contains(&self.slots[s].key)
326 {
327 self.evict_slot(s);
328 evicted += 1;
329 }
330 }
331 evicted
332 }
333
334 pub fn ready_len(&self) -> usize {
336 self.slots
337 .iter()
338 .filter(|s| s.state == SlotState::Ready)
339 .count()
340 }
341
342 pub fn len(&self) -> usize {
344 self.slots
345 .iter()
346 .filter(|s| s.state != SlotState::Empty)
347 .count()
348 }
349
350 pub fn is_empty(&self) -> bool {
352 self.len() == 0
353 }
354
355 fn alloc_slot(&mut self) -> Option<usize> {
359 let mut victim: Option<usize> = None;
360 for s in 0..N {
361 match self.slots[s].state {
362 SlotState::Empty => return Some(s),
363 SlotState::Ready if !self.slots[s].pinned => {
364 let better = match victim {
365 None => true,
366 Some(v) => self.slots[s].last_used < self.slots[v].last_used,
367 };
368 if better {
369 victim = Some(s);
370 }
371 }
372 _ => {}
373 }
374 }
375 victim
376 }
377
378 fn detach(&mut self, slot: usize) {
380 let s = &self.slots[slot];
381 if s.state != SlotState::Empty {
382 let k = s.key as usize;
383 if k < MAX_KEY && self.key_to_slot[k] == slot as u16 {
384 self.key_to_slot[k] = NONE;
385 }
386 }
387 }
388
389 fn evict_slot(&mut self, slot: usize) {
390 self.detach(slot);
391 self.slots[slot] = Slot {
392 key: NONE,
393 value: None,
394 state: SlotState::Empty,
395 last_used: 0,
396 pinned: false,
397 };
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 type Cache = SlotCache<u32, 3, 16>;
406
407 fn put(c: &mut Cache, key: u16, val: u32) {
409 let r = c.begin_load(key).expect("begin_load");
410 c.finish_load(r, val).unwrap();
411 }
412
413 #[test]
414 fn empty_start() {
415 let c = Cache::new();
416 assert!(c.is_empty());
417 assert_eq!(c.ready_len(), 0);
418 assert!(!c.contains_ready(0));
419 }
420
421 #[test]
422 fn insert_then_get_roundtrips_and_slot_is_stable() {
423 let mut c = Cache::new();
424 let r = c.begin_load(5).unwrap();
425 let s = r.slot();
426 assert!(c.is_loading(5));
427 c.finish_load(r, 500).unwrap();
428 assert!(c.contains_ready(5));
429 assert_eq!(c.get(5), Some(&500));
430 assert_eq!(c.slot_of(5), Some(s));
431 assert_eq!(c.begin_load(5), Some(r));
433 }
434
435 #[test]
436 fn a_load_that_finishes_after_its_slot_was_reused_is_refused() {
437 let mut c = SlotCache::<u32, 1, 16>::new();
440 let stale = c.begin_load(1).unwrap();
441 c.evict(1);
442 let live = c.begin_load(2).unwrap();
443 assert_eq!(live.slot(), stale.slot(), "the one slot is reused");
444
445 assert_eq!(c.finish_load(stale, 111), Err(Stale));
446 assert!(c.is_loading(2));
448 assert_eq!(c.get(2), None);
449
450 c.finish_load(live, 222).unwrap();
451 assert_eq!(c.get(2), Some(&222));
452 assert!(!c.contains_ready(1));
453 }
454
455 #[test]
456 fn finishing_a_reservation_whose_slot_was_emptied_is_refused() {
457 let mut c = Cache::new();
458 let r = c.begin_load(3).unwrap();
459 c.evict(3);
460 assert_eq!(c.finish_load(r, 30), Err(Stale));
461 assert!(c.is_empty());
462 }
463
464 #[allow(deprecated)]
467 #[test]
468 fn the_deprecated_pair_still_stores_under_the_wrong_key() {
469 let mut c = SlotCache::<u32, 1, 16>::new();
470 let slot = c.reserve(1).unwrap();
471 c.evict(1);
472 assert_eq!(c.reserve(2), Some(slot));
473 c.mark_ready(slot, 111);
474 assert_eq!(c.get(2), Some(&111));
475 }
476
477 #[test]
478 fn get_or_insert_builds_on_miss_returns_on_hit() {
479 let mut c = Cache::new();
480 let mut builds = 0;
481 let v = *c
482 .get_or_insert_with(7, || {
483 builds += 1;
484 Some(70)
485 })
486 .unwrap();
487 assert_eq!(v, 70);
488 let v2 = *c
490 .get_or_insert_with(7, || {
491 builds += 1;
492 Some(999)
493 })
494 .unwrap();
495 assert_eq!(v2, 70);
496 assert_eq!(builds, 1, "build runs only on a miss");
497 }
498
499 #[test]
500 fn get_or_insert_build_failure_leaves_slot_empty() {
501 let mut c = Cache::new();
502 assert_eq!(c.get_or_insert_with(3, || None), None);
503 assert!(!c.contains_ready(3));
504 assert!(c.is_empty());
505 }
506
507 #[test]
508 fn lru_evicts_least_recently_used() {
509 let mut c = Cache::new();
510 put(&mut c, 1, 10);
511 c.bump_epoch();
512 put(&mut c, 2, 20);
513 c.bump_epoch();
514 put(&mut c, 3, 30);
515 c.bump_epoch();
517 let _ = c.get(1);
518 let _ = c.get(3);
519 c.bump_epoch();
521 put(&mut c, 4, 40);
522 assert!(!c.contains_ready(2), "LRU victim should be evicted");
523 assert!(c.contains_ready(1) && c.contains_ready(3) && c.contains_ready(4));
524 assert_eq!(c.slot_of(2), None);
525 }
526
527 #[test]
528 fn pin_protects_from_eviction() {
529 let mut c = Cache::new();
530 put(&mut c, 1, 10);
531 put(&mut c, 2, 20);
532 put(&mut c, 3, 30);
533 c.bump_epoch();
535 let _ = c.get(2);
536 let _ = c.get(3);
537 c.pin(1); c.bump_epoch();
539 put(&mut c, 4, 40);
540 assert!(c.contains_ready(1), "pinned key must survive");
541 assert!(
542 !c.contains_ready(2),
543 "an unpinned key is the victim instead"
544 );
545 }
546
547 #[test]
548 fn reserve_fails_when_all_slots_pinned() {
549 let mut c = Cache::new();
550 put(&mut c, 1, 10);
551 put(&mut c, 2, 20);
552 put(&mut c, 3, 30);
553 c.set_pinned(&[1, 2, 3]);
554 assert_eq!(c.begin_load(4), None, "no evictable slot");
555 assert!(!c.contains_ready(4));
556 }
557
558 #[test]
559 fn evict_ready_outside_keeps_listed_and_pinned() {
560 let mut c = Cache::new();
561 put(&mut c, 1, 10);
562 put(&mut c, 2, 20);
563 put(&mut c, 3, 30);
564 c.pin(3);
565 let evicted = c.evict_ready_outside(&[1]);
566 assert_eq!(evicted, 1); assert!(c.contains_ready(1)); assert!(!c.contains_ready(2)); assert!(c.contains_ready(3)); }
571
572 #[test]
573 fn reverse_map_stays_consistent_across_reuse() {
574 let mut c = Cache::new();
575 put(&mut c, 1, 10);
576 put(&mut c, 2, 20);
577 put(&mut c, 3, 30);
578 c.bump_epoch();
579 put(&mut c, 4, 40); assert_eq!(c.slot_of(1), None, "evicted key has no slot");
581 assert!(c.contains_ready(4));
582 let s4 = c.slot_of(4).unwrap();
584 assert_ne!(c.slot_of(2), Some(s4));
585 }
586
587 #[test]
588 fn out_of_range_key_is_rejected() {
589 let mut c = Cache::new();
590 assert_eq!(c.begin_load(16), None); assert_eq!(c.begin_load(NONE), None);
592 assert!(!c.contains_ready(16));
593 }
594}