use alloc::boxed::Box;
use alloc::vec;
use alloc::vec::Vec;
use core::alloc::Layout;
use core::cell::UnsafeCell;
use core::ptr::NonNull;
use crate::error::{Error, Result};
const DEFAULT_CHUNK_SIZE: usize = 4096;
pub struct Bump {
state: UnsafeCell<State>,
next_chunk_size: usize,
}
struct State {
chunks: Vec<Box<[u8]>>,
current_chunk: usize,
offset: usize,
}
impl Bump {
#[inline]
#[must_use]
pub fn new() -> Self {
Self {
state: UnsafeCell::new(State {
chunks: Vec::new(),
current_chunk: 0,
offset: 0,
}),
next_chunk_size: DEFAULT_CHUNK_SIZE,
}
}
#[inline]
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
let chunk_size = core::cmp::max(capacity, 1);
let buf: Vec<u8> = vec![0; chunk_size];
Self {
state: UnsafeCell::new(State {
chunks: alloc::vec![buf.into_boxed_slice()],
current_chunk: 0,
offset: 0,
}),
next_chunk_size: core::cmp::max(capacity, DEFAULT_CHUNK_SIZE),
}
}
#[inline]
#[must_use]
pub fn chunk_capacity(&self) -> usize {
let state = unsafe { &*self.state.get() };
state.chunks.iter().map(|c| c.len()).sum()
}
#[inline]
#[must_use]
pub fn chunk_count(&self) -> usize {
let state = unsafe { &*self.state.get() };
state.chunks.len()
}
#[inline]
#[must_use]
pub fn allocated_bytes(&self) -> usize {
let state = unsafe { &*self.state.get() };
if state.chunks.is_empty() {
return 0;
}
let mut total = 0;
for i in 0..state.current_chunk {
if let Some(c) = state.chunks.get(i) {
total += c.len();
}
}
total + state.offset
}
#[inline]
#[allow(
clippy::mut_from_ref,
reason = "interior mutability via UnsafeCell; each call returns a disjoint region of the chunk"
)]
pub fn alloc<T>(&self, value: T) -> &mut T {
match self.try_alloc(value) {
Ok(reference) => reference,
Err(_) => panic!("bump arena failed to grow (global allocator out of memory)"),
}
}
#[allow(
clippy::mut_from_ref,
reason = "interior mutability via UnsafeCell; each call returns a disjoint region of the chunk"
)]
pub fn try_alloc<T>(&self, value: T) -> Result<&mut T> {
let layout = Layout::new::<T>();
let raw = self.try_alloc_layout(layout)?;
let typed = raw.cast::<T>();
unsafe { core::ptr::write(typed.as_ptr(), value) };
Ok(unsafe { &mut *typed.as_ptr() })
}
#[inline]
pub fn reset(&mut self) {
let state = unsafe { &mut *self.state.get() };
state.current_chunk = 0;
state.offset = 0;
}
fn try_alloc_layout(&self, layout: Layout) -> Result<NonNull<u8>> {
let state = unsafe { &mut *self.state.get() };
if let Some(ptr) = try_in_chunk(state, layout) {
return Ok(ptr);
}
while state.current_chunk + 1 < state.chunks.len() {
state.current_chunk += 1;
state.offset = 0;
if let Some(ptr) = try_in_chunk(state, layout) {
return Ok(ptr);
}
}
let min_for_layout = layout
.size()
.checked_add(layout.align())
.ok_or(Error::CapacityExceeded)?;
let new_size = core::cmp::max(self.next_chunk_size, min_for_layout);
let buf: Vec<u8> = vec![0; new_size];
state.chunks.push(buf.into_boxed_slice());
state.current_chunk = state.chunks.len().saturating_sub(1);
state.offset = 0;
try_in_chunk(state, layout).ok_or(Error::CapacityExceeded)
}
}
impl Default for Bump {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl core::fmt::Debug for Bump {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Bump")
.field("allocated_bytes", &self.allocated_bytes())
.field("chunk_capacity", &self.chunk_capacity())
.field("chunk_count", &self.chunk_count())
.finish()
}
}
fn try_in_chunk(state: &mut State, layout: Layout) -> Option<NonNull<u8>> {
let chunk = state.chunks.get(state.current_chunk)?;
let base = chunk.as_ptr() as usize;
let cursor = base.checked_add(state.offset)?;
let align_mask = layout.align().wrapping_sub(1);
let aligned = cursor.checked_add(align_mask).map(|a| a & !align_mask)?;
let end = aligned.checked_add(layout.size())?;
let limit = base.saturating_add(chunk.len());
if end > limit {
return None;
}
state.offset = end - base;
if layout.size() == 0 {
return Some(dangling_aligned(layout.align()));
}
NonNull::new(aligned as *mut u8)
}
fn dangling_aligned(align: usize) -> NonNull<u8> {
unsafe { NonNull::new_unchecked(align as *mut u8) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alloc_returns_unique_reference() {
let bump = Bump::with_capacity(32);
let a = bump.alloc(1_u32);
let b = bump.alloc(2_u32);
assert_eq!(*a, 1);
assert_eq!(*b, 2);
assert!(bump.allocated_bytes() >= 8);
}
#[test]
fn alloc_grows_to_new_chunk_when_current_is_full() {
let bump = Bump::with_capacity(8);
let _ = bump.alloc([0_u8; 8]);
let chunks_before = bump.chunk_count();
let n = bump.alloc(0xfeed_u32);
let chunks_after = bump.chunk_count();
assert_eq!(*n, 0xfeed);
assert!(
chunks_after > chunks_before,
"should have grown to a new chunk"
);
assert!(bump.chunk_capacity() > 8);
}
#[test]
fn try_alloc_succeeds_via_growth() {
let bump = Bump::with_capacity(4);
let _ = bump.alloc(1_u32); assert!(bump.try_alloc(2_u32).is_ok());
}
#[test]
fn reset_clears_cursor_and_retains_chunks() {
let mut bump = Bump::with_capacity(8);
let _ = bump.alloc([0_u8; 8]);
let _ = bump.alloc(42_u64); let chunks_before_reset = bump.chunk_count();
assert!(bump.allocated_bytes() > 0);
bump.reset();
assert_eq!(bump.allocated_bytes(), 0);
assert_eq!(
bump.chunk_count(),
chunks_before_reset,
"reset must retain chunks for reuse"
);
let _ = bump.alloc(7_u64);
assert_eq!(bump.chunk_count(), chunks_before_reset);
}
#[test]
fn alignment_is_respected_across_chunks() {
let bump = Bump::with_capacity(64);
let _ = bump.alloc(1_u8);
let aligned = bump.alloc(0xdead_beef_u32);
let addr = aligned as *const u32 as usize;
assert_eq!(addr % core::mem::align_of::<u32>(), 0);
let _ = bump.alloc([0_u8; 200]);
let aligned2 = bump.alloc(0xcafe_u64);
let addr2 = aligned2 as *const u64 as usize;
assert_eq!(addr2 % core::mem::align_of::<u64>(), 0);
}
#[test]
fn zst_alloc_does_not_advance_offset() {
let bump = Bump::with_capacity(8);
let before = bump.allocated_bytes();
let _: &mut () = bump.alloc(());
assert_eq!(bump.allocated_bytes(), before);
}
#[test]
fn empty_bump_grows_on_first_real_alloc() {
let bump = Bump::new();
assert_eq!(bump.chunk_capacity(), 0);
let n = bump.alloc(99_u32);
assert_eq!(*n, 99);
assert!(bump.chunk_capacity() >= 4096);
}
#[test]
fn very_large_single_alloc_triggers_oversized_chunk() {
let bump = Bump::with_capacity(16);
let big: &mut [u8; 8192] = bump.alloc([0_u8; 8192]);
assert_eq!(big.len(), 8192);
assert!(bump.chunk_capacity() >= 8192);
}
}