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}