use std::ops::{Index, IndexMut};
pub(crate) const PAGE: usize = 64;
const SHIFT: u32 = PAGE.trailing_zeros();
const MASK: usize = PAGE - 1;
pub(crate) fn page_reserve(ceiling: usize) -> usize {
PAGE.min(ceiling)
}
pub(crate) struct Slots<T> {
pages: Vec<Box<[Option<T>]>>,
ceiling: usize,
}
impl<T> Slots<T> {
const VACANT: Option<T> = None;
pub fn new(ceiling: usize) -> Self {
let mut slots = Self {
pages: Vec::new(),
ceiling,
};
slots.grow();
slots
}
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub fn filled(ceiling: usize, mut build: impl FnMut() -> T) -> Self {
let mut slots = Self::new(ceiling);
for slot in slots.iter_mut() {
*slot = Some(build());
}
slots
}
fn grow(&mut self) -> bool {
let base = self.pages.len() * PAGE;
if base >= self.ceiling {
return false;
}
let len = PAGE.min(self.ceiling - base);
self.pages.push(
(0..len)
.map(|_| None)
.collect::<Vec<_>>()
.into_boxed_slice(),
);
true
}
pub fn len(&self) -> usize {
self.ceiling
}
pub fn materialised(&self) -> usize {
(self.pages.len() * PAGE).min(self.ceiling)
}
fn read(&self, i: usize) -> Option<&Option<T>> {
if i >= self.len() {
return None;
}
Some(match self.pages.get(i >> SHIFT) {
Some(page) => &page[i & MASK],
None => &Self::VACANT,
})
}
fn materialise(&mut self, i: usize) -> &mut Option<T> {
assert!(
i < self.len(),
"slot {i} is beyond the configured ceiling {}",
self.ceiling
);
let page = i >> SHIFT;
while self.pages.len() <= page {
assert!(self.grow(), "the ceiling admits index {i}");
}
debug_assert!(i < self.materialised(), "pages are a dense prefix");
&mut self.pages[page][i & MASK]
}
pub fn get(&self, i: usize) -> Option<&Option<T>> {
self.read(i)
}
pub fn get_mut(&mut self, i: usize) -> Option<&mut Option<T>> {
(i < self.len()).then(|| self.materialise(i))
}
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub fn get_or_insert_with(&mut self, i: usize, build: impl FnOnce() -> T) -> &mut T {
self.materialise(i).get_or_insert_with(build)
}
pub fn iter(&self) -> Iter<'_, T> {
self.into_iter()
}
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub fn iter_mut(&mut self) -> IterMut<'_, T> {
self.into_iter()
}
}
pub(crate) type Iter<'a, T> = std::iter::Flatten<std::slice::Iter<'a, Box<[Option<T>]>>>;
pub(crate) type IterMut<'a, T> = std::iter::Flatten<std::slice::IterMut<'a, Box<[Option<T>]>>>;
impl<'a, T> IntoIterator for &'a Slots<T> {
type Item = &'a Option<T>;
type IntoIter = Iter<'a, T>;
fn into_iter(self) -> Self::IntoIter {
self.pages.iter().flatten()
}
}
impl<'a, T> IntoIterator for &'a mut Slots<T> {
type Item = &'a mut Option<T>;
type IntoIter = IterMut<'a, T>;
fn into_iter(self) -> Self::IntoIter {
self.pages.iter_mut().flatten()
}
}
impl<T> IntoIterator for Slots<T> {
type Item = Option<T>;
type IntoIter = std::iter::FlatMap<
std::vec::IntoIter<Box<[Option<T>]>>,
std::vec::Vec<Option<T>>,
fn(Box<[Option<T>]>) -> std::vec::Vec<Option<T>>,
>;
fn into_iter(self) -> Self::IntoIter {
self.pages.into_iter().flat_map(Vec::from)
}
}
impl<T> Index<usize> for Slots<T> {
type Output = Option<T>;
fn index(&self, i: usize) -> &Option<T> {
self.read(i)
.unwrap_or_else(|| panic!("slot {i} is beyond the configured ceiling {}", self.ceiling))
}
}
impl<T> IndexMut<usize> for Slots<T> {
fn index_mut(&mut self, i: usize) -> &mut Option<T> {
self.materialise(i)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unmaterialised_slot_reads_vacant_without_allocating() {
let slots: Slots<u32> = Slots::new(1_000_000);
assert_eq!(
slots.materialised(),
PAGE,
"exactly page zero is built at construction, whatever the ceiling"
);
assert_eq!(slots.len(), 1_000_000, "the ceiling is still the length");
assert!(slots[999_999].is_none());
assert!(slots.get(999_999).expect("within the ceiling").is_none());
assert_eq!(slots.materialised(), PAGE, "reading materialises nothing");
assert!(slots.get(1_000_000).is_none(), "past the ceiling");
}
#[test]
fn growth_leaves_every_earlier_slot_at_its_address() {
let mut slots: Slots<u32> = Slots::new(4096);
let mut addresses = Vec::new();
for i in 0..PAGE {
slots[i] = Some(i as u32);
addresses.push(std::ptr::from_ref(&slots[i]));
}
assert_eq!(slots.materialised(), PAGE);
for i in PAGE..PAGE * 8 {
slots[i] = Some(i as u32);
}
assert_eq!(slots.materialised(), PAGE * 8, "seven more pages");
for (i, address) in addresses.iter().enumerate() {
assert_eq!(slots[i], Some(i as u32), "value survived growth");
assert_eq!(
std::ptr::from_ref(&slots[i]),
*address,
"slot {i} moved across a page boundary"
);
}
for i in 0..PAGE * 8 {
assert_eq!(slots[i], Some(i as u32), "index still names its slot");
}
}
#[test]
fn iteration_matches_the_flat_vector_over_occupied_slots() {
let mut slots: Slots<u32> = Slots::new(4096);
for i in [0, 5, 63, 64, 130] {
slots[i] = Some(i as u32);
}
let seen: Vec<u32> = slots.iter().flatten().copied().collect();
assert_eq!(seen, vec![0, 5, 63, 64, 130]);
let indices: Vec<usize> = slots
.iter()
.enumerate()
.filter_map(|(i, slot)| slot.map(|_| i))
.collect();
assert_eq!(indices, vec![0, 5, 63, 64, 130], "enumerate keeps indices");
assert!(slots.iter().any(Option::is_some));
for slot in slots.iter_mut() {
*slot = None;
}
assert!(slots.iter().all(Option::is_none));
}
#[test]
fn a_page_holds_exactly_its_own_index_range() {
let mut slots: Slots<u32> = Slots::new(PAGE * 3);
assert_eq!(slots.materialised(), PAGE);
slots[PAGE * 3 - 1] = Some(7);
assert_eq!(slots.materialised(), PAGE * 3);
assert_eq!(slots[PAGE * 3 - 1], Some(7));
assert!(
slots.iter().filter(|s| s.is_some()).count() == 1,
"materialising a page leaves its other slots vacant"
);
}
#[test]
#[should_panic(expected = "beyond the configured ceiling")]
fn writing_past_the_ceiling_panics_like_a_flat_vector() {
let mut slots: Slots<u32> = Slots::new(8);
slots[8] = Some(1);
}
#[test]
fn a_ceiling_below_a_page_builds_only_the_slots_it_allows() {
let mut slots: Slots<u32> = Slots::new(3);
assert_eq!(slots.materialised(), 3, "page zero stops at the ceiling");
assert_eq!(slots.iter().count(), 3);
slots[2] = Some(9);
assert_eq!(slots[2], Some(9));
}
#[test]
fn filled_builds_page_zeros_elements_and_later_pages_on_demand() {
let mut built = 0;
let mut slots: Slots<u32> = Slots::filled(4096, || {
built += 1;
Some(built).map(|n| n as u32).expect("counter")
});
assert_eq!(built, PAGE, "exactly page zero is filled at construction");
assert!(slots[0].is_some());
assert!(slots[PAGE - 1].is_some());
assert!(slots[PAGE].is_none(), "a later page starts vacant");
let value = *slots.get_or_insert_with(PAGE, || 4242);
assert_eq!(value, 4242);
}
#[test]
#[should_panic(expected = "beyond the configured ceiling")]
fn reading_past_the_ceiling_panics_like_a_flat_vector() {
let slots: Slots<u32> = Slots::new(8);
let _ = &slots[8];
}
}