use core::mem::size_of;
use core::ptr::NonNull;
#[repr(C)]
struct FreeBlock {
next: Option<NonNull<Self>>,
}
#[allow(dead_code)]
pub(crate) struct FreeList {
head: Option<NonNull<FreeBlock>>,
len: usize,
}
#[allow(dead_code)]
pub(crate) struct Batch {
head: Option<NonNull<FreeBlock>>,
tail: Option<NonNull<FreeBlock>>,
len: usize,
}
unsafe impl Send for FreeList {}
unsafe impl Send for Batch {}
#[allow(dead_code)]
impl FreeList {
#[must_use]
pub(crate) const fn new() -> Self {
Self { head: None, len: 0 }
}
#[must_use]
pub(crate) const fn len(&self) -> usize {
self.len
}
#[must_use]
pub(crate) const fn is_empty(&self) -> bool {
self.head.is_none()
}
#[allow(clippy::missing_const_for_fn)]
pub(crate) unsafe fn push_block(&mut self, block: NonNull<u8>) {
let mut block = block.cast::<FreeBlock>();
unsafe {
block.as_mut().next = self.head;
}
self.head = Some(block);
self.len += 1;
}
#[must_use]
pub(crate) unsafe fn pop_block(&mut self) -> Option<NonNull<u8>> {
let mut head = self.head?;
let next = unsafe { head.as_ref().next };
self.head = next;
self.len -= 1;
unsafe {
head.as_mut().next = None;
}
Some(head.cast())
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) unsafe fn push_batch(&mut self, batch: Batch) {
let Batch { head, tail, len } = batch;
if len == 0 {
return;
}
let head = head.unwrap_or_else(|| unreachable!("non-empty batch must have a head"));
let mut tail = tail.unwrap_or_else(|| unreachable!("non-empty batch must have a tail"));
unsafe {
tail.as_mut().next = self.head;
}
self.head = Some(head);
self.len += len;
}
#[must_use]
pub(crate) unsafe fn pop_batch(&mut self, max: usize) -> Batch {
if max == 0 || self.is_empty() {
return Batch::empty();
}
let take = core::cmp::min(max, self.len);
let head = self
.head
.unwrap_or_else(|| unreachable!("non-empty list must have a head"));
let mut tail = head;
for _ in 1..take {
tail = unsafe {
tail.as_ref()
.next
.unwrap_or_else(|| unreachable!("free list shorter than recorded length"))
};
}
let remainder = unsafe { tail.as_ref().next };
self.head = remainder;
unsafe {
tail.as_mut().next = None;
}
self.len -= take;
Batch {
head: Some(head),
tail: Some(tail),
len: take,
}
}
}
#[allow(dead_code)]
impl Batch {
#[must_use]
pub(crate) const fn empty() -> Self {
Self {
head: None,
tail: None,
len: 0,
}
}
#[must_use]
pub(crate) const fn len(&self) -> usize {
self.len
}
#[must_use]
pub(crate) const fn is_empty(&self) -> bool {
self.head.is_none()
}
}
const _: [(); size_of::<FreeBlock>()] = [(); size_of::<Option<NonNull<FreeBlock>>>()];
#[cfg(test)]
mod tests {
use super::{Batch, FreeList};
use core::ptr::NonNull;
#[repr(align(64))]
struct TestBlock([u8; 64]);
impl TestBlock {
const fn new() -> Self {
Self([0; 64])
}
fn as_ptr(&mut self) -> NonNull<u8> {
NonNull::from(&mut self.0).cast()
}
}
#[test]
fn push_and_pop_are_lifo() {
let mut list = FreeList::new();
let mut blocks = [TestBlock::new(), TestBlock::new(), TestBlock::new()];
let ptrs = blocks.each_mut().map(TestBlock::as_ptr);
unsafe {
list.push_block(ptrs[0]);
list.push_block(ptrs[1]);
list.push_block(ptrs[2]);
}
assert_eq!(list.len(), 3);
assert!(!list.is_empty());
unsafe {
assert_eq!(list.pop_block(), Some(ptrs[2]));
assert_eq!(list.pop_block(), Some(ptrs[1]));
assert_eq!(list.pop_block(), Some(ptrs[0]));
}
assert_eq!(list.len(), 0);
assert!(list.is_empty());
}
#[test]
fn popping_empty_list_returns_none() {
let mut list = FreeList::new();
unsafe {
assert_eq!(list.pop_block(), None);
}
assert_eq!(list.len(), 0);
assert!(list.is_empty());
}
#[test]
fn pop_batch_detaches_requested_prefix() {
let mut list = FreeList::new();
let mut blocks = [
TestBlock::new(),
TestBlock::new(),
TestBlock::new(),
TestBlock::new(),
];
let ptrs = blocks.each_mut().map(TestBlock::as_ptr);
unsafe {
list.push_block(ptrs[0]);
list.push_block(ptrs[1]);
list.push_block(ptrs[2]);
list.push_block(ptrs[3]);
}
let batch = unsafe { list.pop_batch(2) };
assert_eq!(batch.len(), 2);
assert!(!batch.is_empty());
assert_eq!(list.len(), 2);
let mut receiver = FreeList::new();
unsafe {
receiver.push_batch(batch);
}
unsafe {
assert_eq!(receiver.pop_block(), Some(ptrs[3]));
assert_eq!(receiver.pop_block(), Some(ptrs[2]));
assert_eq!(receiver.pop_block(), None);
assert_eq!(list.pop_block(), Some(ptrs[1]));
assert_eq!(list.pop_block(), Some(ptrs[0]));
assert_eq!(list.pop_block(), None);
}
}
#[test]
fn push_batch_preserves_batch_order_ahead_of_existing_nodes() {
let mut source = FreeList::new();
let mut destination = FreeList::new();
let mut blocks = [
TestBlock::new(),
TestBlock::new(),
TestBlock::new(),
TestBlock::new(),
TestBlock::new(),
];
let ptrs = blocks.each_mut().map(TestBlock::as_ptr);
unsafe {
source.push_block(ptrs[0]);
source.push_block(ptrs[1]);
source.push_block(ptrs[2]);
destination.push_block(ptrs[3]);
destination.push_block(ptrs[4]);
}
let batch = unsafe { source.pop_batch(3) };
unsafe {
destination.push_batch(batch);
}
assert_eq!(source.len(), 0);
assert_eq!(destination.len(), 5);
unsafe {
assert_eq!(destination.pop_block(), Some(ptrs[2]));
assert_eq!(destination.pop_block(), Some(ptrs[1]));
assert_eq!(destination.pop_block(), Some(ptrs[0]));
assert_eq!(destination.pop_block(), Some(ptrs[4]));
assert_eq!(destination.pop_block(), Some(ptrs[3]));
assert_eq!(destination.pop_block(), None);
}
}
#[test]
fn zero_length_and_empty_batches_are_noops() {
let mut list = FreeList::new();
let mut block = TestBlock::new();
let ptr = block.as_ptr();
unsafe {
list.push_block(ptr);
}
let batch = unsafe { list.pop_batch(0) };
assert!(batch.is_empty());
assert_eq!(list.len(), 1);
unsafe {
list.push_batch(Batch::empty());
}
assert_eq!(list.len(), 1);
unsafe {
assert_eq!(list.pop_block(), Some(ptr));
assert_eq!(list.pop_block(), None);
}
}
}