#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SlotKey {
pub index: u32,
pub generation: u16,
}
impl SlotKey {
pub const fn new(index: u32, generation: u16) -> Self {
Self { index, generation }
}
pub const fn encode(self) -> u64 {
((self.index as u64) << 16) | self.generation as u64
}
pub const fn decode(key: u64) -> Self {
Self {
index: (key >> 16) as u32,
generation: (key & 0xFFFF) as u16,
}
}
pub const fn ungenerational(self) -> Self {
Self { index: self.index, generation: 0 }
}
pub const fn same_occupant(self, other: Self) -> bool {
self.index == other.index && self.generation == other.generation
}
}
impl From<SlotKey> for u64 {
fn from(key: SlotKey) -> u64 {
key.encode()
}
}
impl From<u64> for SlotKey {
fn from(key: u64) -> SlotKey {
SlotKey::decode(key)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ReusePolicy {
#[default]
Lifo,
Fifo,
}
#[derive(Clone, Debug, Default)]
pub struct SlotAllocator {
slots: Vec<(u16, bool)>,
free: std::collections::VecDeque<u32>,
policy: ReusePolicy,
live: usize,
}
impl SlotAllocator {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(slots: usize) -> Self {
Self {
slots: Vec::with_capacity(slots),
free: std::collections::VecDeque::with_capacity(slots),
policy: ReusePolicy::default(),
live: 0,
}
}
pub fn with_policy(mut self, policy: ReusePolicy) -> Self {
self.policy = policy;
self
}
pub fn alloc(&mut self) -> SlotKey {
let index = match self.policy {
ReusePolicy::Lifo => self.free.pop_back(),
ReusePolicy::Fifo => self.free.pop_front(),
};
self.live += 1;
match index {
Some(index) => {
let slot = &mut self.slots[index as usize];
slot.1 = true;
SlotKey::new(index, slot.0)
}
None => {
let index = self.slots.len() as u32;
self.slots.push((0, true));
SlotKey::new(index, 0)
}
}
}
pub fn free(&mut self, key: SlotKey) -> bool {
let Some(slot) = self.slots.get_mut(key.index as usize) else {
return false;
};
if !slot.1 || slot.0 != key.generation {
return false;
}
slot.1 = false;
slot.0 = slot.0.wrapping_add(1);
self.free.push_back(key.index);
self.live -= 1;
true
}
pub fn is_live(&self, key: SlotKey) -> bool {
self.slots.get(key.index as usize).is_some_and(|slot| slot.1 && slot.0 == key.generation)
}
pub fn is_occupied(&self, index: u32) -> bool {
self.slots.get(index as usize).is_some_and(|slot| slot.1)
}
pub fn key(&self, index: u32) -> Option<SlotKey> {
self.slots.get(index as usize).filter(|slot| slot.1).map(|slot| SlotKey::new(index, slot.0))
}
pub fn iter(&self) -> impl Iterator<Item = SlotKey> + '_ {
self
.slots
.iter()
.enumerate()
.filter(|(_, slot)| slot.1)
.map(|(index, slot)| SlotKey::new(index as u32, slot.0))
}
pub fn len(&self) -> usize {
self.live
}
pub fn is_empty(&self) -> bool {
self.live == 0
}
pub fn index_space(&self) -> usize {
self.slots.len()
}
pub fn clear(&mut self) {
self.free.clear();
for (index, slot) in self.slots.iter_mut().enumerate() {
if slot.1 {
slot.1 = false;
slot.0 = slot.0.wrapping_add(1);
}
self.free.push_back(index as u32);
}
self.live = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_key_survives_the_round_trip() {
for (index, generation) in [(0u32, 0u16), (1, 1), (41, 7), (65_535, 65_535), (1_000_000, 3)] {
let key = SlotKey::new(index, generation);
assert_eq!(SlotKey::decode(key.encode()), key, "{index}:{generation}");
}
}
#[test]
fn a_reused_slot_is_a_different_key() {
let died = SlotKey::new(41, 7);
let refilled = SlotKey::new(41, 8);
assert_ne!(died.encode(), refilled.encode());
assert!(!died.same_occupant(refilled));
assert_eq!(died.index, refilled.index, "and it really is the same slot");
}
#[test]
fn dropping_the_generation_makes_a_reused_slot_indistinguishable() {
let died = SlotKey::new(41, 7).ungenerational();
let refilled = SlotKey::new(41, 8).ungenerational();
assert_eq!(died, refilled, "without generations the two occupants are one key");
}
#[test]
fn keys_order_by_slot_then_occupant() {
let mut keys = [SlotKey::new(2, 0), SlotKey::new(1, 9), SlotKey::new(1, 2)];
keys.sort();
assert_eq!(keys, [SlotKey::new(1, 2), SlotKey::new(1, 9), SlotKey::new(2, 0)]);
assert!(keys.windows(2).all(|w| w[0].encode() < w[1].encode()), "the encoding sorts the same way");
}
}
#[cfg(test)]
mod allocator_tests {
use super::*;
#[test]
fn a_freed_index_comes_back_under_a_new_occupant() {
let mut pool = SlotAllocator::new();
let first = pool.alloc();
assert!(pool.free(first));
let second = pool.alloc();
assert_eq!(second.index, first.index, "the index space stays dense");
assert_ne!(second.generation, first.generation, "but the occupant is distinguishable");
assert!(pool.is_live(second) && !pool.is_live(first));
}
#[test]
fn a_stale_handle_cannot_free_whoever_took_the_slot() {
let mut pool = SlotAllocator::new();
let dead = pool.alloc();
pool.free(dead);
let live = pool.alloc();
assert!(!pool.free(dead), "a stale free must not fire");
assert!(pool.is_live(live), "and the current occupant survived it");
assert_eq!(pool.len(), 1);
}
#[test]
fn freeing_twice_is_refused() {
let mut pool = SlotAllocator::new();
let key = pool.alloc();
assert!(pool.free(key));
assert!(!pool.free(key), "a double free must not put the index in the list twice");
let a = pool.alloc();
let b = pool.alloc();
assert_ne!(a.index, b.index);
}
#[test]
fn the_generation_bumps_on_free_even_if_the_slot_is_never_reused() {
let mut pool = SlotAllocator::new();
let key = pool.alloc();
pool.free(key);
assert!(!pool.is_live(key), "invalidated immediately, with nothing reusing the slot");
assert!(pool.key(key.index).is_none());
}
#[test]
fn the_index_space_settles_at_the_high_water_mark() {
let mut pool = SlotAllocator::new();
for _ in 0..100 {
let batch: Vec<SlotKey> = (0..10).map(|_| pool.alloc()).collect();
for key in batch {
pool.free(key);
}
}
assert_eq!(pool.index_space(), 10, "1000 allocations over 10 simultaneous slots");
assert_eq!(pool.len(), 0);
}
#[test]
fn the_reuse_policy_decides_the_order_indices_come_back() {
let mut lifo = SlotAllocator::new().with_policy(ReusePolicy::Lifo);
let mut fifo = SlotAllocator::new().with_policy(ReusePolicy::Fifo);
for pool in [&mut lifo, &mut fifo] {
let keys: Vec<SlotKey> = (0..4).map(|_| pool.alloc()).collect();
for key in keys {
pool.free(key);
}
}
assert_eq!(lifo.alloc().index, 3, "newest freed first");
assert_eq!(fifo.alloc().index, 0, "oldest freed first, so a slot rests longest");
}
#[test]
fn live_keys_are_enumerable_and_countable() {
let mut pool = SlotAllocator::new();
let keys: Vec<SlotKey> = (0..5).map(|_| pool.alloc()).collect();
pool.free(keys[1]);
pool.free(keys[3]);
let live: Vec<SlotKey> = pool.iter().collect();
assert_eq!(live, vec![keys[0], keys[2], keys[4]]);
assert_eq!(pool.len(), 3);
assert_eq!(pool.index_space(), 5, "the freed indices still exist, they are just empty");
assert_eq!(pool.key(keys[2].index), Some(keys[2]));
}
#[test]
fn clearing_invalidates_every_outstanding_handle() {
let mut pool = SlotAllocator::new();
let old: Vec<SlotKey> = (0..3).map(|_| pool.alloc()).collect();
pool.clear();
assert_eq!(pool.len(), 0);
assert!(old.iter().all(|key| !pool.is_live(*key)));
let fresh = pool.alloc();
assert!(!old.contains(&fresh), "a rebuilt world reissued an old handle");
assert_eq!(pool.index_space(), 3, "and the index space is reused rather than regrown");
}
#[test]
fn the_generation_wraps_and_the_ceiling_is_where_it_is_documented() {
let mut pool = SlotAllocator::new();
let first = pool.alloc();
assert_eq!(first.generation, 0);
for _ in 0..u16::MAX as u32 + 1 {
let key = pool.alloc_or_reuse_for_test();
pool.free(key);
}
let wrapped = pool.alloc();
assert_eq!(wrapped.index, 0, "still the same slot");
assert_eq!(wrapped.generation, 0, "and its generation has come all the way round");
}
impl SlotAllocator {
fn alloc_or_reuse_for_test(&mut self) -> SlotKey {
if let Some(key) = self.key(0) {
return key;
}
self.alloc()
}
}
}