use rustc_hash::FxBuildHasher;
use std::collections::HashMap;
use std::hash::{BuildHasher, Hash};
use crate::ds::intrusive_list::IntrusiveList;
use crate::ds::slot_arena::SlotId;
pub struct GhostList<K, S = FxBuildHasher> {
list: IntrusiveList<K>,
index: HashMap<K, SlotId, S>,
capacity: usize,
}
impl<K, S> std::fmt::Debug for GhostList<K, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GhostList")
.field("capacity", &self.capacity)
.field("len", &self.list.len())
.finish_non_exhaustive()
}
}
impl<K, S> Clone for GhostList<K, S>
where
K: Eq + Hash + Clone,
S: BuildHasher + Clone,
{
fn clone(&self) -> Self {
let alloc = self.list.len().min(self.capacity);
let mut new_list = IntrusiveList::with_capacity(alloc);
let mut new_index = HashMap::with_capacity_and_hasher(alloc, self.index.hasher().clone());
for (_, key) in self.list.iter_entries() {
let id = new_list.push_back(key.clone());
new_index.insert(key.clone(), id);
}
Self {
list: new_list,
index: new_index,
capacity: self.capacity,
}
}
}
pub struct Iter<'a, K> {
inner: MapToKey<'a, K>,
}
type MapToKey<'a, K> = std::iter::Map<
crate::ds::intrusive_list::IntrusiveListEntryIter<'a, K>,
fn((SlotId, &'a K)) -> &'a K,
>;
impl<'a, K: std::fmt::Debug> std::fmt::Debug for Iter<'a, K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Iter").finish_non_exhaustive()
}
}
impl<'a, K> Iterator for Iter<'a, K> {
type Item = &'a K;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<'a, K> ExactSizeIterator for Iter<'a, K> {}
impl<'a, K> std::iter::FusedIterator for Iter<'a, K> {}
#[derive(Debug)]
pub struct IntoIter<K> {
inner: std::vec::IntoIter<K>,
}
impl<K> Iterator for IntoIter<K> {
type Item = K;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<K> ExactSizeIterator for IntoIter<K> {}
impl<K> std::iter::FusedIterator for IntoIter<K> {}
impl<K, S> IntoIterator for GhostList<K, S>
where
K: Eq + Hash + Clone,
S: BuildHasher,
{
type Item = K;
type IntoIter = IntoIter<K>;
fn into_iter(self) -> Self::IntoIter {
let keys: Vec<K> = self.list.iter_entries().map(|(_, k)| k.clone()).collect();
IntoIter {
inner: keys.into_iter(),
}
}
}
impl<'a, K, S> IntoIterator for &'a GhostList<K, S>
where
K: Eq + Hash + Clone,
S: BuildHasher,
{
type Item = &'a K;
type IntoIter = Iter<'a, K>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<K, S> PartialEq for GhostList<K, S>
where
K: Eq + Hash + Clone,
S: BuildHasher,
{
fn eq(&self, other: &Self) -> bool {
self.capacity == other.capacity
&& self.len() == other.len()
&& self.iter().zip(other.iter()).all(|(a, b)| a == b)
}
}
impl<K, S> Eq for GhostList<K, S>
where
K: Eq + Hash + Clone,
S: BuildHasher,
{
}
impl<K, S> Extend<K> for GhostList<K, S>
where
K: Eq + Hash + Clone,
S: BuildHasher,
{
fn extend<I: IntoIterator<Item = K>>(&mut self, iter: I) {
for key in iter {
self.record(key);
}
}
}
impl<K, S> FromIterator<K> for GhostList<K, S>
where
K: Eq + Hash + Clone,
S: BuildHasher + Default,
{
fn from_iter<I: IntoIterator<Item = K>>(iter: I) -> Self {
let iter = iter.into_iter();
let (lower, _) = iter.size_hint();
let initial = lower.clamp(16, Self::MAX_CAPACITY);
let mut ghost = Self::with_capacity_and_hasher(initial, S::default());
for key in iter {
ghost.record(key);
}
ghost.capacity = ghost.len();
ghost
}
}
impl<K> GhostList<K, FxBuildHasher>
where
K: Eq + Hash + Clone,
{
pub fn new(capacity: usize) -> Self {
Self::with_capacity_and_hasher(capacity, FxBuildHasher)
}
pub fn try_new(capacity: usize) -> Result<Self, CapacityOverflowError> {
Self::try_with_capacity_and_hasher(capacity, FxBuildHasher)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CapacityOverflowError {
pub requested: usize,
pub max: usize,
}
impl std::fmt::Display for CapacityOverflowError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"GhostList capacity {} exceeds maximum of {}",
self.requested, self.max
)
}
}
impl std::error::Error for CapacityOverflowError {}
impl<K, S> Default for GhostList<K, S>
where
K: Eq + Hash + Clone,
S: BuildHasher + Default,
{
fn default() -> Self {
Self::with_capacity_and_hasher(0, S::default())
}
}
impl<K, S> GhostList<K, S>
where
K: Eq + Hash + Clone,
S: BuildHasher,
{
pub const MAX_CAPACITY: usize = {
const HASHMAP_LOAD_FACTOR_MULTIPLIER: usize = 3;
let list_per_entry = IntrusiveList::<K>::BYTES_PER_ENTRY;
let bucket_bytes = std::mem::size_of::<(K, SlotId)>().saturating_add(1);
let index_per_entry = bucket_bytes.saturating_mul(HASHMAP_LOAD_FACTOR_MULTIPLIER);
let per_entry = list_per_entry.saturating_add(index_per_entry);
let sixteen_gib_u64: u64 = 16 * 1024 * 1024 * 1024;
let sixteen_gib: usize = if sixteen_gib_u64 <= usize::MAX as u64 {
sixteen_gib_u64 as usize
} else {
usize::MAX
};
let isize_quarter = (isize::MAX as usize) / 4;
let byte_budget = if sixteen_gib < isize_quarter {
sixteen_gib
} else {
isize_quarter
};
byte_budget / if per_entry == 0 { 1 } else { per_entry }
};
pub fn with_capacity_and_hasher(capacity: usize, hasher: S) -> Self {
let capacity = capacity.min(Self::MAX_CAPACITY);
Self {
list: IntrusiveList::with_capacity(capacity),
index: HashMap::with_capacity_and_hasher(capacity, hasher),
capacity,
}
}
pub fn try_with_capacity_and_hasher(
capacity: usize,
hasher: S,
) -> Result<Self, CapacityOverflowError> {
if capacity > Self::MAX_CAPACITY {
return Err(CapacityOverflowError {
requested: capacity,
max: Self::MAX_CAPACITY,
});
}
Ok(Self::with_capacity_and_hasher(capacity, hasher))
}
pub fn with_hasher(hasher: S) -> Self {
Self::with_capacity_and_hasher(0, hasher)
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn len(&self) -> usize {
self.list.len()
}
pub fn is_empty(&self) -> bool {
self.list.is_empty()
}
pub fn contains(&self, key: &K) -> bool {
self.index.contains_key(key)
}
pub fn record(&mut self, key: K) -> Option<K> {
if self.capacity == 0 {
return None;
}
if let Some(&id) = self.index.get(&key) {
self.list.move_to_front(id);
return None;
}
self.index.reserve(1);
let evicted = if self.list.len() >= self.capacity {
let old_key = self.list.pop_back()?;
self.index.remove(&old_key);
Some(old_key)
} else {
None
};
let id = self.list.push_front(key.clone());
self.index.insert(key, id);
evicted
}
pub fn record_batch<'a, I>(&mut self, keys: I) -> usize
where
I: IntoIterator<Item = &'a K>,
K: 'a,
{
let mut count = 0;
for key in keys {
self.record(key.clone());
count += 1;
}
count
}
pub fn remove(&mut self, key: &K) -> bool {
let id = match self.index.remove(key) {
Some(id) => id,
None => return false,
};
self.list.remove(id);
true
}
pub fn evict_lru(&mut self) -> Option<K> {
let key = self.list.pop_back()?;
self.index.remove(&key);
Some(key)
}
pub fn remove_batch<'a, I>(&mut self, keys: I) -> usize
where
I: IntoIterator<Item = &'a K>,
K: 'a,
{
let mut removed = 0;
for key in keys {
if self.remove(key) {
removed += 1;
}
}
removed
}
pub fn clear(&mut self) {
self.list.clear();
self.index.clear();
}
pub fn clear_shrink(&mut self) {
self.clear();
self.list.clear_shrink();
self.index.shrink_to_fit();
}
pub fn approx_bytes(&self) -> usize {
let entry_bytes = self
.index
.capacity()
.saturating_mul(std::mem::size_of::<(K, SlotId)>());
std::mem::size_of::<Self>()
.saturating_add(self.list.approx_bytes())
.saturating_add(entry_bytes)
}
pub fn iter(&self) -> Iter<'_, K> {
fn extract_key<K>((_, key): (SlotId, &K)) -> &K {
key
}
Iter {
inner: self.list.iter_entries().map(extract_key),
}
}
pub fn keys(&self) -> Iter<'_, K> {
self.iter()
}
#[cfg(any(test, debug_assertions))]
pub fn debug_snapshot_keys(&self) -> Vec<K>
where
K: Clone,
{
self.list
.iter_entries()
.map(|(_, key)| key.clone())
.collect()
}
#[cfg(any(test, debug_assertions))]
pub fn debug_peek_lru(&self) -> Option<&K> {
self.list.iter_entries().last().map(|(_, k)| k)
}
#[cfg(any(test, debug_assertions))]
pub fn debug_peek_mru(&self) -> Option<&K> {
self.list.iter_entries().next().map(|(_, k)| k)
}
#[cfg(any(test, debug_assertions))]
pub fn debug_snapshot_ids(&self) -> Vec<SlotId> {
self.list.iter_ids().collect()
}
#[cfg(any(test, debug_assertions))]
pub fn debug_snapshot_ids_sorted(&self) -> Vec<SlotId> {
let mut ids: Vec<_> = self.list.iter_ids().collect();
ids.sort_by_key(|id| id.index());
ids
}
#[cfg(any(test, debug_assertions))]
pub fn debug_snapshot_keys_sorted(&self) -> Vec<K>
where
K: Clone,
{
let mut ids: Vec<_> = self.list.iter_ids().collect();
ids.sort_by_key(|id| id.index());
ids.into_iter()
.filter_map(|id| self.list.get(id).cloned())
.collect()
}
#[cfg(any(test, debug_assertions))]
pub fn debug_validate_invariants(&self) {
assert_eq!(self.list.len(), self.index.len());
assert!(self.list.len() <= self.capacity);
if self.capacity == 0 {
assert!(self.list.is_empty());
}
for &id in self.index.values() {
assert!(self.list.contains(id));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ghost_list_records_and_evictions() {
let mut ghost = GhostList::new(2);
ghost.record("a");
ghost.record("b");
assert!(ghost.contains(&"a"));
assert!(ghost.contains(&"b"));
ghost.record("a");
ghost.record("c");
assert!(ghost.contains(&"a"));
assert!(ghost.contains(&"c"));
assert!(!ghost.contains(&"b"));
}
#[test]
fn ghost_list_zero_capacity_is_noop() {
let mut ghost = GhostList::new(0);
ghost.record("a");
ghost.record("b");
assert!(ghost.is_empty());
assert_eq!(ghost.len(), 0);
assert!(!ghost.contains(&"a"));
assert!(!ghost.contains(&"b"));
}
#[test]
fn ghost_list_record_existing_moves_to_front() {
let mut ghost = GhostList::new(3);
ghost.record("a");
ghost.record("b");
ghost.record("c");
assert!(ghost.contains(&"a"));
assert!(ghost.contains(&"b"));
assert!(ghost.contains(&"c"));
ghost.record("a");
ghost.record("d");
assert!(ghost.contains(&"a"));
assert!(!ghost.contains(&"b"));
assert!(ghost.contains(&"c"));
assert!(ghost.contains(&"d"));
}
#[test]
fn ghost_list_remove_existing_and_missing() {
let mut ghost = GhostList::new(2);
ghost.record("a");
ghost.record("b");
assert!(ghost.remove(&"a"));
assert!(!ghost.contains(&"a"));
assert_eq!(ghost.len(), 1);
assert!(!ghost.remove(&"missing"));
assert_eq!(ghost.len(), 1);
}
#[test]
fn ghost_list_clear_resets_state() {
let mut ghost = GhostList::new(2);
ghost.record("a");
ghost.record("b");
ghost.clear();
assert!(ghost.is_empty());
assert_eq!(ghost.len(), 0);
assert!(!ghost.contains(&"a"));
assert!(!ghost.contains(&"b"));
}
#[test]
fn ghost_list_debug_invariants_hold() {
let mut ghost = GhostList::new(2);
ghost.record("a");
ghost.record("b");
ghost.record("a");
ghost.debug_validate_invariants();
}
#[test]
fn ghost_list_debug_snapshots() {
let mut ghost = GhostList::new(2);
ghost.record("a");
ghost.record("b");
let keys = ghost.debug_snapshot_keys();
let ids = ghost.debug_snapshot_ids();
assert_eq!(keys.len(), 2);
assert_eq!(ids.len(), 2);
assert_eq!(ghost.debug_snapshot_ids_sorted().len(), 2);
assert_eq!(ghost.debug_snapshot_keys_sorted().len(), 2);
}
#[test]
fn ghost_list_batch_ops() {
let mut ghost = GhostList::new(3);
assert_eq!(ghost.record_batch(&["a", "b", "c"]), 3);
assert_eq!(ghost.remove_batch(&["b", "d"]), 1);
assert!(ghost.contains(&"a"));
assert!(ghost.contains(&"c"));
assert!(!ghost.contains(&"b"));
}
#[test]
fn ghost_list_iter() {
let mut ghost = GhostList::new(5);
ghost.record("a");
ghost.record("b");
ghost.record("c");
let keys: Vec<_> = ghost.iter().cloned().collect();
assert_eq!(keys, vec!["c", "b", "a"]);
ghost.clear();
assert_eq!(ghost.iter().count(), 0);
}
#[test]
fn ghost_list_debug_peek_lru_mru() {
let mut ghost = GhostList::new(3);
assert_eq!(ghost.debug_peek_lru(), None);
assert_eq!(ghost.debug_peek_mru(), None);
ghost.record("a");
assert_eq!(ghost.debug_peek_lru(), Some(&"a"));
assert_eq!(ghost.debug_peek_mru(), Some(&"a"));
ghost.record("b");
ghost.record("c");
assert_eq!(ghost.debug_peek_lru(), Some(&"a")); assert_eq!(ghost.debug_peek_mru(), Some(&"c"));
assert_eq!(ghost.record("a"), None); assert_eq!(ghost.debug_peek_lru(), Some(&"b"));
assert_eq!(ghost.debug_peek_mru(), Some(&"a"));
}
#[test]
fn ghost_list_record_returns_evicted() {
let mut ghost = GhostList::new(2);
assert_eq!(ghost.record("a"), None);
assert_eq!(ghost.record("b"), None);
assert_eq!(ghost.len(), 2);
assert_eq!(ghost.record("c"), Some("a")); assert!(!ghost.contains(&"a"));
assert!(ghost.contains(&"b"));
assert!(ghost.contains(&"c"));
assert_eq!(ghost.record("b"), None);
assert_eq!(ghost.record("d"), Some("c"));
let mut zero_ghost = GhostList::new(0);
assert_eq!(zero_ghost.record("x"), None);
assert!(zero_ghost.is_empty());
}
#[test]
fn ghost_list_keys_alias() {
let mut ghost = GhostList::new(3);
ghost.record("a");
ghost.record("b");
ghost.record("c");
let keys_via_keys: Vec<_> = ghost.keys().cloned().collect();
let keys_via_iter: Vec<_> = ghost.iter().cloned().collect();
assert_eq!(keys_via_keys, keys_via_iter);
assert_eq!(keys_via_keys, vec!["c", "b", "a"]);
}
#[test]
fn ghost_list_default() {
let ghost: GhostList<String> = GhostList::default();
assert_eq!(ghost.capacity(), 0);
assert!(ghost.is_empty());
assert_eq!(ghost.len(), 0);
}
#[test]
fn ghost_list_clone() {
let mut ghost = GhostList::new(3);
ghost.record("a");
ghost.record("b");
ghost.record("c");
let cloned = ghost.clone();
assert_eq!(cloned.len(), ghost.len());
assert_eq!(cloned.capacity(), ghost.capacity());
assert!(cloned.contains(&"a"));
assert!(cloned.contains(&"b"));
assert!(cloned.contains(&"c"));
ghost.record("d");
assert!(ghost.contains(&"d"));
assert!(!cloned.contains(&"d"));
}
#[test]
fn ghost_list_into_iter_yields_mru_to_lru() {
let mut ghost = GhostList::new(3);
ghost.record("a");
ghost.record("b");
ghost.record("c");
let keys: Vec<_> = ghost.into_iter().collect();
assert_eq!(keys, vec!["c", "b", "a"]);
}
#[test]
fn ghost_list_into_iter_empty() {
let ghost: GhostList<&str> = GhostList::new(5);
let keys: Vec<_> = ghost.into_iter().collect();
assert!(keys.is_empty());
}
#[test]
fn ghost_list_into_iter_size_hint() {
let mut ghost = GhostList::new(4);
ghost.record("a");
ghost.record("b");
ghost.record("c");
let iter = ghost.into_iter();
assert_eq!(iter.size_hint(), (3, Some(3)));
}
#[test]
fn ghost_list_into_iter_exact_size() {
let mut ghost = GhostList::new(3);
ghost.record("x");
ghost.record("y");
let mut iter = ghost.into_iter();
assert_eq!(iter.len(), 2);
iter.next();
assert_eq!(iter.len(), 1);
iter.next();
assert_eq!(iter.len(), 0);
}
#[test]
fn ghost_list_ref_into_iter_yields_mru_to_lru() {
let mut ghost = GhostList::new(3);
ghost.record("a");
ghost.record("b");
ghost.record("c");
let keys: Vec<_> = (&ghost).into_iter().cloned().collect();
assert_eq!(keys, vec!["c", "b", "a"]);
assert_eq!(ghost.len(), 3);
}
#[test]
fn ghost_list_ref_into_iter_via_for_loop() {
let mut ghost = GhostList::new(3);
ghost.record(1u32);
ghost.record(2u32);
ghost.record(3u32);
let mut seen = Vec::new();
for key in &ghost {
seen.push(*key);
}
assert_eq!(seen, vec![3u32, 2, 1]); }
#[test]
fn ghost_list_equal_same_content_and_capacity() {
let mut a = GhostList::new(3);
a.record("x");
a.record("y");
let mut b = GhostList::new(3);
b.record("x");
b.record("y");
assert_eq!(a, b);
}
#[test]
fn ghost_list_not_equal_different_order() {
let mut a = GhostList::new(3);
a.record("x");
a.record("y");
let mut b = GhostList::new(3);
b.record("y");
b.record("x");
assert_ne!(a, b);
}
#[test]
fn ghost_list_not_equal_different_capacity() {
let mut a = GhostList::new(3);
a.record("x");
let mut b = GhostList::new(5);
b.record("x");
assert_ne!(a, b);
}
#[test]
fn ghost_list_not_equal_different_content() {
let mut a = GhostList::new(3);
a.record("x");
a.record("y");
let mut b = GhostList::new(3);
b.record("x");
b.record("z");
assert_ne!(a, b);
}
#[test]
fn ghost_list_equal_empty_lists_same_capacity() {
let a: GhostList<&str> = GhostList::new(10);
let b: GhostList<&str> = GhostList::new(10);
assert_eq!(a, b);
}
#[test]
fn ghost_list_partial_eq_is_reflexive() {
let mut ghost = GhostList::new(3);
ghost.record("a");
ghost.record("b");
assert_eq!(ghost, ghost.clone());
}
#[test]
fn ghost_list_extend_records_all_keys() {
let mut ghost = GhostList::new(5);
ghost.extend(["a", "b", "c"]);
assert_eq!(ghost.len(), 3);
assert!(ghost.contains(&"a"));
assert!(ghost.contains(&"b"));
assert!(ghost.contains(&"c"));
}
#[test]
fn ghost_list_extend_respects_capacity() {
let mut ghost = GhostList::new(2);
ghost.extend(["a", "b", "c", "d"]);
assert_eq!(ghost.len(), 2);
assert!(ghost.contains(&"c"));
assert!(ghost.contains(&"d"));
assert!(!ghost.contains(&"a"));
assert!(!ghost.contains(&"b"));
}
#[test]
fn ghost_list_extend_promotes_existing_keys() {
let mut ghost = GhostList::new(3);
ghost.record("a");
ghost.record("b");
ghost.record("c");
ghost.extend(["a"]);
let keys: Vec<_> = ghost.iter().cloned().collect();
assert_eq!(keys[0], "a"); assert_eq!(ghost.len(), 3);
}
#[test]
fn ghost_list_extend_from_iterator() {
let mut ghost = GhostList::new(10);
let keys = vec!["p", "q", "r", "s"];
ghost.extend(keys);
assert_eq!(ghost.len(), 4);
assert!(ghost.contains(&"p"));
assert!(ghost.contains(&"s"));
}
#[test]
fn ghost_list_evict_lru_removes_oldest() {
let mut ghost = GhostList::new(3);
ghost.record("a");
ghost.record("b");
ghost.record("c");
assert_eq!(ghost.evict_lru(), Some("a"));
assert!(!ghost.contains(&"a"));
assert_eq!(ghost.len(), 2);
assert_eq!(ghost.evict_lru(), Some("b"));
assert_eq!(ghost.evict_lru(), Some("c"));
assert_eq!(ghost.evict_lru(), None); assert!(ghost.is_empty());
}
#[test]
fn ghost_list_evict_lru_empty_returns_none() {
let mut ghost: GhostList<&str> = GhostList::new(5);
assert_eq!(ghost.evict_lru(), None);
}
#[test]
fn ghost_list_evict_lru_after_promotion() {
let mut ghost = GhostList::new(3);
ghost.record("a");
ghost.record("b");
ghost.record("c");
ghost.record("a");
assert_eq!(ghost.evict_lru(), Some("b"));
assert!(ghost.contains(&"a"));
assert!(ghost.contains(&"c"));
}
#[test]
fn ghost_list_iter_debug() {
let mut ghost = GhostList::new(3);
ghost.record("a");
ghost.record("b");
let iter = ghost.iter();
let debug_str = format!("{:?}", iter);
assert!(debug_str.contains("Iter"));
}
#[test]
fn ghost_list_into_iter_debug() {
let mut ghost = GhostList::new(3);
ghost.record("a");
let iter = ghost.into_iter();
let debug_str = format!("{:?}", iter);
assert!(debug_str.contains("IntoIter"));
}
#[test]
fn ghost_list_debug_does_not_leak_keys() {
const SECRET: &str = "session-token-deadbeef-SECRET";
let mut ghost = GhostList::new(4);
ghost.record(SECRET.to_string());
ghost.record("other-key".to_string());
let debug_str = format!("{:?}", ghost);
assert!(
!debug_str.contains("SECRET"),
"GhostList Debug must not echo stored keys; got: {debug_str}"
);
assert!(
!debug_str.contains("other-key"),
"GhostList Debug must not echo stored keys; got: {debug_str}"
);
assert!(debug_str.contains("GhostList"));
assert!(debug_str.contains("capacity"));
assert!(debug_str.contains("len"));
}
#[test]
fn ghost_list_debug_works_for_non_debug_keys() {
#[derive(Clone, Eq, PartialEq, Hash)]
struct NoDebug(u32);
let mut ghost = GhostList::new(2);
ghost.record(NoDebug(1));
ghost.record(NoDebug(2));
let s = format!("{:?}", ghost);
assert!(s.contains("GhostList"));
}
#[test]
#[ignore = "allocates up to 16 GiB; run: cargo test ghost_list_new_clamps_oversized_capacity -- --ignored --test-threads=1"]
fn ghost_list_new_clamps_oversized_capacity() {
let ghost: GhostList<u32> = GhostList::new(usize::MAX);
assert_eq!(
ghost.capacity(),
GhostList::<u32>::MAX_CAPACITY,
"new() must clamp to MAX_CAPACITY to avoid OOM-abort DoS"
);
assert!(ghost.is_empty());
}
#[test]
fn ghost_list_max_capacity_respects_byte_budget() {
fn assert_fits<K: Clone + Eq + Hash>() {
let max = GhostList::<K>::MAX_CAPACITY;
let list_per_entry = IntrusiveList::<K>::BYTES_PER_ENTRY;
let bucket_bytes = std::mem::size_of::<(K, SlotId)>() + 1;
let index_per_entry = bucket_bytes * 3;
let per_entry = list_per_entry + index_per_entry;
let sixteen_gib: usize = 16 * 1024 * 1024 * 1024;
let isize_quarter = (isize::MAX as usize) / 4;
let budget = sixteen_gib.min(isize_quarter);
let footprint = max.checked_mul(per_entry).unwrap_or_else(|| {
panic!("MAX_CAPACITY overflow for K={}", std::any::type_name::<K>())
});
assert!(
footprint <= budget,
"MAX_CAPACITY footprint {footprint} exceeds budget {budget} for K={}",
std::any::type_name::<K>(),
);
}
assert_fits::<u8>();
assert_fits::<u32>();
assert_fits::<u64>();
assert_fits::<[u8; 64]>();
assert_fits::<[u8; 4096]>();
}
#[test]
fn ghost_list_max_capacity_shrinks_for_large_keys() {
#[repr(C)]
#[derive(Clone, Eq, PartialEq, Hash)]
struct Fat([u8; 4096]);
let small_cap = GhostList::<u32>::MAX_CAPACITY;
let fat_cap = GhostList::<Fat>::MAX_CAPACITY;
assert!(
fat_cap < small_cap,
"MAX_CAPACITY must scale down with size_of::<K>(): \
fat_cap={fat_cap} small_cap={small_cap}"
);
}
#[test]
#[ignore = "allocates multiple GiB; run: cargo test ghost_list_max_capacity_fat_key_materializes_at_clamp -- --ignored --test-threads=1"]
fn ghost_list_max_capacity_fat_key_materializes_at_clamp() {
#[repr(C)]
#[derive(Clone, Eq, PartialEq, Hash)]
struct Fat([u8; 4096]);
let fat_cap = GhostList::<Fat>::MAX_CAPACITY;
let ghost: GhostList<Fat> = GhostList::new(usize::MAX);
assert_eq!(ghost.capacity(), fat_cap);
}
#[test]
fn ghost_list_try_new_rejects_oversized_capacity() {
let err = GhostList::<u32>::try_new(usize::MAX).unwrap_err();
assert_eq!(err.requested, usize::MAX);
assert_eq!(err.max, GhostList::<u32>::MAX_CAPACITY);
const N: usize = 65_536;
let ok = GhostList::<u32>::try_new(N).unwrap();
assert_eq!(ok.capacity(), N);
}
#[test]
#[ignore = "allocates up to 16 GiB; run: cargo test ghost_list_try_new_succeeds_at_max_capacity -- --ignored --test-threads=1"]
fn ghost_list_try_new_succeeds_at_max_capacity() {
let ok = GhostList::<u32>::try_new(GhostList::<u32>::MAX_CAPACITY).unwrap();
assert_eq!(ok.capacity(), GhostList::<u32>::MAX_CAPACITY);
}
#[test]
fn ghost_list_try_with_capacity_and_hasher_rejects_oversized_capacity() {
use std::collections::hash_map::RandomState;
type Gh = GhostList<u32, RandomState>;
let err = Gh::try_with_capacity_and_hasher(usize::MAX, RandomState::new()).unwrap_err();
assert_eq!(err.requested, usize::MAX);
assert_eq!(err.max, Gh::MAX_CAPACITY);
const N: usize = 65_536;
let ok = Gh::try_with_capacity_and_hasher(N, RandomState::new()).unwrap();
assert_eq!(ok.capacity(), N);
let empty = Gh::try_with_capacity_and_hasher(0, RandomState::new()).unwrap();
assert_eq!(empty.capacity(), 0);
assert!(empty.is_empty());
if let Some(over) = Gh::MAX_CAPACITY.checked_add(1) {
let err = Gh::try_with_capacity_and_hasher(over, RandomState::new()).unwrap_err();
assert_eq!(err.requested, over);
}
}
#[test]
#[ignore = "allocates up to 16 GiB; run: cargo test ghost_list_try_with_capacity_and_hasher_succeeds_at_max_capacity -- --ignored --test-threads=1"]
fn ghost_list_try_with_capacity_and_hasher_succeeds_at_max_capacity() {
use std::collections::hash_map::RandomState;
type Gh = GhostList<u32, RandomState>;
let ok = Gh::try_with_capacity_and_hasher(Gh::MAX_CAPACITY, RandomState::new()).unwrap();
assert_eq!(ok.capacity(), Gh::MAX_CAPACITY);
}
#[test]
fn ghost_list_from_iter_clamps_hostile_size_hint() {
struct HostileIter(usize);
impl Iterator for HostileIter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.0 == 0 {
None
} else {
self.0 -= 1;
Some(self.0 as u32)
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(usize::MAX, None)
}
}
let ghost: GhostList<u32> = HostileIter(3).collect();
assert_eq!(ghost.len(), 3);
}
#[test]
fn ghost_list_with_custom_hasher_is_dos_resistant() {
use std::collections::hash_map::RandomState;
let mut ghost: GhostList<u32, RandomState> =
GhostList::with_capacity_and_hasher(4, RandomState::new());
ghost.record(1);
ghost.record(2);
ghost.record(3);
assert!(ghost.contains(&1));
assert!(ghost.contains(&3));
assert_eq!(ghost.len(), 3);
}
#[test]
fn ghost_list_record_preserves_invariant_after_heavy_churn() {
let mut ghost = GhostList::new(8);
for i in 0..1_000u32 {
ghost.record(i);
ghost.debug_validate_invariants();
}
assert_eq!(ghost.len(), 8);
}
#[test]
fn ghost_list_approx_bytes_does_not_overflow_on_huge_capacity() {
let ghost: GhostList<u64> = GhostList::new(1024);
let bytes = ghost.approx_bytes();
assert!(bytes > 0);
assert!(bytes < usize::MAX / 2);
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_invariants_always_hold(
capacity in 1usize..20,
ops in prop::collection::vec((0u8..3, any::<u32>()), 0..50)
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
for (op, key) in ops {
match op % 3 {
0 => { ghost.record(key); }
1 => { ghost.remove(&key); }
2 => { let _ = ghost.contains(&key); }
_ => unreachable!(),
}
ghost.debug_validate_invariants();
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_len_never_exceeds_capacity(
capacity in 1usize..30,
keys in prop::collection::vec(any::<u32>(), 0..100)
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
for key in keys {
ghost.record(key);
prop_assert!(ghost.len() <= capacity);
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_empty_state_consistent(
capacity in 1usize..20,
keys in prop::collection::vec(any::<u32>(), 1..20)
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
for key in keys {
ghost.record(key);
}
ghost.clear();
prop_assert!(ghost.is_empty());
prop_assert_eq!(ghost.len(), 0);
prop_assert_eq!(ghost.capacity(), capacity);
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_lru_eviction_order(
capacity in 2usize..10,
keys in prop::collection::vec(0u32..50, 1..30)
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
let mut reference: std::collections::VecDeque<u32> = std::collections::VecDeque::new();
for key in keys {
ghost.record(key);
if let Some(pos) = reference.iter().position(|&k| k == key) {
reference.remove(pos);
} else if reference.len() >= capacity {
reference.pop_back();
}
reference.push_front(key);
}
prop_assert_eq!(ghost.len(), reference.len());
for &ref_key in &reference {
prop_assert!(ghost.contains(&ref_key));
}
let snapshot = ghost.debug_snapshot_keys();
prop_assert_eq!(snapshot, reference.iter().copied().collect::<Vec<_>>());
ghost.debug_validate_invariants();
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_rerecord_no_length_increase(
capacity in 2usize..10,
key in any::<u32>()
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
ghost.record(key);
let len_after_first = ghost.len();
ghost.record(key);
let len_after_second = ghost.len();
prop_assert_eq!(len_after_first, len_after_second);
prop_assert_eq!(len_after_first, 1);
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_rerecord_promotes_to_mru(
capacity in 2usize..10,
keys in prop::collection::vec(0u32..20, 2..10)
) {
prop_assume!(keys.len() >= 2);
let mut ghost: GhostList<u32> = GhostList::new(capacity);
for &key in &keys {
ghost.record(key);
}
if ghost.is_empty() {
return Ok(());
}
let snapshot = ghost.debug_snapshot_keys();
if snapshot.is_empty() {
return Ok(());
}
let promoted_key = snapshot[snapshot.len() - 1];
ghost.record(promoted_key);
let new_snapshot = ghost.debug_snapshot_keys();
if !new_snapshot.is_empty() {
prop_assert_eq!(new_snapshot[0], promoted_key);
}
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_promoted_keys_survive_longer(
capacity in 3usize..8,
fill_keys in prop::collection::vec(0u32..10, 3..8)
) {
prop_assume!(fill_keys.len() >= capacity);
let mut ghost: GhostList<u32> = GhostList::new(capacity);
let mut unique = Vec::new();
for &key in &fill_keys {
if !unique.contains(&key) && unique.len() < capacity {
ghost.record(key);
unique.push(key);
}
}
if unique.len() < capacity {
return Ok(());
}
let snapshot = ghost.debug_snapshot_keys();
let promoted_key = snapshot[snapshot.len() - 1];
ghost.record(promoted_key);
for i in 100..(100 + capacity - 1) {
ghost.record(i as u32);
}
prop_assert!(ghost.contains(&promoted_key));
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_contains_after_record(
capacity in 1usize..20,
key in any::<u32>()
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
ghost.record(key);
prop_assert!(ghost.contains(&key));
ghost.remove(&key);
prop_assert!(!ghost.contains(&key));
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_remove_returns_correct_bool(
capacity in 1usize..10,
keys in prop::collection::vec(any::<u32>(), 1..20)
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
for &key in &keys {
ghost.record(key);
}
for &key in &keys {
let was_present = ghost.contains(&key);
let remove_result = ghost.remove(&key);
if was_present {
prop_assert!(remove_result);
prop_assert!(!ghost.contains(&key));
}
prop_assert!(!ghost.remove(&key));
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_clear_resets_state(
capacity in 1usize..20,
keys in prop::collection::vec(any::<u32>(), 1..30)
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
for key in keys {
ghost.record(key);
}
ghost.clear();
prop_assert!(ghost.is_empty());
prop_assert_eq!(ghost.len(), 0);
prop_assert_eq!(ghost.capacity(), capacity);
ghost.debug_validate_invariants();
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_clear_shrink_same_as_clear(
capacity in 1usize..20,
keys in prop::collection::vec(any::<u32>(), 1..30)
) {
let mut ghost1: GhostList<u32> = GhostList::new(capacity);
let mut ghost2: GhostList<u32> = GhostList::new(capacity);
for &key in &keys {
ghost1.record(key);
ghost2.record(key);
}
ghost1.clear();
ghost2.clear_shrink();
prop_assert_eq!(ghost1.len(), ghost2.len());
prop_assert_eq!(ghost1.is_empty(), ghost2.is_empty());
prop_assert_eq!(ghost1.capacity(), ghost2.capacity());
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_usable_after_clear(
capacity in 1usize..10,
keys1 in prop::collection::vec(any::<u32>(), 1..20),
keys2 in prop::collection::vec(any::<u32>(), 1..20)
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
for key in keys1 {
ghost.record(key);
}
ghost.clear();
for &key in &keys2 {
ghost.record(key);
}
let unique_keys2: std::collections::HashSet<_> = keys2.into_iter().collect();
let expected_len = unique_keys2.len().min(capacity);
prop_assert_eq!(ghost.len(), expected_len);
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_zero_capacity_always_empty(
keys in prop::collection::vec(any::<u32>(), 0..30)
) {
let mut ghost: GhostList<u32> = GhostList::new(0);
for key in keys {
ghost.record(key);
prop_assert!(ghost.is_empty());
prop_assert_eq!(ghost.len(), 0);
prop_assert_eq!(ghost.capacity(), 0);
prop_assert!(!ghost.contains(&key));
}
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_record_batch_records_all(
capacity in 5usize..20,
keys in prop::collection::vec(0u32..20, 1..10)
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
let count = ghost.record_batch(&keys);
prop_assert_eq!(count, keys.len());
let unique_keys: std::collections::HashSet<_> = keys.into_iter().collect();
let expected_len = unique_keys.len().min(capacity);
prop_assert_eq!(ghost.len(), expected_len);
}
#[cfg_attr(miri, ignore)]
#[test]
fn prop_remove_batch_only_present(
capacity in 5usize..20,
record_keys in prop::collection::vec(0u32..10, 1..10),
remove_keys in prop::collection::vec(0u32..20, 1..10)
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
ghost.record_batch(&record_keys);
let removed = ghost.remove_batch(&remove_keys);
let record_set: std::collections::HashSet<_> = record_keys.into_iter().collect();
let remove_set: std::collections::HashSet<_> = remove_keys.into_iter().collect();
let mut expected_removed = 0;
for key in remove_set {
if record_set.contains(&key) && !ghost.is_empty() {
expected_removed += 1;
}
}
prop_assert!(removed <= expected_removed || removed <= record_set.len());
}
}
proptest! {
#[cfg_attr(miri, ignore)]
#[test]
fn prop_matches_reference_implementation(
capacity in 2usize..10,
keys in prop::collection::vec(0u32..20, 0..30)
) {
let mut ghost: GhostList<u32> = GhostList::new(capacity);
let mut reference: std::collections::VecDeque<u32> = std::collections::VecDeque::new();
for key in keys {
ghost.record(key);
if let Some(pos) = reference.iter().position(|&k| k == key) {
reference.remove(pos);
} else if reference.len() >= capacity {
reference.pop_back(); }
reference.push_front(key);
prop_assert_eq!(ghost.len(), reference.len());
for &ref_key in &reference {
prop_assert!(ghost.contains(&ref_key));
}
let snapshot = ghost.debug_snapshot_keys();
prop_assert_eq!(snapshot.len(), reference.len());
for (snap_key, ref_key) in snapshot.iter().zip(reference.iter()) {
prop_assert_eq!(snap_key, ref_key);
}
}
}
}
}