use std::{
cell::{Cell, UnsafeCell},
fmt,
mem::MaybeUninit,
};
use crate::heap::{HeapId, free_list::FreeList, stable_heap::iter::HeapEntriesIter};
const PAGE_SIZE: usize = 256;
type Page<T> = Box<[Slot<T>; PAGE_SIZE]>;
type Slot<T> = MaybeUninit<Option<T>>;
pub(crate) struct StableHeap<T> {
pages: UnsafeCell<Vec<Page<T>>>,
len: Cell<usize>,
free_list: FreeList,
}
impl<T: fmt::Debug> fmt::Debug for StableHeap<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
.entries(unsafe { HeapEntriesIter::new(self) })
.finish()
}
}
impl<T> StableHeap<T> {
pub fn with_capacity(capacity: usize) -> Self {
let num_pages = capacity.div_ceil(PAGE_SIZE);
let mut pages = Vec::with_capacity(num_pages);
for _ in 0..num_pages {
pages.push(create_page());
}
Self {
pages: UnsafeCell::new(pages),
len: Cell::new(0),
free_list: FreeList::new(),
}
}
#[inline]
pub fn len(&self) -> usize {
self.len.get()
}
#[inline]
#[track_caller]
pub fn get(&self, id: HeapId) -> &T {
let slot = unsafe { self.slot_at(id) };
let Some(entry) = slot else {
panic!("StableHeap::get - {id:?} out of bounds");
};
entry.as_ref().expect("HeapEntries::get - data already freed")
}
#[inline]
#[track_caller]
pub fn entry(&mut self, id: HeapId) -> Option<StableHeapEntry<'_, T>> {
assert!(id.index() < self.len.get(), "StableHeap::entry - {id:?} out of bounds");
let (page_idx, slot_idx) = Self::page_slot_indices(id);
let pages = self.pages.get_mut();
let slot = &mut pages[page_idx][slot_idx];
let value = unsafe { slot.assume_init_mut() };
StableHeapEntry::new(id, value, &mut self.free_list)
}
pub fn allocate(&self, value: T) -> HeapId {
let pages = unsafe { self.pages.get().as_mut_unchecked() };
let (id, slot) = if let Some(id) = self.free_list.pop() {
let (page_idx, slot_idx) = Self::page_slot_indices(id);
let slot = &mut pages[page_idx][slot_idx];
debug_assert!(
unsafe { slot.assume_init_ref() }.is_none(),
"allocate - popped free slot {id:?} is not actually free"
);
(id, slot)
} else {
let id = HeapId::from_index(self.len.get());
let (page_idx, slot_idx) = Self::page_slot_indices(id);
if page_idx >= pages.len() {
pages.push(create_page());
}
self.len.set(id.index() + 1);
(id, &mut pages[page_idx][slot_idx])
};
slot.write(Some(value));
id
}
#[cfg(any(feature = "ref-count-return", test))]
pub fn iter(&self) -> impl Iterator<Item = (HeapId, &T)> {
unsafe { HeapEntriesIter::new(self) }.filter_map(|(idx, slot)| slot.map(|s| (HeapId::from_index(idx), s)))
}
#[track_caller]
pub unsafe fn slot_at(&self, id: HeapId) -> Option<&Option<T>> {
if id.index() >= self.len.get() {
return None;
}
let (page_idx, slot_idx) = Self::page_slot_indices(id);
let pages = unsafe { self.pages.get().as_ref_unchecked() };
let slot = &pages[page_idx][slot_idx];
Some(unsafe { slot.assume_init_ref() })
}
fn page_slot_indices(id: HeapId) -> (usize, usize) {
let index = id.index();
(index / PAGE_SIZE, index % PAGE_SIZE)
}
}
#[expect(clippy::unnecessary_box_returns, reason = "entire intent is to heap-allocate")]
fn create_page<T>() -> Box<[Slot<T>; PAGE_SIZE]> {
let raw = Box::into_raw(Box::<[Slot<T>]>::new_uninit_slice(PAGE_SIZE)).cast();
unsafe { Box::from_raw(raw) }
}
mod stable_heap_entry {
use std::ops::{Deref, DerefMut};
use crate::heap::{HeapId, free_list::FreeList};
pub struct StableHeapEntry<'a, T> {
id: HeapId,
value: &'a mut Option<T>,
free_list: &'a mut FreeList,
}
impl<'a, T> StableHeapEntry<'a, T> {
pub fn new(id: HeapId, value: &'a mut Option<T>, free_list: &'a mut FreeList) -> Option<Self> {
value.is_some().then_some(Self { id, value, free_list })
}
pub fn free(self) -> T {
self.free_list.push(self.id);
unsafe { self.value.take().unwrap_unchecked() }
}
pub fn get(&self) -> &T {
unsafe { self.value.as_ref().unwrap_unchecked() }
}
pub fn get_mut(&mut self) -> &mut T {
unsafe { self.value.as_mut().unwrap_unchecked() }
}
}
impl<T> Deref for StableHeapEntry<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.get()
}
}
impl<T> DerefMut for StableHeapEntry<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.get_mut()
}
}
}
use stable_heap_entry::StableHeapEntry;
impl<T> serde::Serialize for StableHeap<T>
where
T: serde::Serialize,
{
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_seq(unsafe { HeapEntriesIter::new(self) }.map(|(_idx, slot)| slot))
}
}
impl<'de, T> serde::Deserialize<'de> for StableHeap<T>
where
T: serde::Deserialize<'de>,
{
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let entries: Vec<Option<T>> = Vec::deserialize(deserializer)?;
let mut this = Self::with_capacity(entries.len());
this.free_list = entries
.iter()
.enumerate()
.filter(|(_, entry)| entry.is_none())
.map(|(idx, _)| HeapId::from_index(idx))
.collect::<Vec<_>>()
.into();
this.len.set(entries.len());
let pages = this.pages.get_mut();
for (index, entry) in entries.into_iter().enumerate() {
let (page_idx, slot_idx) = Self::page_slot_indices(HeapId::from_index(index));
pages[page_idx][slot_idx].write(entry);
}
Ok(this)
}
}
impl<T> Drop for StableHeap<T> {
fn drop(&mut self) {
let len = self.len.get();
let pages = self.pages.get_mut();
for i in 0..len {
let slot = &mut pages[i / PAGE_SIZE][i % PAGE_SIZE];
unsafe { slot.assume_init_drop() };
}
}
}
mod iter {
use super::StableHeap;
use crate::heap::HeapId;
pub(super) struct HeapEntriesIter<'a, T> {
entries: &'a StableHeap<T>,
index: usize,
}
impl<'a, T> HeapEntriesIter<'a, T> {
pub unsafe fn new(entries: &'a StableHeap<T>) -> Self {
Self { entries, index: 0 }
}
}
impl<'a, T> Iterator for HeapEntriesIter<'a, T> {
type Item = (usize, Option<&'a T>);
fn next(&mut self) -> Option<Self::Item> {
let current_index = self.index;
let entry = unsafe { self.entries.slot_at(HeapId::from_index(current_index)) }?;
self.index += 1;
Some((current_index, entry.as_ref()))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.entries.len().saturating_sub(self.index);
(remaining, Some(remaining))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allocate_while_reference_alive() {
let entries = StableHeap::with_capacity(16);
let id_a = entries.allocate("a");
let ref_a = entries.get(id_a);
let id_b = entries.allocate("b");
assert_eq!(*ref_a, "a");
assert_eq!(*entries.get(id_b), "b");
}
#[test]
fn allocate_triggers_new_page_while_reference_alive() {
let entries = StableHeap::with_capacity(PAGE_SIZE);
for _ in 0..PAGE_SIZE {
entries.allocate("fill");
}
assert_eq!(entries.len(), PAGE_SIZE);
let first_ref = entries.get(HeapId::from_index(0));
let overflow_id = entries.allocate("overflow");
assert_eq!(*first_ref, "fill");
assert_eq!(*entries.get(overflow_id), "overflow");
}
#[test]
fn free_list_reuse_while_reference_alive() {
let mut entries = StableHeap::with_capacity(16);
let id_a = entries.allocate("a");
let id_b = entries.allocate("b");
let _id_c = entries.allocate("c");
entries.entry(id_b).unwrap().free();
let ref_a = entries.get(id_a);
let id_reused = entries.allocate("reused");
assert_eq!(id_reused, id_b);
assert_eq!(*ref_a, "a");
assert_eq!(*entries.get(id_reused), "reused");
}
#[test]
fn multiple_live_references_during_allocation() {
let entries = StableHeap::with_capacity(PAGE_SIZE * 2);
for _ in 0..PAGE_SIZE * 2 {
entries.allocate("filler");
}
let ref_first_page = entries.get(HeapId::from_index(0));
let ref_second_page = entries.get(HeapId::from_index(PAGE_SIZE));
let new_id = entries.allocate("new");
assert_eq!(*ref_first_page, "filler");
assert_eq!(*ref_second_page, "filler");
assert_eq!(*entries.get(new_id), "new");
}
#[test]
fn allocate_into_freed_slot_does_not_alias_other_slots() {
let mut entries = StableHeap::with_capacity(16);
let ids: Vec<_> = (0..8).map(|_| entries.allocate("original")).collect();
for &id in ids.iter().step_by(2) {
entries.entry(id).unwrap().free();
}
let live_refs: Vec<_> = ids.iter().skip(1).step_by(2).map(|id| entries.get(*id)).collect();
for _ in 0..4 {
entries.allocate("realloc");
}
for r in &live_refs {
assert_eq!(**r, "original");
}
}
}