Skip to main content

ocas_core/
arena.rs

1//! Arena allocator for expression nodes.
2//!
3//! oCAS uses a bump allocator to store expression sub-nodes. This avoids the
4//! per-node allocation overhead of `Box` or `Rc` and improves cache locality.
5//! When the arena is dropped, the entire tree is freed at once.
6//!
7//! # Current limitations
8//!
9//! The 0.1.0 `Arena` does **not** run destructors for allocated values. It is
10//! therefore only safe to store `Copy` types or types that do not own resources
11//! requiring explicit cleanup. This restriction will be lifted once expression
12//! trees need to store owned strings or other `Drop` types.
13
14use std::alloc::{self, Layout};
15use std::cell::RefCell;
16use std::marker::PhantomData;
17use std::mem;
18use std::ptr::NonNull;
19
20/// Default block size for new arena chunks.
21const DEFAULT_BLOCK_SIZE: usize = 64 * 1024;
22
23/// A bump-allocated region of memory.
24///
25/// Values allocated in an `Arena` are tied to its lifetime and must not
26/// outlive it. The public API enforces this with borrow checker lifetimes.
27///
28/// # Type safety note
29///
30/// `Arena` does not run destructors. Only store `Copy` or otherwise
31/// non-owning values until drop support is added.
32///
33/// # Example
34///
35/// ```
36/// use ocas_core::arena::Arena;
37///
38/// let arena = Arena::new();
39/// let value = arena.allocate_with(|| 42);
40/// assert_eq!(*value, 42);
41/// ```
42pub struct Arena {
43    chunks: RefCell<Vec<Chunk>>,
44    block_size: usize,
45}
46
47impl Arena {
48    /// Create a new arena with the default block size.
49    ///
50    /// # Example
51    ///
52    /// ```
53    /// use ocas_core::arena::Arena;
54    ///
55    /// let arena = Arena::new();
56    /// let n = arena.allocate_with(|| 7);
57    /// assert_eq!(*n, 7);
58    /// ```
59    pub fn new() -> Self {
60        Self {
61            chunks: RefCell::new(Vec::new()),
62            block_size: DEFAULT_BLOCK_SIZE,
63        }
64    }
65
66    /// Create a new arena with a custom initial block size.
67    pub fn with_capacity(block_size: usize) -> Self {
68        Self {
69            chunks: RefCell::new(Vec::new()),
70            block_size,
71        }
72    }
73
74    /// Allocate a value in the arena, constructing it inside `init`, and return
75    /// a mutable reference tied to `self`.
76    ///
77    /// The closure form avoids any ambiguity about when mutation of the arena
78    /// occurs. The returned `&mut T` is unique because `alloc_raw` advances the
79    /// arena offset for each allocation via interior mutability.
80    ///
81    /// # Panics
82    ///
83    /// Panics if the requested layout has size zero.
84    ///
85    /// # Example
86    ///
87    /// ```
88    /// use ocas_core::arena::Arena;
89    ///
90    /// let arena = Arena::new();
91    /// let value = arena.allocate_with(|| "hello");
92    /// assert_eq!(*value, "hello");
93    /// ```
94    #[allow(clippy::mut_from_ref)]
95    pub fn allocate_with<T>(&self, init: impl FnOnce() -> T) -> &mut T {
96        let layout = Layout::new::<T>();
97        assert!(
98            layout.size() > 0,
99            "cannot allocate zero-sized types in Arena"
100        );
101        let ptr = self.alloc_raw(layout);
102
103        // SAFETY: `ptr` is non-null and properly aligned for `T`.
104        unsafe {
105            let typed = ptr.as_ptr().cast::<T>();
106            typed.write(init());
107            &mut *typed
108        }
109    }
110
111    /// Allocate a contiguous slice of `T` values in the arena.
112    ///
113    /// The returned slice is tied to the arena lifetime. Because the arena does
114    /// not run destructors, `T` must be `Copy` so that dropping the arena does
115    /// not leak resources owned by the slice elements.
116    ///
117    /// # Panics
118    ///
119    /// Panics if `T` has zero size or if the total allocation size overflows.
120    ///
121    /// # Example
122    ///
123    /// ```
124    /// use ocas_core::arena::Arena;
125    ///
126    /// let arena = Arena::new();
127    /// let slice = arena.allocate_slice(&[1, 2, 3, 4, 5]);
128    /// assert_eq!(slice, &[1, 2, 3, 4, 5]);
129    /// ```
130    pub fn allocate_slice<T: Copy>(&self, values: &[T]) -> &[T] {
131        if values.is_empty() {
132            return &[];
133        }
134
135        let layout = Layout::from_size_align(mem::size_of_val(values), mem::align_of::<T>())
136            .expect("invalid slice layout");
137        assert!(
138            layout.size() > 0,
139            "cannot allocate zero-sized types in Arena"
140        );
141
142        let ptr = self.alloc_raw(layout);
143
144        // SAFETY: `ptr` is non-null, properly aligned, and points to a block
145        // large enough to hold `values.len()` elements of type `T`.
146        unsafe {
147            let typed = ptr.as_ptr().cast::<T>();
148            std::ptr::copy_nonoverlapping(values.as_ptr(), typed, values.len());
149            std::slice::from_raw_parts(typed, values.len())
150        }
151    }
152
153    fn alloc_raw(&self, layout: Layout) -> NonNull<u8> {
154        let mut chunks = self.chunks.borrow_mut();
155
156        // Try to allocate from the current chunk.
157        if let Some(chunk) = chunks.last_mut()
158            && let Some(ptr) = chunk.try_alloc(layout)
159        {
160            return ptr;
161        }
162
163        // Need a new chunk. Use at least the requested size and alignment so the
164        // first allocation in the chunk is correctly aligned.
165        let size = layout.size().max(self.block_size);
166        let align = layout.align();
167        let mut new_chunk = Chunk::new(size, align);
168        let ptr = new_chunk
169            .try_alloc(layout)
170            .expect("new chunk should fit any layout up to its size");
171        chunks.push(new_chunk);
172        ptr
173    }
174}
175
176impl Default for Arena {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182impl Drop for Arena {
183    fn drop(&mut self) {
184        // 0.1.0: destructors for allocated values are intentionally not called.
185        // Only `Copy`/non-owning values may be stored.
186    }
187}
188
189struct Chunk {
190    memory: NonNull<u8>,
191    size: usize,
192    align: usize,
193    offset: usize,
194}
195
196impl Chunk {
197    fn new(size: usize, align: usize) -> Self {
198        let layout = Layout::from_size_align(size, align).expect("invalid chunk layout");
199        // SAFETY: layout is non-zero and properly aligned.
200        let memory = unsafe { NonNull::new_unchecked(alloc::alloc(layout)) };
201        Self {
202            memory,
203            size,
204            align,
205            offset: 0,
206        }
207    }
208
209    fn try_alloc(&mut self, layout: Layout) -> Option<NonNull<u8>> {
210        let aligned_offset = align_up(self.offset, layout.align());
211        let end = aligned_offset.checked_add(layout.size())?;
212        if end > self.size {
213            return None;
214        }
215
216        // SAFETY: offset is within the allocated block and aligned.
217        let ptr = unsafe { NonNull::new_unchecked(self.memory.as_ptr().add(aligned_offset)) };
218        self.offset = end;
219        Some(ptr)
220    }
221}
222
223impl Drop for Chunk {
224    fn drop(&mut self) {
225        // Deallocation requires the same size and an alignment that is at least
226        // as large as the original allocation. The chunk was allocated with the
227        // maximum alignment requested by any layout served from this chunk, so
228        // that alignment is stored alongside the chunk.
229        let layout = Layout::from_size_align(self.size, self.align).expect("invalid chunk layout");
230        // SAFETY: `memory` was allocated with this layout.
231        unsafe {
232            alloc::dealloc(self.memory.as_ptr(), layout);
233        }
234    }
235}
236
237fn align_up(offset: usize, align: usize) -> usize {
238    assert!(align.is_power_of_two(), "alignment must be a power of two");
239    (offset + align - 1) & !(align - 1)
240}
241
242/// An owned expression that keeps its arena alive.
243pub struct OwnedExpr<T> {
244    #[allow(dead_code)]
245    arena: Box<Arena>,
246    root: *mut T,
247    _marker: PhantomData<T>,
248}
249
250impl<T> OwnedExpr<T> {
251    /// Create an owned expression from an arena and a root pointer.
252    ///
253    /// # Safety
254    ///
255    /// `root` must point to a value allocated in `arena` and must be valid
256    /// for the lifetime of `arena`.
257    pub unsafe fn new(arena: Box<Arena>, root: *mut T) -> Self {
258        Self {
259            arena,
260            root,
261            _marker: PhantomData,
262        }
263    }
264
265    /// Access the root expression.
266    pub fn root(&self) -> &T {
267        // SAFETY: root is valid as long as arena is alive.
268        unsafe { &*self.root }
269    }
270}
271
272unsafe impl<T: Send> Send for OwnedExpr<T> {}
273unsafe impl<T: Sync> Sync for OwnedExpr<T> {}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use proptest::prelude::*;
279
280    mod simple {
281        use super::*;
282
283        #[test]
284        fn allocate_single_integer() {
285            let arena = Arena::new();
286            let value = arena.allocate_with(|| 42);
287            assert_eq!(*value, 42);
288        }
289
290        #[test]
291        fn allocate_two_integers() {
292            let arena = Arena::new();
293            let a = arena.allocate_with(|| 1);
294            let b = arena.allocate_with(|| 2);
295            assert_eq!(*a, 1);
296            assert_eq!(*b, 2);
297        }
298
299        #[test]
300        fn allocate_empty_slice() {
301            let arena = Arena::new();
302            let slice: &[i32] = arena.allocate_slice(&[]);
303            assert!(slice.is_empty());
304        }
305
306        #[test]
307        fn allocate_small_slice() {
308            let arena = Arena::new();
309            let data = [10, 20, 30];
310            let slice = arena.allocate_slice(&data);
311            assert_eq!(slice, &data[..]);
312        }
313
314        #[test]
315        fn arena_default_matches_new() {
316            let arena: Arena = Default::default();
317            let value = arena.allocate_with(|| "x");
318            assert_eq!(*value, "x");
319        }
320    }
321
322    mod medium {
323        use super::*;
324
325        #[test]
326        fn allocate_larger_than_block() {
327            let arena = Arena::with_capacity(16);
328            let data = [0u8; 128];
329            let ptr = arena.allocate_with(|| data);
330            assert_eq!(*ptr, data);
331        }
332
333        #[test]
334        fn allocate_slice_larger_than_block() {
335            let arena = Arena::with_capacity(16);
336            let values: Vec<u8> = (0..=255).collect();
337            let slice = arena.allocate_slice(&values);
338            assert_eq!(slice, &values[..]);
339        }
340
341        #[test]
342        fn multiple_chunks_for_many_values() {
343            let arena = Arena::with_capacity(32);
344            let mut sum = 0i64;
345            for i in 0..100 {
346                let value = arena.allocate_with(|| i);
347                sum += *value;
348            }
349            assert_eq!(sum, 4950);
350        }
351
352        #[test]
353        fn multiple_chunks_for_many_slices() {
354            let arena = Arena::with_capacity(64);
355            let mut total = 0i64;
356            for i in 0..50 {
357                let values: Vec<i64> = (0..10).map(|j| i * 10 + j).collect();
358                let slice = arena.allocate_slice(&values);
359                total += slice.iter().sum::<i64>();
360            }
361            assert_eq!(total, 124_750);
362        }
363
364        #[test]
365        fn owned_expr_keeps_arena_alive() {
366            let arena = Box::new(Arena::new());
367            let root = arena.allocate_with(|| 123);
368            let root_ptr: *mut i32 = root;
369            // SAFETY: root was allocated in arena, and arena outlives OwnedExpr.
370            let owned = unsafe { OwnedExpr::new(arena, root_ptr) };
371            assert_eq!(*owned.root(), 123);
372        }
373    }
374
375    mod complex {
376        use super::*;
377
378        #[test]
379        fn copy_values_survive_arena_drop() {
380            let value = {
381                let arena = Arena::new();
382                let ptr = arena.allocate_with(|| 42i32);
383                *ptr
384            };
385            assert_eq!(value, 42);
386        }
387
388        #[test]
389        fn alignment_of_large_type() {
390            #[derive(Clone, Copy)]
391            #[repr(C, align(64))]
392            struct BigAlign(u64);
393
394            // The first allocation in a fresh chunk must respect the requested
395            // alignment. Use a single-element slice so the layout alignment is
396            // dominated by BigAlign.
397            let arena = Arena::with_capacity(4096);
398            let values = [BigAlign(7)];
399            let slice = arena.allocate_slice(&values);
400            assert!((slice.as_ptr() as usize).is_multiple_of(64));
401            assert_eq!(slice[0].0, 7);
402        }
403
404        #[test]
405        fn alignment_of_single_value() {
406            #[derive(Clone, Copy)]
407            #[repr(C, align(64))]
408            struct BigAlign(u64);
409
410            let arena = Arena::with_capacity(4096);
411            let value = arena.allocate_with(|| BigAlign(7));
412            assert_eq!((value as *const BigAlign) as usize % 64, 0);
413            assert_eq!(value.0, 7);
414        }
415
416        #[test]
417        #[should_panic(expected = "cannot allocate zero-sized types in Arena")]
418        fn zero_sized_type_panics() {
419            let arena = Arena::new();
420            let _: &mut () = arena.allocate_with(|| ());
421        }
422
423        #[test]
424        #[should_panic(expected = "cannot allocate zero-sized types in Arena")]
425        fn zero_sized_slice_panics() {
426            let arena = Arena::new();
427            let _: &[()] = arena.allocate_slice(&[()]);
428        }
429
430        #[test]
431        fn owned_expr_is_send_sync() {
432            fn assert_send_sync<T: Send + Sync>() {}
433            assert_send_sync::<OwnedExpr<u8>>();
434        }
435    }
436
437    mod extreme {
438        use super::*;
439
440        #[test]
441        fn stress_mixed_allocations() {
442            let arena = Arena::with_capacity(256);
443            let mut total = 0usize;
444            for size in (1usize..=1000).step_by(7) {
445                let data: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
446                let ptr = arena.allocate_with(|| data.clone());
447                total += ptr.iter().map(|&x| x as usize).sum::<usize>();
448            }
449            assert!(total > 0);
450        }
451
452        proptest! {
453            #[test]
454            fn allocate_random_sizes(size in 1usize..10_000) {
455                let arena = Arena::with_capacity(256);
456                let data: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
457                let ptr = arena.allocate_with(|| data.clone());
458                prop_assert_eq!(&ptr[..], &data[..]);
459            }
460
461            #[test]
462            fn allocate_many_random_values(sizes in prop::collection::vec(1usize..512, 1..50)) {
463                let arena = Arena::with_capacity(256);
464                let mut total = 0usize;
465                for (idx, size) in sizes.iter().enumerate() {
466                    let expected: Vec<u8> = (0..*size).map(|i| (i.wrapping_add(idx)) as u8).collect();
467                    let ptr = arena.allocate_with(|| expected.clone());
468                    prop_assert_eq!(&ptr[..], &expected[..]);
469                    total += size;
470                }
471                prop_assert!(total > 0);
472            }
473
474            #[test]
475            fn slice_roundtrip(values in prop::collection::vec(0i32..100, 0..512)) {
476                let arena = Arena::with_capacity(256);
477                let slice = arena.allocate_slice(&values);
478                prop_assert_eq!(slice, &values[..]);
479            }
480        }
481    }
482}