Skip to main content

arena_lib/
bump.rs

1//! Bump arena for short-lived scratch allocations.
2//!
3//! [`Bump`] is a single-chunk linear allocator. Allocations are O(1) (bump a
4//! pointer, write the value), and the entire arena clears in O(1) via
5//! [`Bump::reset`]. Multiple references handed out from a shared `&self`
6//! borrow coexist safely because each allocation hands out a disjoint slice
7//! of the chunk.
8//!
9//! # Cost summary
10//!
11//! - `alloc` / `try_alloc`: O(1) (pointer bump + write).
12//! - `reset`: O(1) (resets the offset; does **not** drop values).
13//! - `allocated_bytes` / `chunk_capacity`: O(1).
14//!
15//! # Drop behavior
16//!
17//! `Bump` does not run destructors when reset or dropped. Allocate types
18//! that do not require `Drop` (anything that is `Copy`, or owns only
19//! arena-internal memory). The 0.5 milestone introduces a typed drop-arena
20//! variant for cases where this restriction is too tight.
21//!
22//! # Growth policy (current and planned)
23//!
24//! 0.2 ships a **single-chunk** allocator: capacity is fixed at
25//! construction time and [`Bump::try_alloc`] returns
26//! [`Error::CapacityExceeded`] when full. The 0.5 milestone replaces the
27//! internal chunk with a linked list of chunks so that `alloc` becomes
28//! effectively infallible.
29
30use alloc::boxed::Box;
31use alloc::vec;
32use alloc::vec::Vec;
33use core::alloc::Layout;
34use core::cell::UnsafeCell;
35use core::ptr::NonNull;
36
37use crate::error::{Error, Result};
38
39/// Single-chunk bump arena. See the [module-level docs](self).
40///
41/// # Examples
42///
43/// ```
44/// use arena_lib::Bump;
45///
46/// let mut bump = Bump::with_capacity(64);
47/// let a = bump.alloc(7_u32);
48/// let b = bump.alloc(42_u32);
49/// assert_eq!(*a, 7);
50/// assert_eq!(*b, 42);
51///
52/// // Resetting clears the offset; capacity is retained.
53/// bump.reset();
54/// assert_eq!(bump.allocated_bytes(), 0);
55/// ```
56pub struct Bump {
57    chunk: Box<[u8]>,
58    state: UnsafeCell<State>,
59}
60
61struct State {
62    offset: usize,
63}
64
65impl Bump {
66    /// Creates an empty bump arena that can satisfy only zero-sized
67    /// allocations.
68    ///
69    /// Use [`Bump::with_capacity`] for any non-trivial use.
70    #[inline]
71    #[must_use]
72    pub fn new() -> Self {
73        Self {
74            chunk: Vec::<u8>::new().into_boxed_slice(),
75            state: UnsafeCell::new(State { offset: 0 }),
76        }
77    }
78
79    /// Creates a bump arena backed by a fixed-size chunk of `capacity` bytes.
80    ///
81    /// `capacity` is the upper bound on the *byte* footprint of every
82    /// allocation made against this arena before [`Bump::reset`] is called.
83    /// Account for alignment padding in addition to raw value size.
84    #[inline]
85    #[must_use]
86    pub fn with_capacity(capacity: usize) -> Self {
87        let buf: Vec<u8> = vec![0; capacity];
88        Self {
89            chunk: buf.into_boxed_slice(),
90            state: UnsafeCell::new(State { offset: 0 }),
91        }
92    }
93
94    /// Total capacity of the underlying chunk, in bytes.
95    #[inline]
96    #[must_use]
97    pub fn chunk_capacity(&self) -> usize {
98        self.chunk.len()
99    }
100
101    /// Bytes consumed since the most recent [`Bump::reset`] (or since
102    /// construction).
103    ///
104    /// Includes alignment padding.
105    #[inline]
106    #[must_use]
107    pub fn allocated_bytes(&self) -> usize {
108        // SAFETY: `&self` read of `state.offset` is sound because the only
109        // writers are `try_alloc_layout` (which advances the offset
110        // monotonically through `&self`) and `reset` (which requires
111        // `&mut self`, excluding any concurrent `&self` access). `Bump` is
112        // not `Sync`, so cross-thread `&self` access is rejected at
113        // compile time.
114        unsafe { (*self.state.get()).offset }
115    }
116
117    /// Allocates `value` and returns a unique reference to it.
118    ///
119    /// Panics if the chunk does not have room for `value` (after alignment
120    /// padding). Use [`Bump::try_alloc`] for an explicit fallible variant.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use arena_lib::Bump;
126    ///
127    /// let bump = Bump::with_capacity(16);
128    /// let n = bump.alloc(123_u32);
129    /// assert_eq!(*n, 123);
130    /// ```
131    #[inline]
132    #[allow(
133        clippy::mut_from_ref,
134        reason = "interior mutability via UnsafeCell; each call returns a disjoint region of the chunk"
135    )]
136    pub fn alloc<T>(&self, value: T) -> &mut T {
137        match self.try_alloc(value) {
138            Ok(reference) => reference,
139            Err(_) => panic!("bump arena exhausted; allocate with `with_capacity` accordingly"),
140        }
141    }
142
143    /// Allocates `value`, returning `Ok(&mut T)` on success or
144    /// [`Error::CapacityExceeded`] if the chunk is full.
145    ///
146    /// The returned `&mut T` borrows from `&self` because the [`Bump`]
147    /// owns the underlying chunk and hands out non-overlapping regions
148    /// per call — the same pattern used by `bumpalo::Bump::alloc`.
149    #[allow(
150        clippy::mut_from_ref,
151        reason = "interior mutability via UnsafeCell; each call returns a disjoint region of the chunk"
152    )]
153    pub fn try_alloc<T>(&self, value: T) -> Result<&mut T> {
154        let layout = Layout::new::<T>();
155        let raw = self.try_alloc_layout(layout)?;
156        let typed = raw.cast::<T>();
157        // SAFETY: `typed` is non-null, aligned for `T`, and points into a
158        // freshly reserved disjoint region of the chunk. Writing `value`
159        // initialises the slot before any read.
160        unsafe { core::ptr::write(typed.as_ptr(), value) };
161        // SAFETY: lifetime is tied to `&self`. The chunk is a `Box<[u8]>`
162        // owned by `self`, so its address is stable. `try_alloc_layout`
163        // guarantees no future allocation reuses this region until
164        // `reset` is called (which requires `&mut self`, invalidating all
165        // outstanding `&self` borrows including this returned reference).
166        Ok(unsafe { &mut *typed.as_ptr() })
167    }
168
169    /// Resets the arena, marking every prior allocation as discarded.
170    ///
171    /// Capacity is retained. Destructors of previously allocated values are
172    /// **not** run (see the [module-level docs](self) for the Drop policy).
173    /// Taking `&mut self` ensures no outstanding references survive across
174    /// the call.
175    #[inline]
176    pub fn reset(&mut self) {
177        // SAFETY: `&mut self` excludes any outstanding `&T` / `&mut T`
178        // produced by `try_alloc`, so resetting the offset cannot dangle
179        // any live reference.
180        unsafe { (*self.state.get()).offset = 0 };
181    }
182
183    fn try_alloc_layout(&self, layout: Layout) -> Result<NonNull<u8>> {
184        // SAFETY: see `allocated_bytes` — `&self` access to `state` is
185        // sound because `Bump` is not `Sync` and the only `&mut self`
186        // operation (`reset`) excludes concurrent `&self` callers.
187        let state = unsafe { &mut *self.state.get() };
188        let base = self.chunk.as_ptr() as usize;
189        let cursor = match base.checked_add(state.offset) {
190            Some(c) => c,
191            None => return Err(Error::CapacityExceeded),
192        };
193        let align_mask = layout.align().wrapping_sub(1);
194        let aligned = match cursor.checked_add(align_mask) {
195            Some(a) => a & !align_mask,
196            None => return Err(Error::CapacityExceeded),
197        };
198        let end = match aligned.checked_add(layout.size()) {
199            Some(e) => e,
200            None => return Err(Error::CapacityExceeded),
201        };
202        let limit = base.saturating_add(self.chunk.len());
203        if end > limit {
204            return Err(Error::CapacityExceeded);
205        }
206
207        state.offset = end - base;
208
209        if layout.size() == 0 {
210            return Ok(dangling_aligned(layout.align()));
211        }
212
213        // SAFETY: `aligned` is in-bounds for the chunk (verified by the
214        // `end > limit` check above) and properly aligned for `layout`.
215        // The pointer is non-null because the chunk has non-zero length
216        // (otherwise `end > limit` would have fired for any non-ZST).
217        let ptr = aligned as *mut u8;
218        NonNull::new(ptr).ok_or(Error::CapacityExceeded)
219    }
220}
221
222impl Default for Bump {
223    #[inline]
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229impl core::fmt::Debug for Bump {
230    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
231        f.debug_struct("Bump")
232            .field("allocated_bytes", &self.allocated_bytes())
233            .field("chunk_capacity", &self.chunk_capacity())
234            .finish()
235    }
236}
237
238fn dangling_aligned(align: usize) -> NonNull<u8> {
239    // SAFETY: `align` is a non-zero power of two (invariant of `Layout`),
240    // so casting it to a pointer yields a non-null, properly aligned
241    // dangling pointer suitable for zero-sized accesses only.
242    unsafe { NonNull::new_unchecked(align as *mut u8) }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn alloc_returns_unique_reference() {
251        let bump = Bump::with_capacity(32);
252        let a = bump.alloc(1_u32);
253        let b = bump.alloc(2_u32);
254        assert_eq!(*a, 1);
255        assert_eq!(*b, 2);
256        assert!(bump.allocated_bytes() >= 8);
257    }
258
259    #[test]
260    fn try_alloc_fails_when_full() {
261        let bump = Bump::with_capacity(4);
262        let _ = bump.alloc(1_u32);
263        assert!(bump.try_alloc(2_u32).is_err());
264    }
265
266    #[test]
267    fn reset_clears_offset() {
268        let mut bump = Bump::with_capacity(16);
269        let _ = bump.alloc(42_u64);
270        assert!(bump.allocated_bytes() > 0);
271        bump.reset();
272        assert_eq!(bump.allocated_bytes(), 0);
273        let _ = bump.alloc(7_u64);
274        assert!(bump.allocated_bytes() > 0);
275    }
276
277    #[test]
278    fn alignment_is_respected() {
279        let bump = Bump::with_capacity(64);
280        let _ = bump.alloc(1_u8);
281        let aligned = bump.alloc(0xdead_beef_u32);
282        let addr = aligned as *const u32 as usize;
283        assert_eq!(addr % core::mem::align_of::<u32>(), 0);
284    }
285
286    #[test]
287    fn zst_alloc_does_not_advance_offset() {
288        let bump = Bump::with_capacity(8);
289        let before = bump.allocated_bytes();
290        let _: &mut () = bump.alloc(());
291        assert_eq!(bump.allocated_bytes(), before);
292    }
293
294    #[test]
295    fn empty_bump_only_satisfies_zsts() {
296        let bump = Bump::new();
297        assert_eq!(bump.chunk_capacity(), 0);
298        let _: &mut () = bump.alloc(());
299        assert!(bump.try_alloc(1_u8).is_err());
300    }
301}