use core::fmt;
use core::marker::PhantomData;
use allocator_api2::alloc::{AllocError, Allocator, Global};
use crate::Arena;
use crate::internal::constants::{MAX_NORMAL_ALLOC, MIN_CHUNK_BYTES, SizeClass};
const MIN_MAX_NORMAL_ALLOC: usize = 4096;
pub struct ArenaBuilder<A: Allocator + Clone = Global> {
allocator: A,
max_normal_alloc: usize,
byte_budget: Option<usize>,
capacity_local: usize,
capacity_shared: usize,
_phantom: PhantomData<A>,
}
impl ArenaBuilder<Global> {
#[must_use]
#[inline]
pub fn new() -> Self {
Self::new_in(Global)
}
}
impl Default for ArenaBuilder<Global> {
fn default() -> Self {
Self::new()
}
}
impl<A: Allocator + Clone> ArenaBuilder<A> {
#[must_use]
#[inline]
pub fn new_in(allocator: A) -> Self {
Self {
allocator,
max_normal_alloc: MAX_NORMAL_ALLOC,
byte_budget: None,
capacity_local: 0,
capacity_shared: 0,
_phantom: PhantomData,
}
}
#[must_use]
#[inline]
pub const fn max_normal_alloc(mut self, bytes: usize) -> Self {
self.max_normal_alloc = bytes;
self
}
#[must_use]
#[inline]
pub const fn byte_budget(mut self, bytes: usize) -> Self {
self.byte_budget = Some(bytes);
self
}
#[must_use]
#[inline]
pub const fn with_capacity_local(mut self, bytes: usize) -> Self {
self.capacity_local = bytes;
self
}
#[must_use]
#[inline]
pub const fn with_capacity_shared(mut self, bytes: usize) -> Self {
self.capacity_shared = bytes;
self
}
#[must_use]
#[inline]
pub fn allocator_in<A2: Allocator + Clone>(self, allocator: A2) -> ArenaBuilder<A2> {
ArenaBuilder {
allocator,
max_normal_alloc: self.max_normal_alloc,
byte_budget: self.byte_budget,
capacity_local: self.capacity_local,
capacity_shared: self.capacity_shared,
_phantom: PhantomData,
}
}
#[cold]
fn validate(&self) {
let upper = crate::internal::local_chunk::max_bump_extent::<A>().min(crate::internal::shared_chunk::max_bump_extent::<A>());
assert!(
(MIN_MAX_NORMAL_ALLOC..=upper).contains(&self.max_normal_alloc),
"max_normal_alloc must be in [{MIN_MAX_NORMAL_ALLOC}, {upper}], got {}",
self.max_normal_alloc,
);
assert!(
self.capacity_local == 0 || self.capacity_local >= MIN_CHUNK_BYTES,
"with_capacity_local(bytes) must be either 0 or at least {MIN_CHUNK_BYTES}, got {}",
self.capacity_local,
);
assert!(
self.capacity_shared == 0 || self.capacity_shared >= MIN_CHUNK_BYTES,
"with_capacity_shared(bytes) must be either 0 or at least {MIN_CHUNK_BYTES}, got {}",
self.capacity_shared,
);
}
#[cfg_attr(test, mutants::skip)] fn resolve_capacity(capacity: usize) -> Option<(SizeClass, usize)> {
if capacity == 0 {
return None;
}
let target_class = SizeClass::min_for_bytes(capacity).min(SizeClass::MAX);
let class_total = target_class.bytes();
let count = capacity.div_ceil(class_total);
Some((target_class, count))
}
#[must_use]
#[cold]
pub fn build(self) -> Arena<A>
where
A: 'static,
{
match self.try_build() {
Ok(a) => a,
Err(_) => panic_build(),
}
}
#[cold]
pub fn try_build(self) -> Result<Arena<A>, AllocError>
where
A: 'static,
{
self.validate();
let local = Self::resolve_capacity(self.capacity_local);
let shared = Self::resolve_capacity(self.capacity_shared);
let arena = Arena::try_from_config(self.allocator, self.max_normal_alloc, self.byte_budget)?;
if let Some((class, n)) = local {
for _ in 0..n {
arena.preallocate_one_local(class)?;
}
}
if let Some((class, n)) = shared {
for _ in 0..n {
arena.preallocate_one_shared(class)?;
}
}
Ok(arena)
}
}
#[expect(
clippy::missing_fields_in_debug,
reason = "Allocator and PhantomData fields are not useful in debug output"
)]
impl<A: Allocator + Clone> fmt::Debug for ArenaBuilder<A> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ArenaBuilder")
.field("max_normal_alloc", &self.max_normal_alloc)
.field("byte_budget", &self.byte_budget)
.field("capacity_local", &self.capacity_local)
.field("capacity_shared", &self.capacity_shared)
.finish()
}
}
#[cold]
#[inline(never)]
#[expect(clippy::panic, reason = "panicking constructor matches Arena's `panic_alloc` style")]
fn panic_build() -> ! {
panic!("multitude::ArenaBuilder::build: backing allocator failed");
}