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}