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 #[allow(
195 clippy::mut_from_ref,
196 reason = "interior mutability via UnsafeCell; each call returns a disjoint region of the chunk"
197 )]
198 pub fn try_alloc<T>(&self, value: T) -> Result<&mut T> {
199 let layout = Layout::new::<T>();
200 let raw = self.try_alloc_layout(layout)?;
201 let typed = raw.cast::<T>();
202 // SAFETY: `typed` is non-null, aligned for `T`, and points into a
203 // freshly reserved disjoint region of a chunk. Writing `value`
204 // initialises the slot before any read.
205 unsafe { core::ptr::write(typed.as_ptr(), value) };
206 // SAFETY: lifetime is tied to `&self`. Chunks are `Box<[u8]>`
207 // values owned by `self`; their addresses are stable for the
208 // lifetime of `&self`. `try_alloc_layout` guarantees no future
209 // allocation reuses this region until `reset` is called (which
210 // requires `&mut self`, invalidating all outstanding `&self`
211 // borrows including this returned reference).
212 Ok(unsafe { &mut *typed.as_ptr() })
213 }
214
215 /// Resets the arena, marking every prior allocation as discarded.
216 ///
217 /// Every chunk is retained — subsequent allocations refill chunk 0
218 /// first, then chunk 1, etc., before any new chunk is requested from
219 /// the global allocator. Destructors are **not** run (see the
220 /// [module-level docs](self) for the Drop policy). Taking `&mut self`
221 /// ensures no outstanding references survive across the call.
222 #[inline]
223 pub fn reset(&mut self) {
224 // SAFETY: `&mut self` excludes any outstanding `&T` / `&mut T`
225 // produced by `try_alloc`, so resetting the cursor cannot dangle
226 // any live reference.
227 let state = unsafe { &mut *self.state.get() };
228 state.current_chunk = 0;
229 state.offset = 0;
230 }
231
232 fn try_alloc_layout(&self, layout: Layout) -> Result<NonNull<u8>> {
233 // SAFETY: see `chunk_capacity` — `&self` access to `state` is
234 // sound because `Bump` is not `Sync` and the only `&mut self`
235 // operation (`reset`) excludes concurrent `&self` callers.
236 let state = unsafe { &mut *self.state.get() };
237
238 // Try the current chunk first.
239 if let Some(ptr) = try_in_chunk(state, layout) {
240 return Ok(ptr);
241 }
242
243 // Walk forward through any retained chunks (typical after reset).
244 while state.current_chunk + 1 < state.chunks.len() {
245 state.current_chunk += 1;
246 state.offset = 0;
247 if let Some(ptr) = try_in_chunk(state, layout) {
248 return Ok(ptr);
249 }
250 }
251
252 // No retained chunk fits — allocate a new one.
253 let min_for_layout = layout
254 .size()
255 .checked_add(layout.align())
256 .ok_or(Error::CapacityExceeded)?;
257 let new_size = core::cmp::max(self.next_chunk_size, min_for_layout);
258 let buf: Vec<u8> = vec![0; new_size];
259 state.chunks.push(buf.into_boxed_slice());
260 state.current_chunk = state.chunks.len().saturating_sub(1);
261 state.offset = 0;
262
263 try_in_chunk(state, layout).ok_or(Error::CapacityExceeded)
264 }
265}
266
267impl Default for Bump {
268 #[inline]
269 fn default() -> Self {
270 Self::new()
271 }
272}
273
274impl core::fmt::Debug for Bump {
275 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
276 f.debug_struct("Bump")
277 .field("allocated_bytes", &self.allocated_bytes())
278 .field("chunk_capacity", &self.chunk_capacity())
279 .field("chunk_count", &self.chunk_count())
280 .finish()
281 }
282}
283
284fn try_in_chunk(state: &mut State, layout: Layout) -> Option<NonNull<u8>> {
285 let chunk = state.chunks.get(state.current_chunk)?;
286 let base = chunk.as_ptr() as usize;
287 let cursor = base.checked_add(state.offset)?;
288 let align_mask = layout.align().wrapping_sub(1);
289 let aligned = cursor.checked_add(align_mask).map(|a| a & !align_mask)?;
290 let end = aligned.checked_add(layout.size())?;
291 let limit = base.saturating_add(chunk.len());
292 if end > limit {
293 return None;
294 }
295 state.offset = end - base;
296
297 if layout.size() == 0 {
298 return Some(dangling_aligned(layout.align()));
299 }
300
301 NonNull::new(aligned as *mut u8)
302}
303
304fn dangling_aligned(align: usize) -> NonNull<u8> {
305 // SAFETY: `align` is a non-zero power of two (invariant of `Layout`),
306 // so casting it to a pointer yields a non-null, properly aligned
307 // dangling pointer suitable for zero-sized accesses only.
308 unsafe { NonNull::new_unchecked(align as *mut u8) }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn alloc_returns_unique_reference() {
317 let bump = Bump::with_capacity(32);
318 let a = bump.alloc(1_u32);
319 let b = bump.alloc(2_u32);
320 assert_eq!(*a, 1);
321 assert_eq!(*b, 2);
322 assert!(bump.allocated_bytes() >= 8);
323 }
324
325 #[test]
326 fn alloc_grows_to_new_chunk_when_current_is_full() {
327 let bump = Bump::with_capacity(8);
328 // Fill the initial chunk.
329 let _ = bump.alloc([0_u8; 8]);
330 let chunks_before = bump.chunk_count();
331 // This allocation forces a new chunk.
332 let n = bump.alloc(0xfeed_u32);
333 let chunks_after = bump.chunk_count();
334 assert_eq!(*n, 0xfeed);
335 assert!(
336 chunks_after > chunks_before,
337 "should have grown to a new chunk"
338 );
339 assert!(bump.chunk_capacity() > 8);
340 }
341
342 #[test]
343 fn try_alloc_succeeds_via_growth() {
344 let bump = Bump::with_capacity(4);
345 let _ = bump.alloc(1_u32); // fills chunk 0
346 // try_alloc would have failed in 0.2; with multi-chunk it grows.
347 assert!(bump.try_alloc(2_u32).is_ok());
348 }
349
350 #[test]
351 fn reset_clears_cursor_and_retains_chunks() {
352 let mut bump = Bump::with_capacity(8);
353 let _ = bump.alloc([0_u8; 8]);
354 let _ = bump.alloc(42_u64); // grows
355 let chunks_before_reset = bump.chunk_count();
356 assert!(bump.allocated_bytes() > 0);
357
358 bump.reset();
359 assert_eq!(bump.allocated_bytes(), 0);
360 assert_eq!(
361 bump.chunk_count(),
362 chunks_before_reset,
363 "reset must retain chunks for reuse"
364 );
365
366 // New allocations refill existing chunks rather than allocating fresh.
367 let _ = bump.alloc(7_u64);
368 assert_eq!(bump.chunk_count(), chunks_before_reset);
369 }
370
371 #[test]
372 fn alignment_is_respected_across_chunks() {
373 let bump = Bump::with_capacity(64);
374 let _ = bump.alloc(1_u8);
375 let aligned = bump.alloc(0xdead_beef_u32);
376 let addr = aligned as *const u32 as usize;
377 assert_eq!(addr % core::mem::align_of::<u32>(), 0);
378
379 // Force a chunk transition and verify alignment again.
380 let _ = bump.alloc([0_u8; 200]);
381 let aligned2 = bump.alloc(0xcafe_u64);
382 let addr2 = aligned2 as *const u64 as usize;
383 assert_eq!(addr2 % core::mem::align_of::<u64>(), 0);
384 }
385
386 #[test]
387 fn zst_alloc_does_not_advance_offset() {
388 let bump = Bump::with_capacity(8);
389 let before = bump.allocated_bytes();
390 let _: &mut () = bump.alloc(());
391 assert_eq!(bump.allocated_bytes(), before);
392 }
393
394 #[test]
395 fn empty_bump_grows_on_first_real_alloc() {
396 let bump = Bump::new();
397 assert_eq!(bump.chunk_capacity(), 0);
398 let n = bump.alloc(99_u32);
399 assert_eq!(*n, 99);
400 assert!(bump.chunk_capacity() >= 4096);
401 }
402
403 #[test]
404 fn very_large_single_alloc_triggers_oversized_chunk() {
405 let bump = Bump::with_capacity(16);
406 // Force a chunk larger than the default.
407 let big: &mut [u8; 8192] = bump.alloc([0_u8; 8192]);
408 assert_eq!(big.len(), 8192);
409 assert!(bump.chunk_capacity() >= 8192);
410 }
411}