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    /// Reset the arena, invalidating all previously allocated values.
176    ///
177    /// The first chunk is kept and reused; additional chunks are released.
178    /// This makes repeated build–reset cycles allocation-free in the steady
179    /// state, which is the basis of the workspace pool in `ocas-atom`.
180    ///
181    /// # Safety contract (enforced by convention)
182    ///
183    /// Any reference returned by [`allocate_with`](Arena::allocate_with) or
184    /// [`allocate_slice`](Arena::allocate_slice) before the reset **must not**
185    /// be used afterwards — the memory may be handed out again for different
186    /// values. Callers must treat reset as the end of a generation.
187    ///
188    /// # Example
189    ///
190    /// ```
191    /// use ocas_core::arena::Arena;
192    ///
193    /// let arena = Arena::new();
194    /// let _ = arena.allocate_with(|| 1);
195    /// arena.reset();
196    /// let value = arena.allocate_with(|| 2);
197    /// assert_eq!(*value, 2);
198    /// ```
199    pub fn reset(&self) {
200        let mut chunks = self.chunks.borrow_mut();
201        // Keep the first chunk (reusable block), release the rest.
202        chunks.truncate(1);
203        if let Some(first) = chunks.first_mut() {
204            first.offset = 0;
205        }
206    }
207
208    /// Return the number of chunks currently held by the arena.
209    pub fn chunk_count(&self) -> usize {
210        self.chunks.borrow().len()
211    }
212}
213
214impl Default for Arena {
215    fn default() -> Self {
216        Self::new()
217    }
218}
219
220impl Drop for Arena {
221    fn drop(&mut self) {
222        // 0.1.0: destructors for allocated values are intentionally not called.
223        // Only `Copy`/non-owning values may be stored.
224    }
225}
226
227struct Chunk {
228    memory: NonNull<u8>,
229    size: usize,
230    align: usize,
231    offset: usize,
232}
233
234impl Chunk {
235    fn new(size: usize, align: usize) -> Self {
236        let layout = Layout::from_size_align(size, align).expect("invalid chunk layout");
237        // SAFETY: layout is non-zero and properly aligned.
238        let memory = unsafe { NonNull::new_unchecked(alloc::alloc(layout)) };
239        Self {
240            memory,
241            size,
242            align,
243            offset: 0,
244        }
245    }
246
247    fn try_alloc(&mut self, layout: Layout) -> Option<NonNull<u8>> {
248        // The chunk's base pointer is only guaranteed to be aligned to
249        // `self.align`; refuse requests needing stricter alignment so the
250        // caller falls through to a fresh, suitably-aligned chunk.
251        if layout.align() > self.align {
252            return None;
253        }
254        let aligned_offset = align_up(self.offset, layout.align());
255        let end = aligned_offset.checked_add(layout.size())?;
256        if end > self.size {
257            return None;
258        }
259
260        // SAFETY: offset is within the allocated block and aligned.
261        let ptr = unsafe { NonNull::new_unchecked(self.memory.as_ptr().add(aligned_offset)) };
262        self.offset = end;
263        Some(ptr)
264    }
265}
266
267impl Drop for Chunk {
268    fn drop(&mut self) {
269        // Deallocation requires the same size and an alignment that is at least
270        // as large as the original allocation. The chunk was allocated with the
271        // maximum alignment requested by any layout served from this chunk, so
272        // that alignment is stored alongside the chunk.
273        let layout = Layout::from_size_align(self.size, self.align).expect("invalid chunk layout");
274        // SAFETY: `memory` was allocated with this layout.
275        unsafe {
276            alloc::dealloc(self.memory.as_ptr(), layout);
277        }
278    }
279}
280
281fn align_up(offset: usize, align: usize) -> usize {
282    assert!(align.is_power_of_two(), "alignment must be a power of two");
283    (offset + align - 1) & !(align - 1)
284}
285
286/// An owned expression that keeps its arena alive.
287pub struct OwnedExpr<T> {
288    #[allow(dead_code)]
289    arena: Box<Arena>,
290    root: *mut T,
291    _marker: PhantomData<T>,
292}
293
294impl<T> OwnedExpr<T> {
295    /// Create an owned expression from an arena and a root pointer.
296    ///
297    /// # Safety
298    ///
299    /// `root` must point to a value allocated in `arena` and must be valid
300    /// for the lifetime of `arena`.
301    pub unsafe fn new(arena: Box<Arena>, root: *mut T) -> Self {
302        Self {
303            arena,
304            root,
305            _marker: PhantomData,
306        }
307    }
308
309    /// Access the root expression.
310    pub fn root(&self) -> &T {
311        // SAFETY: root is valid as long as arena is alive.
312        unsafe { &*self.root }
313    }
314}
315
316unsafe impl<T: Send> Send for OwnedExpr<T> {}
317unsafe impl<T: Sync> Sync for OwnedExpr<T> {}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322    use proptest::prelude::*;
323
324    mod reset {
325        use super::*;
326
327        #[test]
328        fn reset_keeps_first_chunk() {
329            let arena = Arena::with_capacity(64);
330            // Force several chunks (64-byte blocks, 8-byte values).
331            for i in 0..100u64 {
332                arena.allocate_with(|| i);
333            }
334            assert!(arena.chunk_count() > 1);
335            arena.reset();
336            assert_eq!(arena.chunk_count(), 1);
337        }
338
339        #[test]
340        fn reset_allows_reuse() {
341            let arena = Arena::new();
342            let _ = arena.allocate_with(|| 1u64);
343            arena.reset();
344            let value = arena.allocate_with(|| 2u64);
345            assert_eq!(*value, 2);
346        }
347
348        #[test]
349        fn reset_reuse_steady_state_allocates_nothing() {
350            let arena = Arena::with_capacity(4096);
351            for round in 0..1000 {
352                for i in 0..50u64 {
353                    let v = arena.allocate_with(|| i + round);
354                    assert_eq!(*v, i + round);
355                }
356                arena.reset();
357            }
358            // Steady state: still a single chunk after 1000 generations.
359            assert_eq!(arena.chunk_count(), 1);
360        }
361
362        #[test]
363        fn overaligned_allocation_gets_own_chunk() {
364            let arena = Arena::new();
365            let _ = arena.allocate_with(|| 1u8);
366            #[repr(align(64))]
367            #[derive(Copy, Clone)]
368            struct Wide(u64);
369            let w = arena.allocate_with(|| Wide(7));
370            assert_eq!(w as *const Wide as usize % 64, 0);
371            assert_eq!(w.0, 7);
372        }
373    }
374
375    mod simple {
376        use super::*;
377
378        #[test]
379        fn allocate_single_integer() {
380            let arena = Arena::new();
381            let value = arena.allocate_with(|| 42);
382            assert_eq!(*value, 42);
383        }
384
385        #[test]
386        fn allocate_two_integers() {
387            let arena = Arena::new();
388            let a = arena.allocate_with(|| 1);
389            let b = arena.allocate_with(|| 2);
390            assert_eq!(*a, 1);
391            assert_eq!(*b, 2);
392        }
393
394        #[test]
395        fn allocate_empty_slice() {
396            let arena = Arena::new();
397            let slice: &[i32] = arena.allocate_slice(&[]);
398            assert!(slice.is_empty());
399        }
400
401        #[test]
402        fn allocate_small_slice() {
403            let arena = Arena::new();
404            let data = [10, 20, 30];
405            let slice = arena.allocate_slice(&data);
406            assert_eq!(slice, &data[..]);
407        }
408
409        #[test]
410        fn arena_default_matches_new() {
411            let arena: Arena = Default::default();
412            let value = arena.allocate_with(|| "x");
413            assert_eq!(*value, "x");
414        }
415    }
416
417    mod medium {
418        use super::*;
419
420        #[test]
421        fn allocate_larger_than_block() {
422            let arena = Arena::with_capacity(16);
423            let data = [0u8; 128];
424            let ptr = arena.allocate_with(|| data);
425            assert_eq!(*ptr, data);
426        }
427
428        #[test]
429        fn allocate_slice_larger_than_block() {
430            let arena = Arena::with_capacity(16);
431            let values: Vec<u8> = (0..=255).collect();
432            let slice = arena.allocate_slice(&values);
433            assert_eq!(slice, &values[..]);
434        }
435
436        #[test]
437        fn multiple_chunks_for_many_values() {
438            let arena = Arena::with_capacity(32);
439            let mut sum = 0i64;
440            for i in 0..100 {
441                let value = arena.allocate_with(|| i);
442                sum += *value;
443            }
444            assert_eq!(sum, 4950);
445        }
446
447        #[test]
448        fn multiple_chunks_for_many_slices() {
449            let arena = Arena::with_capacity(64);
450            let mut total = 0i64;
451            for i in 0..50 {
452                let values: Vec<i64> = (0..10).map(|j| i * 10 + j).collect();
453                let slice = arena.allocate_slice(&values);
454                total += slice.iter().sum::<i64>();
455            }
456            assert_eq!(total, 124_750);
457        }
458
459        #[test]
460        fn owned_expr_keeps_arena_alive() {
461            let arena = Box::new(Arena::new());
462            let root = arena.allocate_with(|| 123);
463            let root_ptr: *mut i32 = root;
464            // SAFETY: root was allocated in arena, and arena outlives OwnedExpr.
465            let owned = unsafe { OwnedExpr::new(arena, root_ptr) };
466            assert_eq!(*owned.root(), 123);
467        }
468    }
469
470    mod complex {
471        use super::*;
472
473        #[test]
474        fn copy_values_survive_arena_drop() {
475            let value = {
476                let arena = Arena::new();
477                let ptr = arena.allocate_with(|| 42i32);
478                *ptr
479            };
480            assert_eq!(value, 42);
481        }
482
483        #[test]
484        fn alignment_of_large_type() {
485            #[derive(Clone, Copy)]
486            #[repr(C, align(64))]
487            struct BigAlign(u64);
488
489            // The first allocation in a fresh chunk must respect the requested
490            // alignment. Use a single-element slice so the layout alignment is
491            // dominated by BigAlign.
492            let arena = Arena::with_capacity(4096);
493            let values = [BigAlign(7)];
494            let slice = arena.allocate_slice(&values);
495            assert!((slice.as_ptr() as usize).is_multiple_of(64));
496            assert_eq!(slice[0].0, 7);
497        }
498
499        #[test]
500        fn alignment_of_single_value() {
501            #[derive(Clone, Copy)]
502            #[repr(C, align(64))]
503            struct BigAlign(u64);
504
505            let arena = Arena::with_capacity(4096);
506            let value = arena.allocate_with(|| BigAlign(7));
507            assert_eq!((value as *const BigAlign) as usize % 64, 0);
508            assert_eq!(value.0, 7);
509        }
510
511        #[test]
512        #[should_panic(expected = "cannot allocate zero-sized types in Arena")]
513        fn zero_sized_type_panics() {
514            let arena = Arena::new();
515            let _: &mut () = arena.allocate_with(|| ());
516        }
517
518        #[test]
519        #[should_panic(expected = "cannot allocate zero-sized types in Arena")]
520        fn zero_sized_slice_panics() {
521            let arena = Arena::new();
522            let _: &[()] = arena.allocate_slice(&[()]);
523        }
524
525        #[test]
526        fn owned_expr_is_send_sync() {
527            fn assert_send_sync<T: Send + Sync>() {}
528            assert_send_sync::<OwnedExpr<u8>>();
529        }
530    }
531
532    mod extreme {
533        use super::*;
534
535        #[test]
536        fn stress_mixed_allocations() {
537            let arena = Arena::with_capacity(256);
538            let mut total = 0usize;
539            for size in (1usize..=1000).step_by(7) {
540                let data: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
541                let ptr = arena.allocate_with(|| data.clone());
542                total += ptr.iter().map(|&x| x as usize).sum::<usize>();
543            }
544            assert!(total > 0);
545        }
546
547        proptest! {
548            #[test]
549            fn allocate_random_sizes(size in 1usize..10_000) {
550                let arena = Arena::with_capacity(256);
551                let data: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
552                let ptr = arena.allocate_with(|| data.clone());
553                prop_assert_eq!(&ptr[..], &data[..]);
554            }
555
556            #[test]
557            fn allocate_many_random_values(sizes in prop::collection::vec(1usize..512, 1..50)) {
558                let arena = Arena::with_capacity(256);
559                let mut total = 0usize;
560                for (idx, size) in sizes.iter().enumerate() {
561                    let expected: Vec<u8> = (0..*size).map(|i| (i.wrapping_add(idx)) as u8).collect();
562                    let ptr = arena.allocate_with(|| expected.clone());
563                    prop_assert_eq!(&ptr[..], &expected[..]);
564                    total += size;
565                }
566                prop_assert!(total > 0);
567            }
568
569            #[test]
570            fn slice_roundtrip(values in prop::collection::vec(0i32..100, 0..512)) {
571                let arena = Arena::with_capacity(256);
572                let slice = arena.allocate_slice(&values);
573                prop_assert_eq!(slice, &values[..]);
574            }
575        }
576    }
577}