Skip to main content

arena_lib/
drop_arena.rs

1//! Typed arena that *does* run destructors.
2//!
3//! [`DropArena<T>`] is the drop-honouring sibling of [`Bump`](crate::bump::Bump).
4//! Both hand out `&mut T` from a shared `&self` borrow at O(1) cost, but
5//! `DropArena` parks every value inside a `Vec<T>` chunk so that `T`'s
6//! destructor runs when the arena (or the chunk holding the value) is
7//! dropped. Reach for `DropArena` when payloads own resources — boxed
8//! data, file handles, mutexes — and you cannot leak on reset.
9//!
10//! Like `Bump`, the arena is single-threaded: it is `Send` when `T: Send`
11//! but never `Sync`.
12//!
13//! # Cost summary
14//!
15//! - `alloc`: amortised O(1) (chunk grows by allocating a new chunk).
16//! - `len` / `is_empty` / `chunk_count`: O(chunk_count) (typically tiny).
17//!
18//! # Examples
19//!
20//! ```
21//! use arena_lib::DropArena;
22//!
23//! let arena = DropArena::<String>::new();
24//! let s1 = arena.alloc(String::from("alpha"));
25//! let s2 = arena.alloc(String::from("bravo"));
26//! assert_eq!(s1, "alpha");
27//! assert_eq!(s2, "bravo");
28//! assert_eq!(arena.len(), 2);
29//! // When `arena` is dropped, both Strings free their heap buffers.
30//! ```
31
32use alloc::vec::Vec;
33use core::cell::UnsafeCell;
34
35/// Default capacity (in `T` slots) of the first chunk and any
36/// subsequently-grown chunk.
37const DEFAULT_CHUNK_CAPACITY: usize = 16;
38
39/// Typed drop-honouring arena. See the [module-level docs](self).
40pub struct DropArena<T> {
41    chunks: UnsafeCell<Vec<Vec<T>>>,
42    chunk_capacity: usize,
43}
44
45impl<T> DropArena<T> {
46    /// Creates an empty arena. The first allocation triggers the
47    /// allocation of an initial chunk holding 16 `T` slots.
48    #[inline]
49    #[must_use]
50    pub fn new() -> Self {
51        Self {
52            chunks: UnsafeCell::new(Vec::new()),
53            chunk_capacity: DEFAULT_CHUNK_CAPACITY,
54        }
55    }
56
57    /// Creates an empty arena whose chunks each hold `chunk_capacity`
58    /// `T` slots.
59    ///
60    /// A `chunk_capacity` of zero is silently clamped to 1.
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use arena_lib::DropArena;
66    ///
67    /// // Tightly-sized chunks — every alloc beyond the 4th opens a new chunk.
68    /// let arena = DropArena::<u32>::with_chunk_capacity(4);
69    /// for i in 0..4 {
70    ///     let _ = arena.alloc(i);
71    /// }
72    /// assert_eq!(arena.chunk_count(), 1);
73    /// let _ = arena.alloc(99);
74    /// assert_eq!(arena.chunk_count(), 2);
75    /// ```
76    #[inline]
77    #[must_use]
78    pub fn with_chunk_capacity(chunk_capacity: usize) -> Self {
79        Self {
80            chunks: UnsafeCell::new(Vec::new()),
81            chunk_capacity: core::cmp::max(chunk_capacity, 1),
82        }
83    }
84
85    /// Total number of `T` values currently held across every chunk.
86    #[inline]
87    #[must_use]
88    pub fn len(&self) -> usize {
89        // SAFETY: `&self` read of `chunks` is sound because `DropArena`
90        // is not `Sync` and the only `&mut self` operations exclude
91        // concurrent `&self` access.
92        let chunks = unsafe { &*self.chunks.get() };
93        chunks.iter().map(Vec::len).sum()
94    }
95
96    /// Returns `true` when the arena holds no values.
97    #[inline]
98    #[must_use]
99    pub fn is_empty(&self) -> bool {
100        self.len() == 0
101    }
102
103    /// Number of chunks currently held.
104    #[inline]
105    #[must_use]
106    pub fn chunk_count(&self) -> usize {
107        // SAFETY: see `len`.
108        let chunks = unsafe { &*self.chunks.get() };
109        chunks.len()
110    }
111
112    /// Allocates `value` and returns a unique reference to it.
113    ///
114    /// The value is moved into a chunk; its destructor will run when the
115    /// arena is dropped. Panics only if the global allocator itself fails.
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// use arena_lib::DropArena;
121    ///
122    /// let arena = DropArena::<String>::new();
123    /// let s = arena.alloc(String::from("owned"));
124    /// assert_eq!(s.as_str(), "owned");
125    /// ```
126    #[allow(
127        clippy::mut_from_ref,
128        reason = "interior mutability via UnsafeCell; each call returns a disjoint slot in a chunk"
129    )]
130    pub fn alloc(&self, value: T) -> &mut T {
131        // SAFETY: `&self` access to `chunks` is sound because `DropArena`
132        // is not `Sync` and `&mut self` operations exclude concurrent
133        // `&self` access. We use the borrow only to push a new chunk if
134        // needed and to write into the current chunk's spare capacity.
135        let chunks = unsafe { &mut *self.chunks.get() };
136
137        let needs_new_chunk = match chunks.last() {
138            None => true,
139            Some(c) => c.len() == c.capacity(),
140        };
141        if needs_new_chunk {
142            chunks.push(Vec::with_capacity(self.chunk_capacity));
143        }
144
145        let current = match chunks.last_mut() {
146            Some(c) => c,
147            None => panic!("drop arena chunk invariant violated"),
148        };
149
150        let len = current.len();
151        debug_assert!(len < current.capacity());
152
153        // SAFETY: `len < capacity` was just ensured, so adding `len` to
154        // the chunk's base pointer yields an in-bounds pointer into the
155        // reserved (but uninitialised) tail of the chunk's buffer.
156        let slot_ptr: *mut T = unsafe { current.as_mut_ptr().add(len) };
157        // SAFETY: `slot_ptr` is properly aligned for `T` (the chunk is a
158        // `Vec<T>`), points into reserved capacity, and is exclusive.
159        unsafe { core::ptr::write(slot_ptr, value) };
160        // SAFETY: we just initialised the slot at index `len`; growing
161        // `len` by one is sound (the chunk now logically owns that slot).
162        unsafe { current.set_len(len + 1) };
163
164        // SAFETY: `slot_ptr` is valid for `&mut T` for the lifetime of
165        // `&self`:
166        //   - The chunk's `Vec<T>` buffer is heap-allocated and its
167        //     address is stable until the chunk itself is dropped.
168        //   - The outer `Vec<Vec<T>>` may reallocate when chunks are
169        //     pushed, but moving a `Vec<T>` does not move its underlying
170        //     buffer; the slot's address is preserved.
171        //   - We never grow an existing chunk (we always push a fresh
172        //     one), so the chunk's buffer is never reallocated, only the
173        //     `len` advances.
174        //   - The borrow is tied to `&self`; `&mut self` operations
175        //     (`reset` is not provided; the arena clears on `Drop` only)
176        //     would invalidate it via the borrow checker.
177        unsafe { &mut *slot_ptr }
178    }
179}
180
181impl<T> Default for DropArena<T> {
182    #[inline]
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188impl<T: core::fmt::Debug> core::fmt::Debug for DropArena<T> {
189    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
190        f.debug_struct("DropArena")
191            .field("len", &self.len())
192            .field("chunk_count", &self.chunk_count())
193            .field("chunk_capacity", &self.chunk_capacity)
194            .finish()
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use alloc::string::String;
202    use alloc::sync::Arc;
203
204    #[test]
205    fn alloc_returns_unique_references() {
206        let arena = DropArena::<u32>::new();
207        let a = arena.alloc(1);
208        let b = arena.alloc(2);
209        assert_eq!(*a, 1);
210        assert_eq!(*b, 2);
211        assert_eq!(arena.len(), 2);
212    }
213
214    #[test]
215    fn grows_to_new_chunk_when_current_is_full() {
216        let arena = DropArena::<u32>::with_chunk_capacity(4);
217        for i in 0..4 {
218            let _ = arena.alloc(i);
219        }
220        assert_eq!(arena.chunk_count(), 1);
221        let _ = arena.alloc(99); // forces a new chunk
222        assert_eq!(arena.chunk_count(), 2);
223        assert_eq!(arena.len(), 5);
224    }
225
226    #[test]
227    fn destructors_run_on_drop() {
228        let shared = Arc::new(0_u32);
229        {
230            let arena = DropArena::<Arc<u32>>::new();
231            let _ = arena.alloc(Arc::clone(&shared));
232            let _ = arena.alloc(Arc::clone(&shared));
233            assert_eq!(Arc::strong_count(&shared), 3);
234        }
235        assert_eq!(Arc::strong_count(&shared), 1);
236    }
237
238    #[test]
239    fn mutating_returned_reference_is_visible() {
240        let arena = DropArena::<String>::new();
241        let s = arena.alloc(String::from("hello"));
242        s.push_str(", world");
243        assert_eq!(s, "hello, world");
244    }
245
246    #[test]
247    fn references_remain_valid_across_chunk_growth() {
248        let arena = DropArena::<u32>::with_chunk_capacity(2);
249        let first = arena.alloc(100);
250        let _ = arena.alloc(200);
251        // Force chunk growth — first should still be valid.
252        let _ = arena.alloc(300);
253        let _ = arena.alloc(400);
254        let _ = arena.alloc(500);
255        assert_eq!(*first, 100);
256    }
257
258    #[test]
259    fn with_chunk_capacity_clamps_zero_to_one() {
260        let arena = DropArena::<u8>::with_chunk_capacity(0);
261        let _ = arena.alloc(1);
262        let _ = arena.alloc(2);
263        assert_eq!(arena.len(), 2);
264        // Should have grown to at least one chunk per value because cap=1.
265        assert_eq!(arena.chunk_count(), 2);
266    }
267}