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 /// Slow path for allocation when the current chunk is exhausted.
201 ///
202 /// 1. Pushes the current chunk to `full_chunks`.
203 /// 2. Allocates a new chunk (either standard 64KB or larger if `size` requires it).
204 /// 3. Sets the new chunk as the current one.
205 #[inline(never)]
206 unsafe fn alloc_slow(&self, size: usize) -> NonNull<u8> {
207 unsafe {
208 // Retire current chunk if it exists.
209 let current_start = *self.current_chunk_start.get();
210 if current_start != NonNull::dangling() {
211 // We record the full size of the chunk so it can be correctly recycled.
212 // Note: for now we assume chunks are either CHUNK_SIZE or specially sized.
213 let current_end = (*self.end.get()).as_ptr() as usize;
214 let actual_size = current_end - current_start.as_ptr() as usize;
215 (*self.full_chunks.get()).push((current_start, actual_size))
216 }
217
218 // Allocate new chunk.
219 // If request is huge (> CHUNK_SIZE), we allocate a larger chunk specifically for it.
220 // These "huge chunks" are NOT recycled into the pool to avoid wasting space.
221 let alloc_size = usize::max(size + ALIGN, CHUNK_SIZE);
222
223 let chunk_ptr = Self::alloc_chunk(alloc_size);
224
225 *self.current_chunk_start.get() = chunk_ptr;
226
227 let start_addr = chunk_ptr.as_ptr() as usize;
228 // Resulting pointer is the start of the new chunk.
229 let result_ptr = NonNull::new_unchecked(start_addr as *mut u8);
230
231 // Calculate the next free pointer, aligned to ALIGN.
232 let next_free = (start_addr + size + ALIGN - 1) & !(ALIGN - 1);
233
234 *self.ptr.get() = NonNull::new_unchecked(next_free as *mut u8);
235 *self.end.get() = NonNull::new_unchecked((start_addr + alloc_size) as *mut u8);
236
237 result_ptr
238 }
239 }
240
241 /// Allocates a new memory chunk from the thread-local pool or global allocator.
242 unsafe fn alloc_chunk(size: usize) -> NonNull<u8> {
243 // Try to get from pool if size matches the standard chunk size.
244 if size == CHUNK_SIZE {
245 let ptr = CHUNK_POOL.try_with(|pool| pool.borrow_mut().pop());
246
247 if let Ok(Some(ptr)) = ptr {
248 return ptr;
249 }
250 }
251
252 let layout = Layout::from_size_align(size, ALIGN).unwrap();
253 // unsafe block for alloc
254 unsafe {
255 let ptr = alloc(layout);
256 if ptr.is_null() {
257 std::alloc::handle_alloc_error(layout)
258 }
259 NonNull::new_unchecked(ptr)
260 }
261 }
262}
263
264impl Drop for SyntaxArena {
265 /// Drops the arena, recycling all its chunks back to the thread-local pool or freeing them.
266 fn drop(&mut self) {
267 unsafe {
268 // Recycle the current chunk.
269 let current = *self.current_chunk_start.get();
270 if current != NonNull::dangling() {
271 let current_end = (*self.end.get()).as_ptr() as usize;
272 let actual_size = current_end - current.as_ptr() as usize;
273 Self::recycle_chunk(current, actual_size)
274 }
275
276 // Recycle all full chunks.
277 for (ptr, size) in (*self.full_chunks.get()).iter() {
278 Self::recycle_chunk(*ptr, *size)
279 }
280 }
281 }
282}
283
284impl SyntaxArena {
285 /// Returns a chunk to the thread-local pool or deallocates it if the pool is full.
286 ///
287 /// # Safety
288 ///
289 /// `ptr` must have been allocated with `ALIGN` and its size must be `size`.
290 unsafe fn recycle_chunk(ptr: NonNull<u8>, size: usize) {
291 if size == CHUNK_SIZE {
292 // Only pool standard-sized chunks to maintain predictability.
293 let _ = CHUNK_POOL.try_with(|pool| {
294 let mut pool = pool.borrow_mut();
295 if pool.len() < 64 {
296 // Hard limit to prevent memory bloating per thread.
297 pool.push(ptr)
298 }
299 });
300 // If try_with fails (e.g. during thread destruction), we just leak or dealloc?
301 // Since we can't access pool, we should dealloc.
302 // But try_with error usually means TLS is gone.
303 // We can check error kind. For simplicity, we just fallback to dealloc if pool is unreachable.
304 return;
305 }
306 // If not pooled (either because it's a huge chunk or the pool is full/unreachable), deallocate immediately.
307 let layout = Layout::from_size_align(size, ALIGN).unwrap();
308 unsafe { dealloc(ptr.as_ptr(), layout) }
309 }
310}
311
312unsafe impl Send for SyntaxArena {}
313unsafe impl Sync for SyntaxArena {}
314
315impl Default for SyntaxArena {
316 fn default() -> Self {
317 Self::new(16)
318 }
319}