Skip to main content

mago_allocator/
arena.rs

1//! The bump arenas and the [`Arena`] allocation trait.
2
3use core::alloc::Layout;
4use core::fmt;
5use core::ptr::NonNull;
6
7use crate::alloc::AllocError;
8use crate::alloc::Allocator;
9use crate::alloc::Global;
10use crate::boxed::Box;
11use crate::vec::Vec;
12
13/// A single-threaded bump arena.
14///
15/// Fastest of the three; `Send` but not `Sync`. Use it for per-task scratch and
16/// any single-threaded pass.
17pub struct LocalArena<A: Allocator = Global>(blink_alloc::BlinkAlloc<A>);
18
19/// A thread-safe bump arena (`Send + Sync`).
20///
21/// Share `&SharedArena` across threads (for example, one arena for a whole
22/// parallel compilation); every worker allocates into it and the results live for
23/// the shared borrow. For contention-free per-worker scratch, take a
24/// [`ScopedArena`] with [`scoped`](SharedArena::scoped).
25pub struct SharedArena<A: Allocator = Global>(blink_alloc::SyncBlinkAlloc<A>);
26
27/// A thread-local view into a [`SharedArena`], handed out by
28/// [`SharedArena::scoped`].
29///
30/// It draws blocks from its parent [`SharedArena`] and hands out allocations
31/// locally, without the atomic contention of allocating through the shared arena
32/// directly. Allocations are tied to the scope's borrow rather than to the parent
33/// arena, so a `ScopedArena` is for **scratch that does not escape the scope**;
34/// anything that must outlive the scope should be allocated into the
35/// [`SharedArena`] itself.
36pub struct ScopedArena<'shared, A: Allocator = Global>(blink_alloc::LocalBlinkAlloc<'shared, A>);
37
38impl LocalArena<Global> {
39    /// Creates new arena allocator that uses global allocator
40    /// to allocate memory chunks.
41    ///
42    /// See [`LocalArena::new_in`] for using custom allocator.
43    #[inline]
44    #[must_use]
45    pub const fn new() -> Self {
46        Self::new_in(Global)
47    }
48
49    /// Creates new arena allocator that uses global allocator
50    /// to allocate memory chunks.
51    /// With this method you can specify initial chunk size.
52    ///
53    /// See [`LocalArena::new_in`] for using custom allocator.
54    #[inline]
55    #[must_use]
56    pub const fn with_chunk_size(chunk_size: usize) -> Self {
57        Self::with_chunk_size_in(chunk_size, Global)
58    }
59}
60
61impl<A: Allocator> LocalArena<A> {
62    /// Creates a new arena that uses `arena` to allocate its memory chunks.
63    #[inline]
64    #[must_use]
65    pub const fn new_in(arena: A) -> Self {
66        Self(blink_alloc::BlinkAlloc::new_in(arena))
67    }
68
69    /// Creates a new, empty arena whose chunks are at least `chunk_size` bytes.
70    #[inline]
71    #[must_use]
72    pub const fn with_chunk_size_in(chunk_size: usize, arena: A) -> Self {
73        Self(blink_alloc::BlinkAlloc::with_chunk_size_in(chunk_size, arena))
74    }
75
76    /// Frees every allocation at once, keeping the arena's chunks for reuse.
77    #[inline]
78    pub fn reset(&mut self) {
79        self.0.reset();
80    }
81}
82
83impl Default for LocalArena {
84    #[inline]
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl SharedArena<Global> {
91    /// Creates new arena allocator that uses global allocator
92    /// to allocate memory chunks.
93    ///
94    /// See [`SharedArena::new_in`] for using custom allocator.
95    #[inline]
96    #[must_use]
97    pub const fn new() -> Self {
98        Self::new_in(Global)
99    }
100
101    /// Creates new arena allocator that uses global allocator
102    /// to allocate memory chunks.
103    /// With this method you can specify initial chunk size.
104    ///
105    /// See [`SharedArena::new_in`] for using custom allocator.
106    #[inline]
107    #[must_use]
108    pub const fn with_chunk_size(chunk_size: usize) -> Self {
109        Self::with_chunk_size_in(chunk_size, Global)
110    }
111}
112
113impl<A: Allocator> SharedArena<A> {
114    /// Creates new arena allocator that uses the given allocator
115    /// to allocate memory chunks.
116    #[inline]
117    #[must_use]
118    pub const fn new_in(arena: A) -> Self {
119        Self(blink_alloc::SyncBlinkAlloc::new_in(arena))
120    }
121
122    /// Creates a new, empty arena whose chunks are at least `chunk_size` bytes.
123    #[inline]
124    #[must_use]
125    pub const fn with_chunk_size_in(chunk_size: usize, arena: A) -> Self {
126        Self(blink_alloc::SyncBlinkAlloc::with_chunk_size_in(chunk_size, arena))
127    }
128
129    /// Returns a thread-local [`ScopedArena`] drawing from this arena.
130    ///
131    /// Each worker in a parallel pass can take its own scope and allocate scratch
132    /// without contending on the shared bump pointer.
133    #[inline]
134    pub fn scoped(&self) -> ScopedArena<'_, A> {
135        ScopedArena(self.0.local())
136    }
137
138    /// Frees every allocation at once, keeping the arena's chunks for reuse.
139    #[inline]
140    pub fn reset(&mut self) {
141        self.0.reset();
142    }
143}
144
145impl Default for SharedArena {
146    #[inline]
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152impl<A: Allocator> ScopedArena<'_, A> {
153    /// Frees the scope's allocations, returning unused blocks to the parent arena.
154    #[inline]
155    pub fn reset(&mut self) {
156        self.0.reset();
157    }
158}
159
160macro_rules! forward_allocator {
161    () => {
162        #[inline]
163        fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
164            Allocator::allocate(&self.0, layout)
165        }
166
167        #[inline]
168        fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
169            Allocator::allocate_zeroed(&self.0, layout)
170        }
171
172        #[inline]
173        unsafe fn deallocate(&self, pointer: NonNull<u8>, layout: Layout) {
174            // SAFETY: the caller upholds `deallocate`'s contract; forwarded verbatim.
175            unsafe { Allocator::deallocate(&self.0, pointer, layout) }
176        }
177
178        #[inline]
179        unsafe fn grow(
180            &self,
181            pointer: NonNull<u8>,
182            old_layout: Layout,
183            new_layout: Layout,
184        ) -> Result<NonNull<[u8]>, AllocError> {
185            // SAFETY: the caller upholds `grow`'s contract; forwarded verbatim.
186            unsafe { Allocator::grow(&self.0, pointer, old_layout, new_layout) }
187        }
188
189        #[inline]
190        unsafe fn grow_zeroed(
191            &self,
192            pointer: NonNull<u8>,
193            old_layout: Layout,
194            new_layout: Layout,
195        ) -> Result<NonNull<[u8]>, AllocError> {
196            // SAFETY: the caller upholds `grow_zeroed`'s contract; forwarded verbatim.
197            unsafe { Allocator::grow_zeroed(&self.0, pointer, old_layout, new_layout) }
198        }
199
200        #[inline]
201        unsafe fn shrink(
202            &self,
203            pointer: NonNull<u8>,
204            old_layout: Layout,
205            new_layout: Layout,
206        ) -> Result<NonNull<[u8]>, AllocError> {
207            // SAFETY: the caller upholds `shrink`'s contract; forwarded verbatim.
208            unsafe { Allocator::shrink(&self.0, pointer, old_layout, new_layout) }
209        }
210    };
211}
212
213// SAFETY: every method forwards unchanged to the inner blink-alloc allocator,
214// which is itself a sound `Allocator`; the newtype adds no behavior of its own.
215unsafe impl<A: Allocator> Allocator for LocalArena<A> {
216    forward_allocator!();
217}
218
219// SAFETY: see the `LocalArena` impl above; this only forwards to the inner allocator.
220unsafe impl<A: Allocator> Allocator for SharedArena<A> {
221    forward_allocator!();
222}
223
224// SAFETY: see the `LocalArena` impl above; this only forwards to the inner allocator.
225unsafe impl<A: Allocator> Allocator for ScopedArena<'_, A> {
226    forward_allocator!();
227}
228
229struct ArenaFormatter<'buffer, 'arena, A: Allocator + ?Sized> {
230    buffer: &'buffer mut Vec<'arena, u8, A>,
231}
232
233impl<A: Allocator + ?Sized> fmt::Write for ArenaFormatter<'_, '_, A> {
234    #[inline]
235    fn write_str(&mut self, fragment: &str) -> fmt::Result {
236        self.buffer.extend_from_slice(fragment.as_bytes());
237
238        Ok(())
239    }
240}
241
242/// Ergonomic, `bumpalo`-style allocation on top of any [`Allocator`].
243///
244/// Every method returns a reference (or mutable slice) tied to the borrow of the
245/// arena, so the allocation escapes the call and lives for the arena's lifetime.
246/// Implemented for every [`Allocator`] (including the three arenas and shared
247/// references to them) via a blanket impl.
248pub trait Arena: Allocator {
249    /// Allocates `value` and returns a unique reference to it.
250    fn alloc<T>(&self, value: T) -> &mut T;
251
252    /// Allocates the result of `f`, evaluated directly into the arena slot.
253    fn alloc_with<T>(&self, f: impl FnOnce() -> T) -> &mut T;
254
255    /// Copies `src` into the arena and returns the copy.
256    fn alloc_str(&self, src: &str) -> &mut str;
257
258    /// Formats `arguments` directly into the arena and returns the resulting string.
259    ///
260    /// The arena-equivalent of [`format!`], with no intermediate global allocation:
261    ///
262    /// ```
263    /// use mago_allocator::prelude::*;
264    ///
265    /// let arena = LocalArena::new();
266    /// let greeting = arena.alloc_fmt(format_args!("{}-{}", "v", 2));
267    /// assert_eq!(greeting, "v-2");
268    /// ```
269    fn alloc_fmt(&self, arguments: fmt::Arguments<'_>) -> &mut str;
270
271    /// Copies a slice of `Copy` values into the arena.
272    fn alloc_slice_copy<T>(&self, src: &[T]) -> &mut [T]
273    where
274        T: Copy;
275
276    /// Clones a slice of `Clone` values into the arena.
277    fn alloc_slice_clone<T>(&self, src: &[T]) -> &mut [T]
278    where
279        T: Clone;
280
281    /// Allocates `len` copies of `value`.
282    fn alloc_slice_fill_copy<T>(&self, len: usize, value: T) -> &mut [T]
283    where
284        T: Copy;
285
286    /// Allocates `len` clones of `value`.
287    fn alloc_slice_fill_clone<T>(&self, len: usize, value: &T) -> &mut [T]
288    where
289        T: Clone;
290
291    /// Allocates `len` elements, each produced by `f(index)`.
292    fn alloc_slice_fill_with<T>(&self, len: usize, f: impl FnMut(usize) -> T) -> &mut [T];
293
294    /// Allocates `len` default-constructed elements.
295    fn alloc_slice_fill_default<T>(&self, len: usize) -> &mut [T]
296    where
297        T: Default;
298
299    /// Collects `iter` into the arena and returns the resulting slice.
300    fn alloc_slice_fill_iter<T>(&self, iter: impl IntoIterator<Item = T>) -> &mut [T];
301}
302
303impl<A: Allocator + ?Sized> Arena for A {
304    #[inline]
305    fn alloc<T>(&self, value: T) -> &mut T {
306        Box::leak(Box::new_in(value, self))
307    }
308
309    #[inline]
310    fn alloc_with<T>(&self, f: impl FnOnce() -> T) -> &mut T {
311        Box::leak(Box::new_in(f(), self))
312    }
313
314    #[inline]
315    fn alloc_str(&self, src: &str) -> &mut str {
316        let bytes = self.alloc_slice_copy(src.as_bytes());
317
318        // SAFETY: `bytes` is a byte-for-byte copy of a valid `&str`.
319        unsafe { core::str::from_utf8_unchecked_mut(bytes) }
320    }
321
322    #[inline]
323    fn alloc_fmt(&self, arguments: fmt::Arguments<'_>) -> &mut str {
324        let mut buffer = Vec::new_in(self);
325        let mut formatter = ArenaFormatter { buffer: &mut buffer };
326        let _ = fmt::write(&mut formatter, arguments);
327
328        let bytes = buffer.leak();
329
330        // SAFETY: `fmt::write` only ever feeds valid UTF-8 to `write_str`, so the
331        // accumulated bytes are valid UTF-8.
332        unsafe { core::str::from_utf8_unchecked_mut(bytes) }
333    }
334
335    #[inline]
336    fn alloc_slice_copy<T>(&self, src: &[T]) -> &mut [T]
337    where
338        T: Copy,
339    {
340        let mut vec = Vec::with_capacity_in(src.len(), self);
341        vec.extend_from_slice(src);
342        vec.leak()
343    }
344
345    #[inline]
346    fn alloc_slice_clone<T>(&self, src: &[T]) -> &mut [T]
347    where
348        T: Clone,
349    {
350        let mut vec = Vec::with_capacity_in(src.len(), self);
351        vec.extend(src.iter().cloned());
352        vec.leak()
353    }
354
355    #[inline]
356    fn alloc_slice_fill_copy<T>(&self, len: usize, value: T) -> &mut [T]
357    where
358        T: Copy,
359    {
360        let mut vec = Vec::with_capacity_in(len, self);
361        vec.resize(len, value);
362        vec.leak()
363    }
364
365    #[inline]
366    fn alloc_slice_fill_clone<T>(&self, len: usize, value: &T) -> &mut [T]
367    where
368        T: Clone,
369    {
370        let mut vec = Vec::with_capacity_in(len, self);
371        vec.resize_with(len, || value.clone());
372        vec.leak()
373    }
374
375    #[inline]
376    fn alloc_slice_fill_with<T>(&self, len: usize, mut f: impl FnMut(usize) -> T) -> &mut [T] {
377        let mut vec = Vec::with_capacity_in(len, self);
378        for index in 0..len {
379            vec.push(f(index));
380        }
381        vec.leak()
382    }
383
384    #[inline]
385    fn alloc_slice_fill_default<T>(&self, len: usize) -> &mut [T]
386    where
387        T: Default,
388    {
389        self.alloc_slice_fill_with(len, |_| T::default())
390    }
391
392    #[inline]
393    fn alloc_slice_fill_iter<T>(&self, iter: impl IntoIterator<Item = T>) -> &mut [T] {
394        let mut vec = Vec::new_in(self);
395        vec.extend(iter);
396        vec.leak()
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::collections::HashMap;
404    use crate::vec::Vec;
405
406    #[test]
407    fn local_alloc_roundtrips() {
408        let arena = LocalArena::new();
409        let value = arena.alloc(42u32);
410
411        assert_eq!(*value, 42);
412    }
413
414    #[test]
415    fn alloc_str_copies() {
416        let arena = LocalArena::new();
417        let copied = arena.alloc_str("hello");
418
419        assert_eq!(copied, "hello");
420    }
421
422    #[test]
423    fn alloc_fmt_formats_into_arena() {
424        let arena = LocalArena::new();
425        let formatted = arena.alloc_fmt(format_args!("{}+{}={}", 2, 3, 5));
426
427        assert_eq!(formatted, "2+3=5");
428    }
429
430    #[test]
431    fn format_in_macro_formats_into_arena() {
432        let arena = LocalArena::new();
433        let label = crate::format_in!(arena, "{}::{}", "App", "VERSION");
434
435        assert_eq!(label, "App::VERSION");
436    }
437
438    #[test]
439    fn alloc_slice_copy_copies() {
440        let arena = LocalArena::new();
441        let slice = arena.alloc_slice_copy(&[1, 2, 3]);
442
443        assert_eq!(slice, &[1, 2, 3]);
444    }
445
446    #[test]
447    fn alloc_slice_fill_with_invokes_closure() {
448        let arena = LocalArena::new();
449        let slice = arena.alloc_slice_fill_with(4, |index| index * 2);
450
451        assert_eq!(slice, &[0, 2, 4, 6]);
452    }
453
454    #[test]
455    fn shared_vec_grows_in_place() {
456        let arena = SharedArena::new();
457        let mut vec = Vec::new_in(&arena);
458        for value in 0..1000 {
459            vec.push(value);
460        }
461
462        assert_eq!(vec.len(), 1000);
463        assert_eq!(vec.last(), Some(&999));
464    }
465
466    #[test]
467    fn scoped_alloc_roundtrips() {
468        let shared = SharedArena::new();
469        let scoped = shared.scoped();
470        let value = scoped.alloc(7u8);
471
472        assert_eq!(*value, 7);
473    }
474
475    #[test]
476    fn arena_hashmap_roundtrips() {
477        let arena = SharedArena::new();
478        let mut map = HashMap::new_in(&arena);
479        map.insert(1u32, "one");
480
481        assert_eq!(map.get(&1), Some(&"one"));
482    }
483
484    #[cfg(feature = "serde")]
485    #[test]
486    fn arena_vec_serializes() {
487        let arena = LocalArena::new();
488        let mut numbers = Vec::new_in(&arena);
489        numbers.extend([1, 2, 3]);
490
491        let json = serde_json::to_string(&numbers);
492
493        assert!(matches!(json.as_deref(), Ok("[1,2,3]")));
494    }
495}