Skip to main content

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}