#[cfg(feature = "metrics")]
use crate::metrics::metrics_impl::CoreOnlyMetrics;
#[cfg(feature = "metrics")]
use crate::metrics::snapshot::CoreOnlyMetricsSnapshot;
#[cfg(feature = "metrics")]
use crate::metrics::traits::{CoreMetricsRecorder, MetricsSnapshotProvider};
use crate::prelude::ReadOnlyCache;
use crate::traits::CoreCache;
use rustc_hash::FxHashMap;
use std::hash::Hash;
#[must_use]
pub struct LifoCore<K, V> {
map: FxHashMap<K, V>,
stack: Vec<K>,
capacity: usize,
#[cfg(feature = "metrics")]
metrics: CoreOnlyMetrics,
}
impl<K, V> LifoCore<K, V>
where
K: Clone + Eq + Hash,
{
#[inline]
pub fn new(capacity: usize) -> Self {
Self {
map: FxHashMap::with_capacity_and_hasher(capacity, Default::default()),
stack: Vec::with_capacity(capacity),
capacity,
#[cfg(feature = "metrics")]
metrics: CoreOnlyMetrics::default(),
}
}
#[inline]
pub fn get(&mut self, key: &K) -> Option<&V> {
#[cfg(feature = "metrics")]
if self.map.contains_key(key) {
self.metrics.record_get_hit();
} else {
self.metrics.record_get_miss();
}
self.map.get(key)
}
#[inline]
#[must_use]
pub fn peek(&self, key: &K) -> Option<&V> {
self.map.get(key)
}
#[inline]
pub fn insert(&mut self, key: K, value: V) -> Option<V> {
#[cfg(feature = "metrics")]
self.metrics.record_insert_call();
if self.capacity == 0 {
return None;
}
if let Some(v) = self.map.get_mut(&key) {
#[cfg(feature = "metrics")]
self.metrics.record_insert_update();
return Some(std::mem::replace(v, value));
}
#[cfg(feature = "metrics")]
self.metrics.record_insert_new();
self.evict_if_needed();
self.stack.push(key.clone());
self.map.insert(key, value);
None
}
#[inline]
fn evict_if_needed(&mut self) {
#[cfg(feature = "metrics")]
if self.len() >= self.capacity && !self.stack.is_empty() {
self.metrics.record_evict_call();
}
while self.len() >= self.capacity && !self.stack.is_empty() {
if let Some(key) = self.stack.pop() {
self.map.remove(&key);
#[cfg(feature = "metrics")]
self.metrics.record_evicted_entry();
} else {
break;
}
}
#[cfg(debug_assertions)]
self.validate_invariants();
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.map.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
#[inline]
#[must_use]
pub fn capacity(&self) -> usize {
self.capacity
}
#[inline]
#[must_use]
pub fn contains(&self, key: &K) -> bool {
self.map.contains_key(key)
}
pub fn clear(&mut self) {
#[cfg(feature = "metrics")]
self.metrics.record_clear();
self.map.clear();
self.stack.clear();
#[cfg(debug_assertions)]
self.validate_invariants();
}
#[must_use]
pub fn pop_newest(&mut self) -> Option<(K, V)> {
let key = self.stack.pop()?;
let value = self.map.remove(&key)?;
Some((key, value))
}
#[must_use]
pub fn peek_newest(&self) -> Option<(&K, &V)> {
let key = self.stack.last()?;
let value = self.map.get(key)?;
Some((key, value))
}
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
self.map.iter()
}
pub fn keys(&self) -> impl Iterator<Item = &K> {
self.map.keys()
}
pub fn values(&self) -> impl Iterator<Item = &V> {
self.map.values()
}
#[cfg(debug_assertions)]
fn validate_invariants(&self) {
debug_assert_eq!(
self.map.len(),
self.stack.len(),
"Map and stack have different sizes"
);
for key in self.map.keys() {
debug_assert!(self.stack.contains(key), "Key in map not found in stack");
}
for key in &self.stack {
debug_assert!(self.map.contains_key(key), "Key in stack not found in map");
}
let unique_count = self
.stack
.iter()
.collect::<std::collections::HashSet<_>>()
.len();
debug_assert_eq!(unique_count, self.stack.len(), "Duplicate keys in stack");
}
}
impl<K, V> std::fmt::Debug for LifoCore<K, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LifoCore")
.field("capacity", &self.capacity)
.field("len", &self.map.len())
.field("stack_len", &self.stack.len())
.finish_non_exhaustive()
}
}
impl<K, V> ReadOnlyCache<K, V> for LifoCore<K, V>
where
K: Clone + Eq + Hash,
{
#[inline]
fn contains(&self, key: &K) -> bool {
self.map.contains_key(key)
}
#[inline]
fn len(&self) -> usize {
self.map.len()
}
#[inline]
fn capacity(&self) -> usize {
self.capacity
}
}
impl<K, V> CoreCache<K, V> for LifoCore<K, V>
where
K: Clone + Eq + Hash,
{
#[inline]
fn insert(&mut self, key: K, value: V) -> Option<V> {
LifoCore::insert(self, key, value)
}
#[inline]
fn get(&mut self, key: &K) -> Option<&V> {
LifoCore::get(self, key)
}
fn clear(&mut self) {
LifoCore::clear(self);
}
}
#[cfg(feature = "metrics")]
impl<K, V> LifoCore<K, V>
where
K: Clone + Eq + Hash,
{
pub fn metrics_snapshot(&self) -> CoreOnlyMetricsSnapshot {
CoreOnlyMetricsSnapshot {
get_calls: self.metrics.get_calls,
get_hits: self.metrics.get_hits,
get_misses: self.metrics.get_misses,
insert_calls: self.metrics.insert_calls,
insert_updates: self.metrics.insert_updates,
insert_new: self.metrics.insert_new,
evict_calls: self.metrics.evict_calls,
evicted_entries: self.metrics.evicted_entries,
cache_len: self.len(),
capacity: self.capacity,
}
}
}
#[cfg(feature = "metrics")]
impl<K, V> MetricsSnapshotProvider<CoreOnlyMetricsSnapshot> for LifoCore<K, V>
where
K: Clone + Eq + Hash,
{
fn snapshot(&self) -> CoreOnlyMetricsSnapshot {
self.metrics_snapshot()
}
}
impl<K, V> Default for LifoCore<K, V>
where
K: Clone + Eq + Hash,
{
fn default() -> Self {
Self::new(0)
}
}
impl<K, V> Clone for LifoCore<K, V>
where
K: Clone + Eq + Hash,
V: Clone,
{
fn clone(&self) -> Self {
Self {
map: self.map.clone(),
stack: self.stack.clone(),
capacity: self.capacity,
#[cfg(feature = "metrics")]
metrics: CoreOnlyMetrics::default(),
}
}
}
impl<K, V> Extend<(K, V)> for LifoCore<K, V>
where
K: Clone + Eq + Hash,
{
fn extend<T: IntoIterator<Item = (K, V)>>(&mut self, iter: T) {
for (k, v) in iter {
self.insert(k, v);
}
}
}
impl<K, V> FromIterator<(K, V)> for LifoCore<K, V>
where
K: Clone + Eq + Hash,
{
fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
let iter = iter.into_iter();
let (lower, _) = iter.size_hint();
let mut cache = Self::new(lower.max(16));
for (k, v) in iter {
cache.insert(k, v);
}
cache
}
}
impl<K, V> IntoIterator for LifoCore<K, V>
where
K: Clone + Eq + Hash,
{
type Item = (K, V);
type IntoIter = std::collections::hash_map::IntoIter<K, V>;
fn into_iter(self) -> Self::IntoIter {
self.map.into_iter()
}
}
impl<'a, K, V> IntoIterator for &'a LifoCore<K, V>
where
K: Clone + Eq + Hash,
{
type Item = (&'a K, &'a V);
type IntoIter = std::collections::hash_map::Iter<'a, K, V>;
fn into_iter(self) -> Self::IntoIter {
self.map.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
mod basic_operations {
use super::*;
#[test]
fn new_cache_is_empty() {
let cache: LifoCore<&str, i32> = LifoCore::new(100);
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert_eq!(cache.capacity(), 100);
}
#[test]
fn insert_and_get() {
let mut cache = LifoCore::new(100);
cache.insert("key1", "value1");
assert_eq!(cache.len(), 1);
assert_eq!(cache.get(&"key1"), Some(&"value1"));
}
#[test]
fn insert_multiple_items() {
let mut cache = LifoCore::new(100);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
assert_eq!(cache.len(), 3);
assert_eq!(cache.get(&"a"), Some(&1));
assert_eq!(cache.get(&"b"), Some(&2));
assert_eq!(cache.get(&"c"), Some(&3));
}
#[test]
fn get_missing_key_returns_none() {
let mut cache: LifoCore<&str, i32> = LifoCore::new(100);
assert_eq!(cache.get(&"missing"), None);
}
#[test]
fn update_existing_key() {
let mut cache = LifoCore::new(100);
cache.insert("key", "initial");
cache.insert("key", "updated");
assert_eq!(cache.len(), 1);
assert_eq!(cache.get(&"key"), Some(&"updated"));
}
#[test]
fn contains_returns_correct_result() {
let mut cache = LifoCore::new(100);
cache.insert("exists", 1);
assert!(cache.contains(&"exists"));
assert!(!cache.contains(&"missing"));
}
#[test]
fn clear_removes_all_entries() {
let mut cache = LifoCore::new(100);
cache.insert("a", 1);
cache.insert("b", 2);
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert!(!cache.contains(&"a"));
assert!(!cache.contains(&"b"));
}
#[test]
fn capacity_returns_correct_value() {
let cache: LifoCore<i32, i32> = LifoCore::new(500);
assert_eq!(cache.capacity(), 500);
}
}
mod lifo_behavior {
use super::*;
#[test]
fn evicts_most_recently_inserted() {
let mut cache = LifoCore::new(3);
cache.insert("first", 1);
cache.insert("second", 2);
cache.insert("third", 3);
assert_eq!(cache.len(), 3);
cache.insert("fourth", 4);
assert_eq!(cache.len(), 3);
assert!(cache.contains(&"first"));
assert!(cache.contains(&"second"));
assert!(
!cache.contains(&"third"),
"Most recent 'third' should be evicted"
);
assert!(cache.contains(&"fourth"));
}
#[test]
fn stack_order_maintained() {
let mut cache = LifoCore::new(3);
cache.insert(1, 10);
cache.insert(2, 20);
cache.insert(3, 30);
cache.insert(4, 40);
assert!(!cache.contains(&3));
assert!(cache.contains(&1));
assert!(cache.contains(&2));
assert!(cache.contains(&4));
cache.insert(5, 50);
assert!(!cache.contains(&4));
assert!(cache.contains(&1));
assert!(cache.contains(&2));
assert!(cache.contains(&5));
}
#[test]
fn opposite_of_fifo_behavior() {
let mut cache = LifoCore::new(3);
cache.insert("oldest", 1);
cache.insert("middle", 2);
cache.insert("newest", 3);
cache.insert("new", 4);
assert!(cache.contains(&"oldest"), "Oldest should stay in LIFO");
assert!(cache.contains(&"middle"));
assert!(
!cache.contains(&"newest"),
"Newest should be evicted in LIFO"
);
assert!(cache.contains(&"new"));
}
}
mod eviction_behavior {
use super::*;
#[test]
fn eviction_occurs_when_over_capacity() {
let mut cache = LifoCore::new(5);
for i in 0..10 {
cache.insert(i, i * 10);
}
assert_eq!(cache.len(), 5);
}
#[test]
fn eviction_removes_from_map() {
let mut cache = LifoCore::new(3);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
assert!(cache.contains(&"c"));
cache.insert("d", 4);
assert!(!cache.contains(&"c"));
assert_eq!(cache.get(&"c"), None);
}
#[test]
fn continuous_insertions_evict_correctly() {
let mut cache = LifoCore::new(3);
cache.insert(1, 10);
cache.insert(2, 20);
cache.insert(3, 30);
assert_eq!(cache.len(), 3);
cache.insert(4, 40);
assert_eq!(cache.len(), 3);
assert!(!cache.contains(&3));
cache.insert(5, 50);
assert_eq!(cache.len(), 3);
assert!(!cache.contains(&4)); }
#[test]
fn oldest_items_survive() {
let mut cache = LifoCore::new(3);
cache.insert(1, 10);
cache.insert(2, 20);
cache.insert(3, 30);
for i in 4..=10 {
cache.insert(i, i * 10);
}
assert!(cache.contains(&1), "Oldest item should survive in LIFO");
assert_eq!(cache.len(), 3);
}
}
mod get_behavior {
use super::*;
#[test]
fn get_does_not_change_eviction_order() {
let mut cache = LifoCore::new(3);
cache.insert(1, 10);
cache.insert(2, 20);
cache.insert(3, 30);
for _ in 0..100 {
cache.get(&1);
}
cache.insert(4, 40);
assert!(cache.contains(&1));
assert!(cache.contains(&2));
assert!(
!cache.contains(&3),
"Most recent insert evicted despite 1 being accessed"
);
assert!(cache.contains(&4));
}
}
mod edge_cases {
use super::*;
#[test]
fn single_capacity_cache() {
let mut cache = LifoCore::new(1);
cache.insert("a", 1);
assert_eq!(cache.get(&"a"), Some(&1));
cache.insert("b", 2);
assert!(!cache.contains(&"a"));
assert_eq!(cache.get(&"b"), Some(&2));
}
#[test]
fn zero_capacity_cache() {
let mut cache = LifoCore::new(0);
cache.insert("a", 1);
assert_eq!(cache.len(), 0);
assert!(!cache.contains(&"a"));
}
#[test]
fn get_after_update() {
let mut cache = LifoCore::new(100);
cache.insert("key", "v1");
assert_eq!(cache.get(&"key"), Some(&"v1"));
cache.insert("key", "v2");
assert_eq!(cache.get(&"key"), Some(&"v2"));
cache.insert("key", "v3");
cache.insert("key", "v4");
assert_eq!(cache.get(&"key"), Some(&"v4"));
}
#[test]
fn large_capacity() {
let mut cache = LifoCore::new(10000);
for i in 0..10000 {
cache.insert(i, i * 2);
}
assert_eq!(cache.len(), 10000);
assert_eq!(cache.get(&5000), Some(&10000));
assert_eq!(cache.get(&9999), Some(&19998));
}
#[test]
fn empty_cache_operations() {
let mut cache: LifoCore<i32, i32> = LifoCore::new(100);
assert!(cache.is_empty());
assert_eq!(cache.get(&1), None);
assert!(!cache.contains(&1));
}
#[test]
fn string_keys_and_values() {
let mut cache = LifoCore::new(100);
cache.insert(String::from("hello"), String::from("world"));
cache.insert(String::from("foo"), String::from("bar"));
assert_eq!(
cache.get(&String::from("hello")),
Some(&String::from("world"))
);
assert_eq!(cache.get(&String::from("foo")), Some(&String::from("bar")));
}
#[test]
fn update_preserves_stack_position() {
let mut cache = LifoCore::new(3);
cache.insert(1, 10);
cache.insert(2, 20);
cache.insert(3, 30);
cache.insert(1, 100);
cache.insert(4, 40);
assert!(cache.contains(&1), "Updated item should preserve position");
assert!(cache.contains(&2));
assert!(!cache.contains(&3), "Most recent insert still evicted");
assert!(cache.contains(&4));
}
}
mod lifo_api {
use super::*;
#[test]
fn peek_returns_value_immutably() {
let mut cache = LifoCore::new(100);
cache.insert("key", 42);
assert_eq!(cache.peek(&"key"), Some(&42));
assert_eq!(cache.peek(&"missing"), None);
}
#[test]
fn peek_allows_multiple_borrows() {
let mut cache = LifoCore::new(100);
cache.insert("a", 1);
cache.insert("b", 2);
let a = cache.peek(&"a");
let b = cache.peek(&"b");
assert_eq!(a, Some(&1));
assert_eq!(b, Some(&2));
}
#[test]
fn pop_newest_returns_most_recent() {
let mut cache = LifoCore::new(10);
cache.insert(1, "first");
cache.insert(2, "second");
cache.insert(3, "third");
assert_eq!(cache.pop_newest(), Some((3, "third")));
assert_eq!(cache.pop_newest(), Some((2, "second")));
assert_eq!(cache.pop_newest(), Some((1, "first")));
assert_eq!(cache.pop_newest(), None);
}
#[test]
fn peek_newest_shows_top_of_stack() {
let mut cache = LifoCore::new(10);
assert_eq!(cache.peek_newest(), None);
cache.insert(1, "first");
assert_eq!(cache.peek_newest(), Some((&1, &"first")));
cache.insert(2, "second");
assert_eq!(cache.peek_newest(), Some((&2, &"second")));
assert_eq!(cache.len(), 2);
}
#[test]
fn insert_returns_previous_value() {
let mut cache = LifoCore::new(100);
assert_eq!(cache.insert("key", "v1"), None);
assert_eq!(cache.insert("key", "v2"), Some("v1"));
assert_eq!(cache.insert("key", "v3"), Some("v2"));
assert_eq!(cache.insert("new", "val"), None);
}
}
mod collection_traits {
use super::*;
#[test]
fn iter_yields_all_entries() {
let mut cache = LifoCore::new(10);
cache.insert("a", 1);
cache.insert("b", 2);
cache.insert("c", 3);
let mut pairs: Vec<_> = cache.iter().collect();
pairs.sort_by_key(|(k, _)| *k);
assert_eq!(pairs, vec![(&"a", &1), (&"b", &2), (&"c", &3)]);
}
#[test]
fn keys_and_values() {
let mut cache = LifoCore::new(10);
cache.insert(1, "a");
cache.insert(2, "b");
let mut keys: Vec<_> = cache.keys().collect();
keys.sort();
assert_eq!(keys, vec![&1, &2]);
let mut values: Vec<_> = cache.values().collect();
values.sort();
assert_eq!(values, vec![&"a", &"b"]);
}
#[test]
fn extend_inserts_all() {
let mut cache = LifoCore::new(10);
cache.insert(1, "one");
cache.extend(vec![(2, "two"), (3, "three")]);
assert_eq!(cache.len(), 3);
assert_eq!(cache.peek(&2), Some(&"two"));
assert_eq!(cache.peek(&3), Some(&"three"));
}
#[test]
fn from_iterator() {
let cache: LifoCore<i32, &str> = vec![(1, "one"), (2, "two"), (3, "three")]
.into_iter()
.collect();
assert_eq!(cache.len(), 3);
assert!(cache.capacity() >= 3);
assert!(cache.contains(&1));
assert!(cache.contains(&2));
assert!(cache.contains(&3));
}
#[test]
fn into_iterator_owned() {
let mut cache = LifoCore::new(10);
cache.insert(1, "a");
cache.insert(2, "b");
let mut pairs: Vec<_> = cache.into_iter().collect();
pairs.sort_by_key(|(k, _)| *k);
assert_eq!(pairs, vec![(1, "a"), (2, "b")]);
}
#[test]
fn into_iterator_ref() {
let mut cache = LifoCore::new(10);
cache.insert(1, "a");
cache.insert(2, "b");
let mut pairs: Vec<_> = (&cache).into_iter().collect();
pairs.sort_by_key(|(k, _)| *k);
assert_eq!(pairs, vec![(&1, &"a"), (&2, &"b")]);
}
}
mod default_and_clone {
use super::*;
#[test]
fn default_creates_zero_capacity() {
let cache: LifoCore<String, i32> = LifoCore::default();
assert_eq!(cache.capacity(), 0);
assert!(cache.is_empty());
}
#[test]
fn clone_preserves_state() {
let mut original = LifoCore::new(10);
original.insert(1, "a");
original.insert(2, "b");
let cloned = original.clone();
assert_eq!(cloned.len(), 2);
assert_eq!(cloned.capacity(), 10);
assert_eq!(cloned.peek(&1), Some(&"a"));
assert_eq!(cloned.peek(&2), Some(&"b"));
}
#[test]
fn clone_is_independent() {
let mut original = LifoCore::new(10);
original.insert(1, "a");
let mut cloned = original.clone();
cloned.insert(2, "b");
assert_eq!(original.len(), 1);
assert_eq!(cloned.len(), 2);
}
}
#[test]
#[cfg(debug_assertions)]
fn validate_invariants_after_operations() {
let mut cache = LifoCore::new(10);
for i in 1..=10 {
cache.insert(i, i * 100);
}
cache.validate_invariants();
for _ in 0..5 {
cache.get(&5);
}
cache.validate_invariants();
cache.insert(11, 1100);
cache.validate_invariants();
cache.insert(12, 1200);
cache.validate_invariants();
cache.clear();
cache.validate_invariants();
assert_eq!(cache.len(), 0);
assert_eq!(cache.stack.len(), 0);
}
#[test]
#[cfg(debug_assertions)]
fn validate_invariants_with_stack_consistency() {
let mut cache = LifoCore::new(5);
cache.insert(1, 100);
cache.insert(2, 200);
cache.insert(3, 300);
cache.validate_invariants();
for i in 4..=10 {
cache.insert(i, i * 100);
cache.validate_invariants();
}
assert_eq!(cache.len(), 5);
assert_eq!(cache.stack.len(), 5);
for key in &cache.stack {
assert!(cache.map.contains_key(key));
}
}
}