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}