arena_lib/bump.rs
1//! Bump arena for short-lived scratch allocations.
2//!
3//! [`Bump`] is a linear allocator backed by a linked list of chunks.
4//! Allocations are O(1) (bump a pointer, write the value); the entire
5//! arena clears in O(1) via [`Bump::reset`]. Multiple references handed
6//! out from a shared `&self` borrow coexist safely because each allocation
7//! hands out a disjoint slice of the chunk.
8//!
9//! When a chunk is full the arena allocates a new chunk and continues —
10//! [`Bump::alloc`] is effectively infallible. The original chunk is
11//! retained so subsequent allocations after a [`Bump::reset`] refill the
12//! existing memory before any new chunk is requested from the global
13//! allocator.
14//!
15//! # Cost summary
16//!
17//! - `alloc` / `try_alloc`: O(1) (pointer bump + write; amortised over
18//! chunk growth).
19//! - `reset`: O(1) (resets the chunk cursor; does **not** drop values).
20//! - `allocated_bytes` / `chunk_capacity` / `chunk_count`: O(n) in the
21//! number of chunks (typically tiny).
22//!
23//! # Drop behavior
24//!
25//! `Bump` does not run destructors when reset or dropped. Allocate types
26//! that do not require `Drop` (anything that is `Copy`, or owns only
27//! arena-internal memory). For payloads that own resources (heap
28//! allocations, file handles, etc.) use [`DropArena`](crate::drop_arena::DropArena)
29//! instead — it has the same alloc-from-`&self` ergonomics but runs
30//! destructors on drop.
31
32use alloc::boxed::Box;
33use alloc::vec;
34use alloc::vec::Vec;
35use core::alloc::Layout;
36use core::cell::UnsafeCell;
37use core::ptr::NonNull;
38
39use crate::error::{Error, Result};
40
41/// Default minimum chunk size, used when [`Bump::new`] is called or when
42/// the user-supplied capacity is smaller than this value.
43const DEFAULT_CHUNK_SIZE: usize = 4096;
44
45/// Multi-chunk bump arena. See the [module-level docs](self).
46///
47/// # Examples
48///
49/// ```
50/// use arena_lib::Bump;
51///
52/// let mut bump = Bump::with_capacity(64);
53/// let a = bump.alloc(7_u32);
54/// let b = bump.alloc(42_u32);
55/// assert_eq!(*a, 7);
56/// assert_eq!(*b, 42);
57///
58/// // Resetting clears the cursor; chunks are retained for reuse.
59/// bump.reset();
60/// assert_eq!(bump.allocated_bytes(), 0);
61/// assert!(bump.chunk_capacity() >= 64);
62/// ```
63pub struct Bump {
64 state: UnsafeCell<State>,
65 next_chunk_size: usize,
66}
67
68struct State {
69 chunks: Vec<Box<[u8]>>,
70 current_chunk: usize,
71 offset: usize,
72}
73
74impl Bump {
75 /// Creates an empty bump arena. The first allocation triggers the
76 /// allocation of an initial chunk (default size 4 KiB).
77 #[inline]
78 #[must_use]
79 pub fn new() -> Self {
80 Self {
81 state: UnsafeCell::new(State {
82 chunks: Vec::new(),
83 current_chunk: 0,
84 offset: 0,
85 }),
86 next_chunk_size: DEFAULT_CHUNK_SIZE,
87 }
88 }
89
90 /// Creates a bump arena that pre-allocates an initial chunk of
91 /// `capacity` bytes.
92 ///
93 /// Subsequent chunks (if needed) are sized to at least `capacity`
94 /// bytes, with a floor of 4 KiB.
95 #[inline]
96 #[must_use]
97 pub fn with_capacity(capacity: usize) -> Self {
98 let chunk_size = core::cmp::max(capacity, 1);
99 let buf: Vec<u8> = vec![0; chunk_size];
100 Self {
101 state: UnsafeCell::new(State {
102 chunks: alloc::vec![buf.into_boxed_slice()],
103 current_chunk: 0,
104 offset: 0,
105 }),
106 next_chunk_size: core::cmp::max(capacity, DEFAULT_CHUNK_SIZE),
107 }
108 }
109
110 /// Total bytes reserved across every chunk currently held by the arena.
111 #[inline]
112 #[must_use]
113 pub fn chunk_capacity(&self) -> usize {
114 // SAFETY: `&self` read of `state` is sound — the only writers are
115 // allocation paths (advancing `offset` / pushing a chunk through
116 // `&self`) and `reset` (which takes `&mut self`, excluding any
117 // concurrent `&self` access). `Bump` is not `Sync`, so cross-thread
118 // `&self` access is rejected at compile time.
119 let state = unsafe { &*self.state.get() };
120 state.chunks.iter().map(|c| c.len()).sum()
121 }
122
123 /// Number of chunks currently held.
124 ///
125 /// Starts at 0 for a [`Bump::new`] arena, and grows by one each time
126 /// an allocation forces a new chunk to be requested from the global
127 /// allocator. [`Bump::reset`] does not reduce this count.
128 #[inline]
129 #[must_use]
130 pub fn chunk_count(&self) -> usize {
131 // SAFETY: see `chunk_capacity`.
132 let state = unsafe { &*self.state.get() };
133 state.chunks.len()
134 }
135
136 /// Bytes consumed since the most recent [`Bump::reset`] (or since
137 /// construction).
138 ///
139 /// Counts the fully-used capacity of every chunk before
140 /// `current_chunk` plus the cursor within `current_chunk`. Alignment
141 /// padding is included.
142 #[inline]
143 #[must_use]
144 pub fn allocated_bytes(&self) -> usize {
145 // SAFETY: see `chunk_capacity`.
146 let state = unsafe { &*self.state.get() };
147 if state.chunks.is_empty() {
148 return 0;
149 }
150 let mut total = 0;
151 for i in 0..state.current_chunk {
152 if let Some(c) = state.chunks.get(i) {
153 total += c.len();
154 }
155 }
156 total + state.offset
157 }
158
159 /// Allocates `value` and returns a unique reference to it.
160 ///
161 /// Allocates a new chunk if the current chunk does not have enough
162 /// space. Panics only if the global allocator itself fails (same
163 /// failure model as `Vec::push` / `Box::new`).
164 ///
165 /// # Examples
166 ///
167 /// ```
168 /// use arena_lib::Bump;
169 ///
170 /// let bump = Bump::with_capacity(16);
171 /// let n = bump.alloc(123_u32);
172 /// assert_eq!(*n, 123);
173 /// ```
174 #[inline]
175 #[allow(
176 clippy::mut_from_ref,
177 reason = "interior mutability via UnsafeCell; each call returns a disjoint region of the chunk"
178 )]
179 pub fn alloc<T>(&self, value: T) -> &mut T {
180 match self.try_alloc(value) {
181 Ok(reference) => reference,
182 Err(_) => panic!("bump arena failed to grow (global allocator out of memory)"),
183 }
184 }
185
186 /// Allocates `value`, returning `Ok(&mut T)` on success or
187 /// [`Error::CapacityExceeded`] only if a new chunk could not be
188 /// allocated (effectively never, on systems with a working global
189 /// allocator).
190 ///
191 /// The returned `&mut T` borrows from `&self` because the [`Bump`]
192 /// owns the underlying chunks and hands out non-overlapping regions
193 /// per call — the same pattern used by `bumpalo::Bump::alloc`.
194 ///
195 /// # Examples
196 ///
197 /// ```
198 /// use arena_lib::Bump;
199 ///
200 /// let bump = Bump::with_capacity(16);
201 /// let r = bump.try_alloc(0xfeed_u32).expect("global allocator should be live");
202 /// assert_eq!(*r, 0xfeed);
203 /// ```
204 #[allow(
205 clippy::mut_from_ref,
206 reason = "interior mutability via UnsafeCell; each call returns a disjoint region of the chunk"
207 )]
208 pub fn try_alloc<T>(&self, value: T) -> Result<&mut T> {
209 let layout = Layout::new::<T>();
210 let raw = self.try_alloc_layout(layout)?;
211 let typed = raw.cast::<T>();
212 // SAFETY: `typed` is non-null, aligned for `T`, and points into a
213 // freshly reserved disjoint region of a chunk. Writing `value`
214 // initialises the slot before any read.
215 unsafe { core::ptr::write(typed.as_ptr(), value) };
216 // SAFETY: lifetime is tied to `&self`. Chunks are `Box<[u8]>`
217 // values owned by `self`; their addresses are stable for the
218 // lifetime of `&self`. `try_alloc_layout` guarantees no future
219 // allocation reuses this region until `reset` is called (which
220 // requires `&mut self`, invalidating all outstanding `&self`
221 // borrows including this returned reference).
222 Ok(unsafe { &mut *typed.as_ptr() })
223 }
224
225 /// Resets the arena, marking every prior allocation as discarded.
226 ///
227 /// Every chunk is retained — subsequent allocations refill chunk 0
228 /// first, then chunk 1, etc., before any new chunk is requested from
229 /// the global allocator. Destructors are **not** run (see the
230 /// [module-level docs](self) for the Drop policy). Taking `&mut self`
231 /// ensures no outstanding references survive across the call.
232 ///
233 /// # Examples
234 ///
235 /// ```
236 /// use arena_lib::Bump;
237 ///
238 /// let mut bump = Bump::with_capacity(64);
239 /// let _ = bump.alloc([0_u8; 32]);
240 /// let chunks_before = bump.chunk_count();
241 ///
242 /// bump.reset();
243 /// assert_eq!(bump.allocated_bytes(), 0);
244 /// assert_eq!(bump.chunk_count(), chunks_before, "reset retains chunks");
245 /// ```
246 #[inline]
247 pub fn reset(&mut self) {
248 // SAFETY: `&mut self` excludes any outstanding `&T` / `&mut T`
249 // produced by `try_alloc`, so resetting the cursor cannot dangle
250 // any live reference.
251 let state = unsafe { &mut *self.state.get() };
252 state.current_chunk = 0;
253 state.offset = 0;
254 }
255
256 fn try_alloc_layout(&self, layout: Layout) -> Result<NonNull<u8>> {
257 // SAFETY: see `chunk_capacity` — `&self` access to `state` is
258 // sound because `Bump` is not `Sync` and the only `&mut self`
259 // operation (`reset`) excludes concurrent `&self` callers.
260 let state = unsafe { &mut *self.state.get() };
261
262 // Try the current chunk first.
263 if let Some(ptr) = try_in_chunk(state, layout) {
264 return Ok(ptr);
265 }
266
267 // Walk forward through any retained chunks (typical after reset).
268 while state.current_chunk + 1 < state.chunks.len() {
269 state.current_chunk += 1;
270 state.offset = 0;
271 if let Some(ptr) = try_in_chunk(state, layout) {
272 return Ok(ptr);
273 }
274 }
275
276 // No retained chunk fits — allocate a new one.
277 let min_for_layout = layout
278 .size()
279 .checked_add(layout.align())
280 .ok_or(Error::CapacityExceeded)?;
281 let new_size = core::cmp::max(self.next_chunk_size, min_for_layout);
282 let buf: Vec<u8> = vec![0; new_size];
283 state.chunks.push(buf.into_boxed_slice());
284 state.current_chunk = state.chunks.len().saturating_sub(1);
285 state.offset = 0;
286
287 try_in_chunk(state, layout).ok_or(Error::CapacityExceeded)
288 }
289}
290
291impl Default for Bump {
292 #[inline]
293 fn default() -> Self {
294 Self::new()
295 }
296}
297
298impl core::fmt::Debug for Bump {
299 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
300 f.debug_struct("Bump")
301 .field("allocated_bytes", &self.allocated_bytes())
302 .field("chunk_capacity", &self.chunk_capacity())
303 .field("chunk_count", &self.chunk_count())
304 .finish()
305 }
306}
307
308fn try_in_chunk(state: &mut State, layout: Layout) -> Option<NonNull<u8>> {
309 let chunk = state.chunks.get(state.current_chunk)?;
310 let base = chunk.as_ptr() as usize;
311 let cursor = base.checked_add(state.offset)?;
312 let align_mask = layout.align().wrapping_sub(1);
313 let aligned = cursor.checked_add(align_mask).map(|a| a & !align_mask)?;
314 let end = aligned.checked_add(layout.size())?;
315 let limit = base.saturating_add(chunk.len());
316 if end > limit {
317 return None;
318 }
319 state.offset = end - base;
320
321 if layout.size() == 0 {
322 return Some(dangling_aligned(layout.align()));
323 }
324
325 NonNull::new(aligned as *mut u8)
326}
327
328fn dangling_aligned(align: usize) -> NonNull<u8> {
329 // SAFETY: `align` is a non-zero power of two (invariant of `Layout`),
330 // so casting it to a pointer yields a non-null, properly aligned
331 // dangling pointer suitable for zero-sized accesses only.
332 unsafe { NonNull::new_unchecked(align as *mut u8) }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn alloc_returns_unique_reference() {
341 let bump = Bump::with_capacity(32);
342 let a = bump.alloc(1_u32);
343 let b = bump.alloc(2_u32);
344 assert_eq!(*a, 1);
345 assert_eq!(*b, 2);
346 assert!(bump.allocated_bytes() >= 8);
347 }
348
349 #[test]
350 fn alloc_grows_to_new_chunk_when_current_is_full() {
351 let bump = Bump::with_capacity(8);
352 // Fill the initial chunk.
353 let _ = bump.alloc([0_u8; 8]);
354 let chunks_before = bump.chunk_count();
355 // This allocation forces a new chunk.
356 let n = bump.alloc(0xfeed_u32);
357 let chunks_after = bump.chunk_count();
358 assert_eq!(*n, 0xfeed);
359 assert!(
360 chunks_after > chunks_before,
361 "should have grown to a new chunk"
362 );
363 assert!(bump.chunk_capacity() > 8);
364 }
365
366 #[test]
367 fn try_alloc_succeeds_via_growth() {
368 let bump = Bump::with_capacity(4);
369 let _ = bump.alloc(1_u32); // fills chunk 0
370 // try_alloc would have failed in 0.2; with multi-chunk it grows.
371 assert!(bump.try_alloc(2_u32).is_ok());
372 }
373
374 #[test]
375 fn reset_clears_cursor_and_retains_chunks() {
376 let mut bump = Bump::with_capacity(8);
377 let _ = bump.alloc([0_u8; 8]);
378 let _ = bump.alloc(42_u64); // grows
379 let chunks_before_reset = bump.chunk_count();
380 assert!(bump.allocated_bytes() > 0);
381
382 bump.reset();
383 assert_eq!(bump.allocated_bytes(), 0);
384 assert_eq!(
385 bump.chunk_count(),
386 chunks_before_reset,
387 "reset must retain chunks for reuse"
388 );
389
390 // New allocations refill existing chunks rather than allocating fresh.
391 let _ = bump.alloc(7_u64);
392 assert_eq!(bump.chunk_count(), chunks_before_reset);
393 }
394
395 #[test]
396 fn alignment_is_respected_across_chunks() {
397 let bump = Bump::with_capacity(64);
398 let _ = bump.alloc(1_u8);
399 let aligned = bump.alloc(0xdead_beef_u32);
400 let addr = aligned as *const u32 as usize;
401 assert_eq!(addr % core::mem::align_of::<u32>(), 0);
402
403 // Force a chunk transition and verify alignment again.
404 let _ = bump.alloc([0_u8; 200]);
405 let aligned2 = bump.alloc(0xcafe_u64);
406 let addr2 = aligned2 as *const u64 as usize;
407 assert_eq!(addr2 % core::mem::align_of::<u64>(), 0);
408 }
409
410 #[test]
411 fn zst_alloc_does_not_advance_offset() {
412 let bump = Bump::with_capacity(8);
413 let before = bump.allocated_bytes();
414 let _: &mut () = bump.alloc(());
415 assert_eq!(bump.allocated_bytes(), before);
416 }
417
418 #[test]
419 fn empty_bump_grows_on_first_real_alloc() {
420 let bump = Bump::new();
421 assert_eq!(bump.chunk_capacity(), 0);
422 let n = bump.alloc(99_u32);
423 assert_eq!(*n, 99);
424 assert!(bump.chunk_capacity() >= 4096);
425 }
426
427 #[test]
428 fn very_large_single_alloc_triggers_oversized_chunk() {
429 let bump = Bump::with_capacity(16);
430 // Force a chunk larger than the default.
431 let big: &mut [u8; 8192] = bump.alloc([0_u8; 8192]);
432 assert_eq!(big.len(), 8192);
433 assert!(bump.chunk_capacity() >= 8192);
434 }
435}