Skip to main content

oak_core/memory/
arena.rs

1#![doc = include_str!("readme.md")]
2use crate::tree::TokenProvenance;
3use std::{
4    alloc::{Layout, alloc, dealloc},
5    cell::{RefCell, UnsafeCell},
6    ptr::{NonNull, copy_nonoverlapping},
7};
8
9/// Default chunk size: 64KB.
10/// Large enough to amortize the cost of system-level allocations, yet small enough to be
11/// L2-cache friendly and avoid excessive internal fragmentation for small templates.
12const CHUNK_SIZE: usize = 64 * 1024;
13
14/// Alignment for all allocations: 8 bytes.
15/// This covers all standard Rust primitives (u64, f64, pointers) on 64-bit architectures.
16const ALIGN: usize = 8;
17
18// A thread-local pool of memory chunks to avoid hitting the global allocator.
19//
20// **Memory Safety & Leak Prevention:**
21// - **Bounded Capacity:** The pool is limited to 64 chunks (4MB) per thread to prevent
22//   unbounded memory growth in long-running processes.
23// - **Large Allocations:** Chunks larger than `CHUNK_SIZE` (64KB) are not pooled and
24//   are returned directly to the global allocator upon drop.
25// - **Automatic Cleanup:** All chunks are either recycled into this pool or freed when
26//   the `SyntaxArena` is dropped.
27thread_local! {
28    static CHUNK_POOL: RefCell<Vec<NonNull<u8>>> = RefCell::new(Vec::with_capacity(16))
29}
30
31/// A high-performance bump allocator optimized for AST nodes.
32///
33/// The arena works by "bumping" a pointer within a pre-allocated chunk of memory.
34/// When a chunk is exhausted, a new one is requested from the thread-local pool
35/// or the global allocator.
36pub struct SyntaxArena {
37    /// Pointer to the next free byte in the current chunk.
38    /// Always kept 8-byte aligned.
39    ptr: UnsafeCell<NonNull<u8>>,
40    /// Pointer to the end of the current chunk (exclusive).
41    end: UnsafeCell<NonNull<u8>>,
42    /// List of full chunks allocated by this arena (excluding the current one).
43    /// Stored as (start_pointer, total_size).
44    full_chunks: UnsafeCell<Vec<(NonNull<u8>, usize)>>,
45    /// The start pointer of the current chunk (used for recycling/freeing).
46    current_chunk_start: UnsafeCell<NonNull<u8>>,
47    /// Store for token provenance metadata.
48    metadata: UnsafeCell<Vec<TokenProvenance>>,
49}
50
51impl SyntaxArena {
52    /// Creates a new empty arena.
53    ///
54    /// Initial pointers are set to dangling. The first allocation will trigger
55    /// a chunk allocation.
56    pub fn new(capacity: usize) -> Self {
57        // Use a pointer aligned to ALIGN even for the dangling state to satisfy debug assertions.
58        let dangling = unsafe { NonNull::new_unchecked(ALIGN as *mut u8) };
59        Self { ptr: UnsafeCell::new(dangling), end: UnsafeCell::new(dangling), full_chunks: UnsafeCell::new(Vec::with_capacity(capacity)), current_chunk_start: UnsafeCell::new(NonNull::dangling()), metadata: UnsafeCell::new(Vec::new()) }
60    }
61
62    /// Stores a token provenance in the arena and returns its index.
63    pub fn add_metadata(&self, provenance: TokenProvenance) -> std::num::NonZeroU32 {
64        let metadata = unsafe { &mut *self.metadata.get() };
65        metadata.push(provenance);
66        std::num::NonZeroU32::new(metadata.len() as u32).expect("Metadata index overflow")
67    }
68
69    /// Retrieves a token provenance by index.
70    pub fn get_metadata(&self, index: std::num::NonZeroU32) -> Option<&TokenProvenance> {
71        let metadata = unsafe { &*self.metadata.get() };
72        metadata.get(index.get() as usize - 1)
73    }
74
75    /// Allocates a value of type `T` in the arena and moves `value` into it.
76    ///
77    /// # Safety
78    ///
79    /// The caller must ensure that `T` is a POD (Plain Old Data) type.
80    /// The `Drop` implementation for `T` (if any) will **not** be called when
81    /// the arena is dropped.
82    ///
83    /// # Panics
84    ///
85    /// Panics if the allocation fails (OOM).
86    #[inline(always)]
87    pub fn alloc<T>(&self, value: T) -> &mut T {
88        let layout = Layout::new::<T>();
89        // Ensure the type's alignment requirement is within our 8-byte guarantee.
90        debug_assert!(layout.align() <= ALIGN);
91
92        unsafe {
93            let ptr = self.alloc_raw(layout.size());
94            let ptr = ptr.as_ptr() as *mut T;
95            // Write the value into the allocated space.
96            ptr.write(value);
97            &mut *ptr
98        }
99    }
100
101    /// Allocates a slice in the arena and copies the contents of `slice` into it.
102    ///
103    /// This is useful for storing strings or other contiguous data in the arena.
104    ///
105    /// # Safety
106    ///
107    /// Same as `alloc`, `T` must be `Copy` (and thus POD).
108    #[inline(always)]
109    pub fn alloc_slice_copy<T: Copy>(&self, slice: &[T]) -> &mut [T] {
110        if slice.is_empty() {
111            return &mut [];
112        }
113        let layout = Layout::for_value(slice);
114        debug_assert!(layout.align() <= ALIGN);
115
116        unsafe {
117            let ptr = self.alloc_raw(layout.size());
118            let ptr = ptr.as_ptr() as *mut T;
119            copy_nonoverlapping(slice.as_ptr(), ptr, slice.len());
120            std::slice::from_raw_parts_mut(ptr, slice.len())
121        }
122    }
123
124    /// Allocates a slice in the arena and fills it using an iterator.
125    ///
126    /// This is more efficient than collecting into a temporary `Vec` and then copying,
127    /// as it writes directly into the arena memory.
128    ///
129    /// # Safety
130    ///
131    /// The iterator must yield exactly `count` items. If it yields fewer, the remaining
132    /// memory will be uninitialized (UB if accessed). If it yields more, the extra
133    /// items are ignored. `T` must be POD.
134    #[inline(always)]
135    pub fn alloc_slice_fill_iter<T, I>(&self, count: usize, iter: I) -> &mut [T]
136    where
137        I: IntoIterator<Item = T>,
138    {
139        if count == 0 {
140            return &mut [];
141        }
142        let layout = Layout::array::<T>(count).unwrap();
143        debug_assert!(layout.align() <= ALIGN);
144
145        unsafe {
146            let ptr = self.alloc_raw(layout.size());
147            let base_ptr = ptr.as_ptr() as *mut T;
148
149            let mut i = 0;
150            for item in iter {
151                if i >= count {
152                    break;
153                }
154                base_ptr.add(i).write(item);
155                i += 1
156            }
157
158            // In a production-ready system, we should handle the case where iter is short.
159            // But for our internal use in deep_clone, we know the count is exact.
160            debug_assert_eq!(i, count, "Iterator yielded fewer items than expected");
161
162            std::slice::from_raw_parts_mut(base_ptr, count)
163        }
164    }
165
166    /// Internal raw allocation logic.
167    ///
168    /// Attempts to allocate `size` bytes from the current chunk.
169    /// If there is not enough space, it falls back to `alloc_slow`.
170    ///
171    /// # Safety
172    ///
173    /// `size` must be non-zero. The returned pointer is guaranteed to be 8-byte aligned.
174    #[inline(always)]
175    unsafe fn alloc_raw(&self, size: usize) -> NonNull<u8> {
176        // Unsafe block to wrap unsafe ops
177        unsafe {
178            let ptr = *self.ptr.get();
179            let end = *self.end.get();
180
181            // Calculate aligned pointer. Since we always maintain ALIGN (8) byte alignment
182            // for `self.ptr`, we only need to add the size and check against `end`.
183            let current_addr = ptr.as_ptr() as usize;
184
185            // Safety check: ensure the pointer is indeed aligned as we expect.
186            debug_assert!(current_addr % ALIGN == 0);
187
188            // We add `size` and then align up the result for the NEXT allocation.
189            let next_addr = (current_addr + size + ALIGN - 1) & !(ALIGN - 1);
190
191            if std::intrinsics::likely(next_addr <= end.as_ptr() as usize) {
192                *self.ptr.get() = NonNull::new_unchecked(next_addr as *mut u8);
193                return ptr;
194            }
195
196            self.alloc_slow(size)
197        }
198    }
199
200    /// Internal raw allocation logic for aligned memory.
201    #[inline(always)]
202    unsafe fn alloc_raw_aligned(&self, size: usize, align: usize) -> NonNull<u8> {
203        // Unsafe block to wrap unsafe ops
204        unsafe {
205            let ptr = *self.ptr.get();
206            let end = *self.end.get();
207
208            let current_addr = ptr.as_ptr() as usize;
209            // Align the current address to the requested alignment
210            let aligned_addr = (current_addr + align - 1) & !(align - 1);
211            let next_addr = aligned_addr + size;
212
213            if std::intrinsics::likely(next_addr <= end.as_ptr() as usize) {
214                *self.ptr.get() = NonNull::new_unchecked(next_addr as *mut u8);
215                return NonNull::new_unchecked(aligned_addr as *mut u8);
216            }
217
218            self.alloc_slow_aligned(size, align)
219        }
220    }
221
222    /// Slow path for aligned allocation when the current chunk is exhausted.
223    #[inline(never)]
224    unsafe fn alloc_slow_aligned(&self, size: usize, align: usize) -> NonNull<u8> {
225        unsafe {
226            // Retire current chunk if it exists.
227            let current_start = *self.current_chunk_start.get();
228            if current_start != NonNull::dangling() {
229                // We record the full size of the chunk so it can be correctly recycled.
230                // Note: for now we assume chunks are either CHUNK_SIZE or specially sized.
231                let current_end = (*self.end.get()).as_ptr() as usize;
232                let actual_size = current_end - current_start.as_ptr() as usize;
233                (*self.full_chunks.get()).push((current_start, actual_size))
234            }
235
236            // Allocate new chunk with the required alignment
237            let alloc_size = usize::max(size + align, CHUNK_SIZE);
238            let chunk_ptr = Self::alloc_chunk_aligned(alloc_size, align);
239
240            *self.current_chunk_start.get() = chunk_ptr;
241
242            let start_addr = chunk_ptr.as_ptr() as usize;
243            // Resulting pointer is the start of the new chunk (already aligned)
244            let result_ptr = NonNull::new_unchecked(start_addr as *mut u8);
245
246            // Calculate the next free pointer
247            let next_free = start_addr + size;
248
249            *self.ptr.get() = NonNull::new_unchecked(next_free as *mut u8);
250            *self.end.get() = NonNull::new_unchecked((start_addr + alloc_size) as *mut u8);
251
252            result_ptr
253        }
254    }
255
256    /// Allocates a new aligned memory chunk from the thread-local pool or global allocator.
257    unsafe fn alloc_chunk_aligned(size: usize, align: usize) -> NonNull<u8> {
258        // For aligned allocations, we always go to the global allocator
259        // since aligned chunks are less likely to be reused for different alignments
260        let layout = Layout::from_size_align(size, align).unwrap();
261        let ptr = alloc(layout);
262        if ptr.is_null() {
263            std::alloc::handle_alloc_error(layout)
264        }
265        NonNull::new_unchecked(ptr)
266    }
267
268    /// Slow path for allocation when the current chunk is exhausted.
269    ///
270    /// 1. Pushes the current chunk to `full_chunks`.
271    /// 2. Allocates a new chunk (either standard 64KB or larger if `size` requires it).
272    /// 3. Sets the new chunk as the current one.
273    #[inline(never)]
274    unsafe fn alloc_slow(&self, size: usize) -> NonNull<u8> {
275        unsafe {
276            // Retire current chunk if it exists.
277            let current_start = *self.current_chunk_start.get();
278            if current_start != NonNull::dangling() {
279                // We record the full size of the chunk so it can be correctly recycled.
280                // Note: for now we assume chunks are either CHUNK_SIZE or specially sized.
281                let current_end = (*self.end.get()).as_ptr() as usize;
282                let actual_size = current_end - current_start.as_ptr() as usize;
283                (*self.full_chunks.get()).push((current_start, actual_size))
284            }
285
286            // Allocate new chunk.
287            // If request is huge (> CHUNK_SIZE), we allocate a larger chunk specifically for it.
288            // These "huge chunks" are NOT recycled into the pool to avoid wasting space.
289            let alloc_size = usize::max(size + ALIGN, CHUNK_SIZE);
290
291            let chunk_ptr = Self::alloc_chunk(alloc_size);
292
293            *self.current_chunk_start.get() = chunk_ptr;
294
295            let start_addr = chunk_ptr.as_ptr() as usize;
296            // Resulting pointer is the start of the new chunk.
297            let result_ptr = NonNull::new_unchecked(start_addr as *mut u8);
298
299            // Calculate the next free pointer, aligned to ALIGN.
300            let next_free = (start_addr + size + ALIGN - 1) & !(ALIGN - 1);
301
302            *self.ptr.get() = NonNull::new_unchecked(next_free as *mut u8);
303            *self.end.get() = NonNull::new_unchecked((start_addr + alloc_size) as *mut u8);
304
305            result_ptr
306        }
307    }
308
309    /// Allocates a new memory chunk from the thread-local pool or global allocator.
310    unsafe fn alloc_chunk(size: usize) -> NonNull<u8> {
311        // Try to get from pool if size matches the standard chunk size.
312        if size == CHUNK_SIZE {
313            let ptr = CHUNK_POOL.try_with(|pool| pool.borrow_mut().pop());
314
315            if let Ok(Some(ptr)) = ptr {
316                return ptr;
317            }
318        }
319
320        let layout = Layout::from_size_align(size, ALIGN).unwrap();
321        // unsafe block for alloc
322        unsafe {
323            let ptr = alloc(layout);
324            if ptr.is_null() {
325                std::alloc::handle_alloc_error(layout)
326            }
327            NonNull::new_unchecked(ptr)
328        }
329    }
330}
331
332impl Drop for SyntaxArena {
333    /// Drops the arena, recycling all its chunks back to the thread-local pool or freeing them.
334    fn drop(&mut self) {
335        unsafe {
336            // Recycle the current chunk.
337            let current = *self.current_chunk_start.get();
338            if current != NonNull::dangling() {
339                let current_end = (*self.end.get()).as_ptr() as usize;
340                let actual_size = current_end - current.as_ptr() as usize;
341                Self::recycle_chunk(current, actual_size)
342            }
343
344            // Recycle all full chunks.
345            for (ptr, size) in (*self.full_chunks.get()).iter() {
346                Self::recycle_chunk(*ptr, *size)
347            }
348        }
349    }
350}
351
352impl SyntaxArena {
353    /// Returns a chunk to the thread-local pool or deallocates it if the pool is full.
354    ///
355    /// # Safety
356    ///
357    /// `ptr` must have been allocated with `ALIGN` and its size must be `size`.
358    unsafe fn recycle_chunk(ptr: NonNull<u8>, size: usize) {
359        if size == CHUNK_SIZE {
360            // Only pool standard-sized chunks to maintain predictability.
361            let _ = CHUNK_POOL
362                .try_with(|pool| {
363                    let mut pool = pool.borrow_mut();
364                    if pool.len() < 64 {
365                        // Hard limit to prevent memory bloating per thread.
366                        pool.push(ptr)
367                    }
368                    else {
369                        // If pool is full, deallocate the chunk
370                        unsafe {
371                            let layout = Layout::from_size_align(size, ALIGN).unwrap();
372                            dealloc(ptr.as_ptr(), layout);
373                        }
374                    }
375                })
376                .unwrap_or_else(|_| {
377                    // If try_with fails (e.g. during thread destruction), deallocate the chunk
378                    unsafe {
379                        let layout = Layout::from_size_align(size, ALIGN).unwrap();
380                        dealloc(ptr.as_ptr(), layout);
381                    }
382                });
383            return;
384        }
385        // If not pooled (either because it's a huge chunk or the pool is full/unreachable), deallocate immediately.
386        let layout = Layout::from_size_align(size, ALIGN).unwrap();
387        unsafe { dealloc(ptr.as_ptr(), layout) }
388    }
389}
390
391unsafe impl Send for SyntaxArena {}
392unsafe impl Sync for SyntaxArena {}
393
394impl Default for SyntaxArena {
395    fn default() -> Self {
396        Self::new(16)
397    }
398}