keleusma_arena/lib.rs
1#![doc = include_str!("../README.md")]
2//!
3//! # API Reference
4//!
5//! ## Construction
6//!
7//! - [`Arena::with_capacity`]. Heap-backed. Requires the `alloc` feature.
8//! - [`Arena::from_static_buffer`]. Borrows a `&'static mut [u8]`. Safe.
9//! - [`Arena::from_buffer_unchecked`]. Raw pointer and length. Unsafe.
10//!
11//! ## Allocation
12//!
13//! [`BottomHandle`] and [`TopHandle`] borrow the arena and implement
14//! `allocator_api2::alloc::Allocator`. Pass them to `Vec::new_in` and
15//! similar constructors for arena-backed collections. The bottom end
16//! starts at offset zero and grows upward. The top end starts at the
17//! buffer's high address and grows downward. The arena imposes no
18//! semantic distinction between the two ends.
19//!
20//! Code that prefers a CPU-memory mental model may use the method
21//! aliases [`Arena::stack_handle`] and [`Arena::heap_handle`], which
22//! return the same `BottomHandle` and `TopHandle` types under
23//! conventional names.
24//!
25//! Aligned allocations go through the `Allocator` trait with a
26//! `Layout` that carries the desired alignment. Alignment is computed
27//! against the actual buffer base address, so any base alignment is
28//! supported. Unaligned byte allocations have direct convenience
29//! methods [`Arena::alloc_bottom_bytes`] and [`Arena::alloc_top_bytes`]
30//! that allocate `n` bytes without padding for alignment. Use the
31//! aligned form for typed values and pointers. Use the byte form for
32//! packed byte buffers.
33//!
34//! ## Reset, Rewind, and Marks
35//!
36//! [`Arena::reset`] takes `&mut self` and clears both ends safely. Each
37//! end also exposes a LIFO mark and rewind discipline. The mark
38//! accessors [`Arena::bottom_mark`] and [`Arena::top_mark`] are safe.
39//! The rewind and per-end reset operations [`Arena::rewind_bottom`],
40//! [`Arena::rewind_top`], [`Arena::reset_bottom`], and
41//! [`Arena::reset_top`] are unsafe because they invalidate the rewound
42//! region while raw pointers obtained through the `Allocator` trait may
43//! still be held by the caller.
44//!
45//! ## Observability and Budget
46//!
47//! [`Arena::bottom_peak`] and [`Arena::top_peak`] track high watermarks
48//! since arena creation or the most recent [`Arena::clear_peaks`].
49//! [`Arena::bottom_used`], [`Arena::top_used`], [`Arena::free`], and
50//! [`Arena::capacity`] report current state.
51//!
52//! [`Budget`] is a generic memory budget structure. Producers compute a
53//! budget through any analysis they choose. [`Arena::fits_budget`]
54//! checks whether the budget is admissible against the arena's capacity.
55//!
56//! ## Thread Safety
57//!
58//! Not thread-safe. Interior mutability uses `Cell<usize>` rather than
59//! atomic primitives. The arena is designed for scoped per-thread use
60//! through the `Allocator` trait. Setting it as the program's
61//! `#[global_allocator]` requires a thread-safe wrapper that this crate
62//! does not provide.
63
64#![no_std]
65#![cfg_attr(docsrs, feature(doc_cfg))]
66
67#[cfg(feature = "alloc")]
68extern crate alloc;
69
70use core::alloc::Layout;
71use core::cell::Cell;
72use core::ptr::NonNull;
73
74use allocator_api2::alloc::{AllocError, Allocator};
75
76/// A worst-case memory usage budget.
77///
78/// A producer-agnostic structure describing a worst-case stack and heap
79/// memory bound. The arena's [`Arena::fits_budget`] method checks whether
80/// the budget is admissible against the arena's capacity. The two bounds
81/// must be non-overlapping in any single state of the arena, but they
82/// represent peak usage of the two ends and so must sum within the
83/// arena's capacity.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub struct Budget {
86 /// Maximum bytes consumed at the bottom end.
87 pub bottom_bytes: usize,
88 /// Maximum bytes consumed at the top end.
89 pub top_bytes: usize,
90}
91
92impl Budget {
93 /// Construct a budget with the given bottom and top bounds.
94 pub const fn new(bottom_bytes: usize, top_bytes: usize) -> Self {
95 Self {
96 bottom_bytes,
97 top_bytes,
98 }
99 }
100
101 /// Total bytes required by this budget. Saturates at `usize::MAX` on
102 /// overflow so that an oversized budget does not silently wrap.
103 pub const fn total(&self) -> usize {
104 self.bottom_bytes.saturating_add(self.top_bytes)
105 }
106}
107
108/// A mark for the bottom end of an arena.
109///
110/// Returned by [`Arena::bottom_mark`]. Pass back to
111/// [`Arena::rewind_bottom`] to restore the bottom pointer to this
112/// position. Marks are tied to the arena that produced them; passing a
113/// mark to a different arena is a logic error and produces undefined
114/// behavior under the unsafe rewind contract.
115#[derive(Debug, Clone, Copy)]
116pub struct BottomMark(usize);
117
118/// A mark for the top end of an arena.
119///
120/// Returned by [`Arena::top_mark`]. Pass back to [`Arena::rewind_top`]
121/// to restore the top pointer to this position.
122#[derive(Debug, Clone, Copy)]
123pub struct TopMark(usize);
124
125/// Storage backing variants for an arena.
126///
127/// The arena holds the raw pointer and capacity directly in the
128/// `buffer` and `capacity` fields. The variant tracks ownership for
129/// the explicit `Drop` impl on `Arena`. Owned arenas reconstruct the
130/// `Box` via `Box::from_raw` and let it drop, releasing the buffer.
131/// External arenas leave the buffer untouched; the caller owns the
132/// storage.
133///
134/// Using a raw pointer rather than holding the `Box` directly gives
135/// the buffer "raw" provenance from the perspective of the borrow
136/// checker and miri's aliasing models. This is necessary because
137/// allocations through `BottomHandle` and `TopHandle` derive write
138/// pointers into the buffer through a shared `&Arena`; deriving
139/// through a unique-reference ancestor would make subsequent
140/// derivations from the same source aliasing-unsound under both
141/// stacked borrows and tree borrows.
142#[derive(Clone, Copy)]
143enum Storage {
144 /// Externally owned buffer. The caller is responsible for keeping
145 /// the buffer alive for the arena's lifetime.
146 External,
147 /// Owned buffer allocated through the global allocator. The arena
148 /// reconstructs the `Box` and drops it on its own `Drop`.
149 #[cfg(feature = "alloc")]
150 Owned,
151}
152
153/// A dual-end bump-allocated arena.
154///
155/// Owns or borrows a fixed-size buffer of bytes. Two bump pointers track
156/// allocation positions at each end. The bottom end grows from low
157/// addresses upward. The top end grows from high addresses downward.
158/// Allocation fails when the two pointers would meet.
159///
160/// The arena is not the program's `#[global_allocator]` and is not
161/// intended to be one. It is designed for scoped per-region or
162/// per-thread use through `BottomHandle` and `TopHandle`, which the
163/// host passes to allocator-aware collection constructors. The standard
164/// global allocator continues to handle every allocation that does not
165/// route through an arena handle. Hosts that want every allocation in
166/// the program to be arena-backed must wrap the arena in a thread-safe
167/// allocator and install it via `#[global_allocator]`; this crate does
168/// not provide such a wrapper because doing so well requires choices
169/// that depend on the host's threading and synchronization model.
170///
171/// ## Generations and stale-pointer detection
172///
173/// The arena carries an `epoch` counter that increments on [`Arena::reset`].
174/// The `ArenaHandle` family of safe wrappers captures the epoch at
175/// construction and validates it on access, returning [`Stale`] if the
176/// arena has been reset since the handle was issued. The counter is
177/// `u64` and uses checked arithmetic. A saturated counter halts the
178/// arena's reset path with [`EpochSaturated`]. Saturation requires
179/// roughly five hundred eighty four thousand years at one reset per
180/// microsecond and is documentation rather than a real failure mode in
181/// expected use.
182///
183/// In-process recovery from saturation is possible through
184/// [`Arena::force_reset_epoch`], which is unsafe and requires the
185/// caller to certify that no `ArenaHandle` from any prior epoch is
186/// reachable. Cross-process recovery for very long-lived deployments
187/// uses checkpoint and restart against host-owned non-volatile storage.
188/// `ArenaHandle` is intentionally not serializable because its pointer
189/// is not stable across processes.
190///
191/// See the crate-level documentation for the design overview.
192pub struct Arena {
193 /// Pointer to the start of the backing buffer. Stable for the
194 /// arena's lifetime.
195 buffer: NonNull<u8>,
196 /// Total capacity of the buffer in bytes.
197 capacity: usize,
198 /// Current bottom pointer. Allocations from the bottom end consume
199 /// the range `[0, bottom_top)`.
200 bottom_top: Cell<usize>,
201 /// Current top pointer. Allocations from the top end consume the
202 /// range `[top_top, capacity)`.
203 top_top: Cell<usize>,
204 /// Peak observed value of `bottom_top`. Watermark for sizing
205 /// analysis.
206 bottom_peak: Cell<usize>,
207 /// Lowest observed value of `top_top`. Combined with `capacity`
208 /// gives the peak top usage.
209 top_peak_low: Cell<usize>,
210 /// Generation counter. Incremented on [`Arena::reset`]. Captured by
211 /// [`ArenaHandle`] values and validated on access for stale-pointer
212 /// detection. Saturates at `u64::MAX`, at which point further
213 /// resets fail with [`EpochSaturated`] until the caller invokes
214 /// [`Arena::force_reset_epoch`].
215 epoch: Cell<u64>,
216 /// Storage discriminator. The field is read implicitly via `Drop`.
217 #[allow(dead_code)]
218 storage: Storage,
219}
220
221/// Hard halt error returned by [`Arena::reset`] when the epoch counter
222/// would saturate.
223///
224/// Saturation requires roughly five hundred eighty four thousand years
225/// at one reset per microsecond, but explicit refusal at saturation is
226/// the correct posture for safety-critical use. Recovery is via
227/// [`Arena::force_reset_epoch`].
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub struct EpochSaturated;
230
231/// Error returned by [`ArenaHandle::get`] when the arena has been
232/// reset since the handle was issued.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234pub struct Stale;
235
236// SAFETY: The arena uses `Cell` for interior mutability of the bump
237// pointers and peaks. `Cell` is `Send` but not `Sync`. The arena itself
238// is not `Sync` for the same reason.
239
240impl Arena {
241 /// Create an arena backed by a freshly allocated heap buffer of the
242 /// given byte capacity.
243 ///
244 /// Available only with the `alloc` feature. The buffer is zeroed at
245 /// construction and is allocated with 16-byte alignment, which
246 /// covers the alignment requirements of `i64`, `f64`, `u128`, and
247 /// most platform-native pointers and primitives.
248 ///
249 /// Panics on allocation failure via the standard `handle_alloc_error`
250 /// path. A capacity of zero produces an arena that satisfies
251 /// allocation requests for zero-size layouts only; non-zero
252 /// allocations return `AllocError`.
253 ///
254 /// # Examples
255 ///
256 /// ```
257 /// use allocator_api2::vec::Vec as ArenaVec;
258 /// use keleusma_arena::Arena;
259 ///
260 /// let arena = Arena::with_capacity(1024);
261 /// let mut v: ArenaVec<i64, _> = ArenaVec::new_in(arena.stack_handle());
262 /// v.push(1);
263 /// v.push(2);
264 /// v.push(3);
265 /// assert_eq!(v.len(), 3);
266 /// assert!(arena.bottom_used() >= 24);
267 /// ```
268 #[cfg(feature = "alloc")]
269 #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
270 pub fn with_capacity(capacity: usize) -> Self {
271 use alloc::alloc::{Layout as AllocLayout, alloc_zeroed, handle_alloc_error};
272
273 let buffer = if capacity == 0 {
274 NonNull::<u8>::dangling()
275 } else {
276 // Allocate a 16-byte-aligned buffer. The alignment covers
277 // every standard primitive type and gives the arena
278 // predictable behavior across allocators that may otherwise
279 // return only minimally-aligned memory for byte allocations.
280 let layout = AllocLayout::from_size_align(capacity, 16).expect("invalid arena layout");
281 // SAFETY: `layout` has non-zero size because `capacity > 0`.
282 let raw = unsafe { alloc_zeroed(layout) };
283 if raw.is_null() {
284 handle_alloc_error(layout);
285 }
286 // SAFETY: `alloc_zeroed` returned non-null on success.
287 unsafe { NonNull::new_unchecked(raw) }
288 };
289 Self {
290 buffer,
291 capacity,
292 bottom_top: Cell::new(0),
293 top_top: Cell::new(capacity),
294 bottom_peak: Cell::new(0),
295 top_peak_low: Cell::new(capacity),
296 epoch: Cell::new(0),
297 storage: Storage::Owned,
298 }
299 }
300
301 /// Create an arena backed by a static buffer.
302 ///
303 /// The buffer must outlive the arena. The `'static mut` requirement
304 /// satisfies this for typical embedded patterns where the buffer is
305 /// a static array placed in BSS or DATA. For shorter-lived buffers,
306 /// see [`Arena::from_buffer_unchecked`].
307 pub fn from_static_buffer(buffer: &'static mut [u8]) -> Self {
308 let capacity = buffer.len();
309 // SAFETY: `&'static mut [u8]` is non-null and lives for the
310 // duration of the program.
311 let ptr = unsafe { NonNull::new_unchecked(buffer.as_mut_ptr()) };
312 Self {
313 buffer: ptr,
314 capacity,
315 bottom_top: Cell::new(0),
316 top_top: Cell::new(capacity),
317 bottom_peak: Cell::new(0),
318 top_peak_low: Cell::new(capacity),
319 epoch: Cell::new(0),
320 storage: Storage::External,
321 }
322 }
323
324 /// Create an arena from a raw pointer and length.
325 ///
326 /// The buffer's base alignment does not need to match the alignment
327 /// of any particular allocation type. The arena computes alignment
328 /// against the actual buffer base address and pads as needed for
329 /// each aligned allocation.
330 ///
331 /// # Safety
332 ///
333 /// The caller must uphold the following.
334 ///
335 /// - `ptr` is non-null.
336 /// - `ptr` is valid for reads and writes of `capacity` bytes for the
337 /// entire lifetime of the returned arena.
338 /// - No other code accesses the buffer through any path that would
339 /// alias with the arena's allocations during the arena's lifetime.
340 ///
341 /// This constructor is the only path that admits buffers with
342 /// non-`'static` lifetimes. It exists for embedded contexts where
343 /// the lifetime is known statically through other means but the
344 /// type system cannot express it. Most callers should prefer
345 /// [`Arena::from_static_buffer`].
346 pub unsafe fn from_buffer_unchecked(ptr: *mut u8, capacity: usize) -> Self {
347 // SAFETY: Caller asserts non-null and validity. `NonNull::new_unchecked`
348 // is sound under the caller's assertion.
349 let buffer = unsafe { NonNull::new_unchecked(ptr) };
350 Self {
351 buffer,
352 capacity,
353 bottom_top: Cell::new(0),
354 top_top: Cell::new(capacity),
355 bottom_peak: Cell::new(0),
356 top_peak_low: Cell::new(capacity),
357 epoch: Cell::new(0),
358 storage: Storage::External,
359 }
360 }
361
362 /// Total capacity of the arena in bytes.
363 pub fn capacity(&self) -> usize {
364 self.capacity
365 }
366
367 /// Bytes currently allocated from the bottom end.
368 pub fn bottom_used(&self) -> usize {
369 self.bottom_top.get()
370 }
371
372 /// Bytes currently allocated from the top end.
373 pub fn top_used(&self) -> usize {
374 self.capacity - self.top_top.get()
375 }
376
377 /// Bytes available for either end to consume.
378 pub fn free(&self) -> usize {
379 self.top_top.get().saturating_sub(self.bottom_top.get())
380 }
381
382 /// Highest observed bottom usage in bytes since arena creation or
383 /// the most recent [`Arena::clear_peaks`] call.
384 pub fn bottom_peak(&self) -> usize {
385 self.bottom_peak.get()
386 }
387
388 /// Highest observed top usage in bytes since arena creation or the
389 /// most recent [`Arena::clear_peaks`] call.
390 pub fn top_peak(&self) -> usize {
391 self.capacity - self.top_peak_low.get()
392 }
393
394 /// Return a snapshot of the bottom-end bump pointer for later use
395 /// with [`Arena::rewind_bottom`].
396 pub fn bottom_mark(&self) -> BottomMark {
397 BottomMark(self.bottom_top.get())
398 }
399
400 /// Return a snapshot of the top-end bump pointer for later use with
401 /// [`Arena::rewind_top`].
402 pub fn top_mark(&self) -> TopMark {
403 TopMark(self.top_top.get())
404 }
405
406 /// Reset both ends, reclaiming all allocations.
407 ///
408 /// Constant-time. Does not zero the buffer contents because
409 /// subsequent allocations will overwrite as needed. Does not clear
410 /// peak watermarks; use [`Arena::clear_peaks`] for that.
411 ///
412 /// Advances the epoch counter, invalidating every outstanding
413 /// [`ArenaHandle`]. Returns [`EpochSaturated`] if the counter is
414 /// already at `u64::MAX`. See [`Arena::force_reset_epoch`] for
415 /// recovery.
416 ///
417 /// Takes `&mut self` so the borrow checker prevents calling reset
418 /// while any handle borrows the arena. This guarantees no live
419 /// allocations through `Allocator` trait users at the moment of
420 /// reset.
421 pub fn reset(&mut self) -> Result<(), EpochSaturated> {
422 let next = self.epoch.get().checked_add(1).ok_or(EpochSaturated)?;
423 self.bottom_top.set(0);
424 self.top_top.set(self.capacity);
425 self.epoch.set(next);
426 Ok(())
427 }
428
429 /// Reset both ends and advance the epoch through a shared reference.
430 ///
431 /// Companion to [`Arena::reset`] for callers that hold the arena
432 /// through a shared reference and cannot temporarily acquire
433 /// exclusive access. The interior-mutable bump pointers and epoch
434 /// counter make the implementation race-free for single-threaded
435 /// use.
436 ///
437 /// # Safety
438 ///
439 /// The caller must certify that no allocator-bound collection
440 /// holds storage in the arena at the moment of reset. Concretely,
441 /// no `allocator_api2::vec::Vec<T, BottomHandle>` or
442 /// `allocator_api2::vec::Vec<T, TopHandle>` value may have non-zero
443 /// capacity when this is called. Outstanding [`ArenaHandle`] values
444 /// are correctly invalidated by the epoch advance and remain safe.
445 ///
446 /// Returns [`EpochSaturated`] when the epoch counter is at
447 /// `u64::MAX`. Recovery is via [`Arena::force_reset_epoch`].
448 pub unsafe fn reset_unchecked(&self) -> Result<(), EpochSaturated> {
449 let next = self.epoch.get().checked_add(1).ok_or(EpochSaturated)?;
450 self.bottom_top.set(0);
451 self.top_top.set(self.capacity);
452 self.epoch.set(next);
453 Ok(())
454 }
455
456 /// Reset the top end and advance the epoch through a shared
457 /// reference, leaving the bottom end untouched.
458 ///
459 /// Intended for hosts that use the bottom end for long-lived
460 /// allocator-bound collections (such as an operand stack) while
461 /// using the top end for short-lived scratch (such as dynamic
462 /// strings). The epoch advance invalidates every outstanding
463 /// [`ArenaHandle`] regardless of which end produced it. This is
464 /// the desired discipline because handles do not record which end
465 /// they came from and any handle that survives a reset is by
466 /// definition stale.
467 ///
468 /// # Safety
469 ///
470 /// The caller must certify that no allocator-bound collection
471 /// holds storage in the top end at the moment of reset. Bottom-end
472 /// allocator-bound collections are unaffected by this call and
473 /// retain their storage. Outstanding [`ArenaHandle`] values are
474 /// correctly invalidated by the epoch advance and remain safe.
475 ///
476 /// Returns [`EpochSaturated`] when the epoch counter is at
477 /// `u64::MAX`. Recovery is via [`Arena::force_reset_epoch`].
478 pub unsafe fn reset_top_unchecked(&self) -> Result<(), EpochSaturated> {
479 let next = self.epoch.get().checked_add(1).ok_or(EpochSaturated)?;
480 self.top_top.set(self.capacity);
481 self.epoch.set(next);
482 Ok(())
483 }
484
485 /// Current epoch counter value.
486 ///
487 /// Captured by [`ArenaHandle`] at construction and compared on
488 /// access. Hosts performing long-running missions may consult this
489 /// alongside [`Arena::epoch_remaining`] to schedule a graceful
490 /// restart well before saturation.
491 pub fn epoch(&self) -> u64 {
492 self.epoch.get()
493 }
494
495 /// Number of resets remaining before the epoch counter saturates.
496 pub fn epoch_remaining(&self) -> u64 {
497 u64::MAX - self.epoch.get()
498 }
499
500 /// Reset the epoch counter to zero.
501 ///
502 /// Recovery path for [`EpochSaturated`]. Resets bump pointers as
503 /// well so the arena is in the same observable state as a freshly
504 /// constructed arena, except for retained capacity.
505 ///
506 /// # Safety
507 ///
508 /// The caller must certify that no [`ArenaHandle`] produced under
509 /// any prior epoch is reachable. Calling this while such handles
510 /// exist invalidates the stale-detection guarantee and may permit
511 /// use after invalidation that the type system would otherwise
512 /// catch through epoch comparison.
513 ///
514 /// The intended use is recovery after a [`Arena::reset`] call has
515 /// returned [`EpochSaturated`]. The host halts every consumer of
516 /// the arena, drains every cache that holds an [`ArenaHandle`],
517 /// and only then invokes this method.
518 pub unsafe fn force_reset_epoch(&mut self) {
519 self.bottom_top.set(0);
520 self.top_top.set(self.capacity);
521 self.epoch.set(0);
522 }
523
524 /// Clear the peak watermarks for both ends.
525 ///
526 /// Sets each peak to the current pointer value. After this call,
527 /// peak readings reflect only allocations made after the call.
528 pub fn clear_peaks(&mut self) {
529 self.bottom_peak.set(self.bottom_top.get());
530 self.top_peak_low.set(self.top_top.get());
531 }
532
533 /// Rewind the bottom end to a previously recorded mark.
534 ///
535 /// # Safety
536 ///
537 /// The caller must ensure that no live values reference memory in
538 /// the range `[mark.0, current_bottom_top)`. References obtained
539 /// through the `Allocator` trait, including those held by
540 /// `allocator_api2::vec::Vec` and similar collections, must be
541 /// dropped or otherwise abandoned before this call. Subsequent
542 /// allocations may overwrite the rewound region, which would alias
543 /// with any retained reference and produce undefined behavior.
544 ///
545 /// Marks from a different arena are a logic error.
546 pub unsafe fn rewind_bottom(&self, mark: BottomMark) {
547 let target = mark.0.min(self.bottom_top.get());
548 self.bottom_top.set(target);
549 }
550
551 /// Rewind the top end to a previously recorded mark.
552 ///
553 /// # Safety
554 ///
555 /// Same contract as [`Arena::rewind_bottom`].
556 pub unsafe fn rewind_top(&self, mark: TopMark) {
557 let target = mark.0.max(self.top_top.get());
558 self.top_top.set(target);
559 }
560
561 /// Clear the bottom end without checking for live references.
562 ///
563 /// # Safety
564 ///
565 /// The caller must ensure no live references into the bottom region
566 /// exist. Equivalent to [`Arena::rewind_bottom`] with a mark of
567 /// zero, with the same safety contract.
568 pub unsafe fn reset_bottom(&self) {
569 self.bottom_top.set(0);
570 }
571
572 /// Clear the top end without checking for live references.
573 ///
574 /// # Safety
575 ///
576 /// The caller must ensure no live references into the top region
577 /// exist. Equivalent to [`Arena::rewind_top`] with a mark of
578 /// `capacity`, with the same safety contract.
579 pub unsafe fn reset_top(&self) {
580 self.top_top.set(self.capacity);
581 }
582
583 /// Returns true if the given budget fits within the arena's
584 /// capacity. The check is `budget.bottom_bytes + budget.top_bytes
585 /// <= capacity`.
586 ///
587 /// This is the generic budget contract referenced in the crate
588 /// documentation. Producers compute a budget through whatever
589 /// analysis they choose and use this method to verify admissibility
590 /// before relying on the arena.
591 pub fn fits_budget(&self, budget: &Budget) -> bool {
592 budget.total() <= self.capacity
593 }
594
595 /// Obtain a bottom-end allocation handle.
596 pub fn bottom_handle(&self) -> BottomHandle<'_> {
597 BottomHandle(self)
598 }
599
600 /// Obtain a top-end allocation handle.
601 pub fn top_handle(&self) -> TopHandle<'_> {
602 TopHandle(self)
603 }
604
605 /// Alias for [`Arena::bottom_handle`]. Suitable for code that
606 /// treats the bottom end as a stack-like region.
607 pub fn stack_handle(&self) -> BottomHandle<'_> {
608 self.bottom_handle()
609 }
610
611 /// Alias for [`Arena::top_handle`]. Suitable for code that treats
612 /// the top end as a heap-like region whose allocations are reset
613 /// together rather than freed individually.
614 pub fn heap_handle(&self) -> TopHandle<'_> {
615 self.top_handle()
616 }
617
618 /// Allocate `n` bytes from the bottom end with no alignment
619 /// requirement. Convenience wrapper for byte buffers and similar
620 /// allocations where the caller does not care about alignment.
621 ///
622 /// Equivalent to allocating with a `Layout::from_size_align(n, 1)`
623 /// through the `BottomHandle` Allocator implementation.
624 pub fn alloc_bottom_bytes(&self, n: usize) -> Result<NonNull<[u8]>, AllocError> {
625 let layout = Layout::from_size_align(n, 1).map_err(|_| AllocError)?;
626 self.alloc_bottom(layout)
627 }
628
629 /// Allocate `n` bytes from the top end with no alignment requirement.
630 pub fn alloc_top_bytes(&self, n: usize) -> Result<NonNull<[u8]>, AllocError> {
631 let layout = Layout::from_size_align(n, 1).map_err(|_| AllocError)?;
632 self.alloc_top(layout)
633 }
634
635 /// Allocate from the bottom end.
636 ///
637 /// Alignment is computed against the actual buffer base address, not
638 /// the offset within the buffer. This makes the arena correct for
639 /// buffers with any base alignment, including buffers obtained from
640 /// allocators that only guarantee one-byte alignment and static
641 /// arrays declared without explicit alignment annotations.
642 fn alloc_bottom(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
643 let cur = self.bottom_top.get();
644 let base_addr = self.buffer.as_ptr() as usize;
645 let cur_addr = base_addr.checked_add(cur).ok_or(AllocError)?;
646 let align_mask = layout.align().saturating_sub(1);
647 let aligned_addr = cur_addr.checked_add(align_mask).ok_or(AllocError)? & !align_mask;
648 // `aligned_addr >= cur_addr >= base_addr`, so the subtraction
649 // does not underflow.
650 let aligned_offset = aligned_addr - base_addr;
651 let new_top = aligned_offset
652 .checked_add(layout.size())
653 .ok_or(AllocError)?;
654 if new_top > self.top_top.get() {
655 return Err(AllocError);
656 }
657 self.bottom_top.set(new_top);
658 if new_top > self.bottom_peak.get() {
659 self.bottom_peak.set(new_top);
660 }
661 // SAFETY: `aligned_offset` is within `[0, top_top)` which is a
662 // subset of `[0, capacity)`. The reserved range
663 // `[aligned_offset, new_top)` is exclusive to this allocation
664 // until the next reset or rewind.
665 let ptr = unsafe { self.buffer.as_ptr().add(aligned_offset) };
666 let slice = core::ptr::slice_from_raw_parts_mut(ptr, layout.size());
667 NonNull::new(slice).ok_or(AllocError)
668 }
669
670 /// Allocate from the top end.
671 ///
672 /// Alignment is computed against the actual buffer base address.
673 fn alloc_top(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
674 let cur = self.top_top.get();
675 let new_end_offset = cur.checked_sub(layout.size()).ok_or(AllocError)?;
676 let base_addr = self.buffer.as_ptr() as usize;
677 let new_end_addr = base_addr.checked_add(new_end_offset).ok_or(AllocError)?;
678 let align_mask = layout.align().saturating_sub(1);
679 // Round down to alignment. The result may be less than
680 // `base_addr` if the buffer base is itself misaligned and the
681 // allocation is near the bottom of the buffer; that case fails.
682 let aligned_addr = new_end_addr & !align_mask;
683 if aligned_addr < base_addr {
684 return Err(AllocError);
685 }
686 let aligned_offset = aligned_addr - base_addr;
687 if aligned_offset < self.bottom_top.get() {
688 return Err(AllocError);
689 }
690 self.top_top.set(aligned_offset);
691 if aligned_offset < self.top_peak_low.get() {
692 self.top_peak_low.set(aligned_offset);
693 }
694 // SAFETY: `aligned_offset` is within `[bottom_top, capacity)`
695 // and the reserved range `[aligned_offset, aligned_offset + size)`
696 // is exclusive to this allocation until the next reset or
697 // rewind.
698 let ptr = unsafe { self.buffer.as_ptr().add(aligned_offset) };
699 let slice = core::ptr::slice_from_raw_parts_mut(ptr, layout.size());
700 NonNull::new(slice).ok_or(AllocError)
701 }
702}
703
704impl core::fmt::Debug for Arena {
705 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
706 f.debug_struct("Arena")
707 .field("capacity", &self.capacity)
708 .field("bottom_used", &self.bottom_used())
709 .field("top_used", &self.top_used())
710 .field("free", &self.free())
711 .field("bottom_peak", &self.bottom_peak())
712 .field("top_peak", &self.top_peak())
713 .finish()
714 }
715}
716
717// Soundness audit for the explicit `Drop` impl below.
718//
719// The arena holds a raw `NonNull<u8>` pointer to the backing storage.
720// The `storage` field tracks ownership.
721//
722// - `Storage::External`: the caller owns the buffer. The `Drop` impl
723// does not free it. The caller's safety contracts on
724// `Arena::from_static_buffer` and `Arena::from_buffer_unchecked`
725// require the buffer to outlive the arena.
726// - `Storage::Owned`: the arena owns the heap allocation that backs
727// the buffer. The `Drop` impl reconstitutes a `Box<[u8]>` from the
728// raw pointer and drops it, releasing the buffer.
729//
730// The buffer pointer has raw provenance (derived from `Box::into_raw`)
731// so that handle allocations through a shared `&Arena` reference do
732// not run afoul of stacked-borrows or tree-borrows aliasing rules.
733impl Drop for Arena {
734 fn drop(&mut self) {
735 #[cfg(feature = "alloc")]
736 if matches!(self.storage, Storage::Owned) && self.capacity > 0 {
737 use alloc::alloc::{Layout as AllocLayout, dealloc};
738 // SAFETY: When `storage` is `Owned` with non-zero capacity,
739 // the buffer was obtained from `alloc_zeroed` with this
740 // exact layout. The same layout is used for `dealloc`. The
741 // arena is being dropped, so no further access to the
742 // buffer occurs after this point.
743 let layout = unsafe { AllocLayout::from_size_align_unchecked(self.capacity, 16) };
744 unsafe { dealloc(self.buffer.as_ptr(), layout) };
745 }
746 }
747}
748
749/// Allocation handle for the bottom end of an arena.
750///
751/// Implements `allocator_api2::alloc::Allocator`. Use with constructors
752/// such as `allocator_api2::vec::Vec::new_in(arena.bottom_handle())`.
753#[derive(Clone, Copy, Debug)]
754pub struct BottomHandle<'a>(&'a Arena);
755
756/// Allocation handle for the top end of an arena.
757///
758/// Implements `allocator_api2::alloc::Allocator`. Use with constructors
759/// such as `allocator_api2::vec::Vec::new_in(arena.top_handle())`.
760#[derive(Clone, Copy, Debug)]
761pub struct TopHandle<'a>(&'a Arena);
762
763// SAFETY: The arena's allocation methods uphold the `Allocator`
764// contract. Returned pointers are valid for the requested layout,
765// unique to the caller, and remain valid until the next reset or
766// rewind. Deallocation is a no-op because the bump allocator reclaims
767// memory in bulk.
768unsafe impl Allocator for BottomHandle<'_> {
769 fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
770 self.0.alloc_bottom(layout)
771 }
772
773 unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: Layout) {
774 // No-op. Bump allocator reclaims at reset.
775 }
776}
777
778// SAFETY: Same reasoning as `BottomHandle`.
779unsafe impl Allocator for TopHandle<'_> {
780 fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
781 self.0.alloc_top(layout)
782 }
783
784 unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: Layout) {
785 // No-op. Bump allocator reclaims at reset.
786 }
787}
788
789/// Lifetime-free safe handle to a value stored in an arena.
790///
791/// Stores a raw pointer to a value of type `T` together with the epoch
792/// at which the value was allocated. Access goes through [`ArenaHandle::get`],
793/// which takes a borrow of the arena and validates the epoch. A mismatch
794/// returns [`Stale`].
795///
796/// `ArenaHandle` does not borrow the arena directly. This makes it safe
797/// to embed inside types whose lifetime is unrelated to the arena, such
798/// as a runtime value enum that flows through caches and channels in
799/// the host. The trade-off is that every dereference requires explicit
800/// arena context. The wrapper does not implement `Deref` for that
801/// reason.
802///
803/// `T: ?Sized` is supported. `T = str` and `T = [U]` are the canonical
804/// unsized cases; the wide pointer carries the slice length alongside
805/// the data pointer. Higher-level helpers (for example a string handle
806/// in a downstream crate) build on top of this generic mechanism by
807/// allocating storage in the arena and wrapping the resulting pointer
808/// through [`ArenaHandle::from_raw_parts`].
809///
810/// # Safety contract
811///
812/// The pointer must reference a region of the same arena that produced
813/// the handle. The region must remain unmodified across resets while
814/// the epoch is unchanged. The constructors in this crate uphold this
815/// contract. Hand-rolled construction through public fields is not
816/// possible because the fields are private.
817///
818/// # Serialization
819///
820/// `ArenaHandle` is intentionally not serializable. Its pointer is not
821/// stable across processes. Long-lived deployments must convert handles
822/// to owned bytes before checkpointing.
823pub struct ArenaHandle<T: ?Sized> {
824 ptr: NonNull<T>,
825 epoch: u64,
826}
827
828// SAFETY: `ArenaHandle` is `Copy` for any `T: ?Sized` because both
829// fields are `Copy`. `NonNull<T>` is `Copy` for unsized `T`.
830impl<T: ?Sized> Copy for ArenaHandle<T> {}
831
832impl<T: ?Sized> Clone for ArenaHandle<T> {
833 fn clone(&self) -> Self {
834 *self
835 }
836}
837
838impl<T: ?Sized> core::fmt::Debug for ArenaHandle<T> {
839 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
840 f.debug_struct("ArenaHandle")
841 .field("ptr", &self.ptr.as_ptr())
842 .field("epoch", &self.epoch)
843 .finish()
844 }
845}
846
847// `ArenaHandle` is intentionally not `Send` or `Sync` because the
848// arena it references is single-threaded. The pointer is `*mut` under
849// `NonNull`, which inherits the conservative auto-trait posture.
850
851impl<T: ?Sized> ArenaHandle<T> {
852 /// Construct a handle from raw parts.
853 ///
854 /// Used by higher-level helpers that allocate typed storage in the
855 /// arena (for example a string or boxed-value helper) and want to
856 /// wrap the resulting pointer in a stale-detecting handle.
857 ///
858 /// # Safety
859 ///
860 /// The caller must guarantee both of the following until the next
861 /// arena reset.
862 ///
863 /// - `ptr` references storage that lives in the arena whose
864 /// `epoch()` returned `epoch` at allocation time, and the storage
865 /// is initialised and aligned for `T`.
866 /// - The bytes addressed by `ptr` are not aliased by any other
867 /// live reference for as long as the handle is held.
868 ///
869 /// Mixing handles between arenas is a logic error: passing the
870 /// wrong arena to [`ArenaHandle::get`] would dereference memory
871 /// that belongs to a different allocator if the wrong arena's
872 /// epoch happened to match.
873 pub unsafe fn from_raw_parts(ptr: NonNull<T>, epoch: u64) -> Self {
874 Self { ptr, epoch }
875 }
876
877 /// Resolve the handle against the arena that produced it.
878 ///
879 /// Returns [`Stale`] if the arena has been reset since the handle
880 /// was issued. The borrow of `arena` ties the returned reference's
881 /// lifetime to the arena, preventing the reference from outliving
882 /// the next reset.
883 ///
884 /// # Safety
885 ///
886 /// The arena must be the same arena that produced the handle. Mixing
887 /// handles between arenas is a logic error. The arena allocations
888 /// are uniquely owned by the arena, so passing the wrong arena will
889 /// dereference memory that is not the original allocation. This
890 /// would be unsound if the wrong arena's epoch happened to match.
891 pub fn get<'a>(&self, arena: &'a Arena) -> Result<&'a T, Stale> {
892 if arena.epoch() != self.epoch {
893 return Err(Stale);
894 }
895 // SAFETY: The handle was issued under the current epoch. The
896 // arena guarantees that allocated regions remain intact until
897 // the next reset, which advances the epoch.
898 Ok(unsafe { self.ptr.as_ref() })
899 }
900
901 /// Epoch captured when the handle was issued.
902 pub fn epoch(&self) -> u64 {
903 self.epoch
904 }
905}
906
907#[cfg(test)]
908mod tests {
909 extern crate alloc as test_alloc;
910
911 use super::*;
912 use allocator_api2::vec::Vec as ArenaVec;
913
914 #[cfg(feature = "alloc")]
915 #[test]
916 fn arena_with_capacity() {
917 let arena = Arena::with_capacity(1024);
918 assert_eq!(arena.capacity(), 1024);
919 assert_eq!(arena.bottom_used(), 0);
920 assert_eq!(arena.top_used(), 0);
921 assert_eq!(arena.free(), 1024);
922 assert_eq!(arena.bottom_peak(), 0);
923 assert_eq!(arena.top_peak(), 0);
924 }
925
926 // Skipped under miri because the test deliberately leaks a Vec to
927 // synthesize a `'static mut [u8]`. Real embedded use of
928 // `from_static_buffer` is a `static mut` array, which has no leak.
929 #[cfg_attr(miri, ignore)]
930 #[test]
931 fn arena_from_static_buffer() {
932 // Use a leaked Box for a 'static-like buffer in tests. In real
933 // embedded use, this would be a `static mut [u8; N]`.
934 let leaked: &'static mut [u8] = test_alloc::vec![0u8; 256].leak();
935 let arena = Arena::from_static_buffer(leaked);
936 assert_eq!(arena.capacity(), 256);
937 let layout = Layout::new::<u64>();
938 let p = arena.bottom_handle().allocate(layout).unwrap();
939 // The leaked Vec<u8> has alignment-of-u8 (one byte) per Rust's
940 // contract. The arena pads as needed to satisfy the requested
941 // u64 alignment, so usage is at least size and at most
942 // size + alignment.
943 assert!(arena.bottom_used() >= 8);
944 assert!(arena.bottom_used() <= 8 + 8);
945 let addr = p.as_ptr() as *const u8 as usize;
946 assert_eq!(addr % 8, 0);
947 }
948
949 #[test]
950 fn arena_from_buffer_unchecked() {
951 let mut buffer = test_alloc::vec![0u8; 128];
952 let ptr = buffer.as_mut_ptr();
953 let len = buffer.len();
954 // SAFETY: `buffer` outlives the arena because we hold it until
955 // the test ends, and we do not access it through `buffer` while
956 // the arena is in use.
957 let arena = unsafe { Arena::from_buffer_unchecked(ptr, len) };
958 assert_eq!(arena.capacity(), 128);
959 let layout = Layout::new::<u32>();
960 let _p = arena.bottom_handle().allocate(layout).unwrap();
961 // The buffer base may be any alignment for from_buffer_unchecked.
962 // The arena pads to satisfy the requested alignment, so usage
963 // is at least the layout size and at most size + alignment.
964 assert!(arena.bottom_used() >= 4);
965 assert!(arena.bottom_used() <= 4 + 4);
966 drop(arena);
967 // `buffer` is still alive here.
968 assert_eq!(buffer.len(), 128);
969 }
970
971 #[cfg(feature = "alloc")]
972 #[test]
973 fn arena_dual_end() {
974 let arena = Arena::with_capacity(64);
975 let layout = Layout::new::<u64>();
976 let _b = arena.bottom_handle().allocate(layout).unwrap();
977 let _t = arena.top_handle().allocate(layout).unwrap();
978 assert_eq!(arena.bottom_used(), 8);
979 assert_eq!(arena.top_used(), 8);
980 assert_eq!(arena.free(), 48);
981 }
982
983 #[cfg(feature = "alloc")]
984 #[test]
985 fn arena_alignment() {
986 let arena = Arena::with_capacity(64);
987 let _byte = arena.bottom_handle().allocate(Layout::new::<u8>()).unwrap();
988 let p_u64 = arena
989 .bottom_handle()
990 .allocate(Layout::new::<u64>())
991 .unwrap();
992 let addr = p_u64.as_ptr() as *const u8 as usize;
993 assert_eq!(addr % 8, 0);
994 }
995
996 #[cfg(feature = "alloc")]
997 #[test]
998 fn arena_exhaustion() {
999 let arena = Arena::with_capacity(16);
1000 let layout = Layout::new::<u64>();
1001 let _a = arena.bottom_handle().allocate(layout).unwrap();
1002 let _b = arena.bottom_handle().allocate(layout).unwrap();
1003 assert!(arena.bottom_handle().allocate(layout).is_err());
1004 }
1005
1006 #[cfg(feature = "alloc")]
1007 #[test]
1008 fn arena_reset() {
1009 let mut arena = Arena::with_capacity(64);
1010 let layout = Layout::new::<u64>();
1011 {
1012 let _b = arena.bottom_handle().allocate(layout).unwrap();
1013 let _t = arena.top_handle().allocate(layout).unwrap();
1014 }
1015 assert_eq!(arena.bottom_used(), 8);
1016 assert_eq!(arena.top_used(), 8);
1017 arena.reset().unwrap();
1018 assert_eq!(arena.bottom_used(), 0);
1019 assert_eq!(arena.top_used(), 0);
1020 assert_eq!(arena.epoch(), 1);
1021 }
1022
1023 #[cfg(feature = "alloc")]
1024 #[test]
1025 fn arena_peak_tracking() {
1026 let arena = Arena::with_capacity(128);
1027 let layout = Layout::new::<u64>();
1028 let mark = arena.bottom_mark();
1029 let _a = arena.bottom_handle().allocate(layout).unwrap();
1030 let _b = arena.bottom_handle().allocate(layout).unwrap();
1031 assert_eq!(arena.bottom_peak(), 16);
1032 // Rewind reduces current usage but not the peak.
1033 // SAFETY: Drops happen at scope end, and we are about to
1034 // re-allocate. The peak observation is from before any rewind.
1035 unsafe {
1036 arena.rewind_bottom(mark);
1037 }
1038 assert_eq!(arena.bottom_used(), 0);
1039 assert_eq!(arena.bottom_peak(), 16);
1040 }
1041
1042 #[cfg(feature = "alloc")]
1043 #[test]
1044 fn arena_clear_peaks() {
1045 let mut arena = Arena::with_capacity(64);
1046 let layout = Layout::new::<u64>();
1047 let _a = arena.bottom_handle().allocate(layout).unwrap();
1048 assert_eq!(arena.bottom_peak(), 8);
1049 arena.reset().unwrap();
1050 assert_eq!(arena.bottom_used(), 0);
1051 // Peak persists after reset.
1052 assert_eq!(arena.bottom_peak(), 8);
1053 arena.clear_peaks();
1054 assert_eq!(arena.bottom_peak(), 0);
1055 }
1056
1057 #[cfg(feature = "alloc")]
1058 #[test]
1059 fn arena_mark_rewind() {
1060 let arena = Arena::with_capacity(128);
1061 let layout = Layout::new::<u32>();
1062 let mark = arena.bottom_mark();
1063 let _a = arena.bottom_handle().allocate(layout).unwrap();
1064 let _b = arena.bottom_handle().allocate(layout).unwrap();
1065 assert_eq!(arena.bottom_used(), 8);
1066 // SAFETY: We have not retained any references to the
1067 // allocations beyond this scope. The handles' allocations are
1068 // raw pointers that we are not using past this point.
1069 unsafe {
1070 arena.rewind_bottom(mark);
1071 }
1072 assert_eq!(arena.bottom_used(), 0);
1073 }
1074
1075 #[cfg(feature = "alloc")]
1076 #[test]
1077 fn arena_per_end_reset() {
1078 let arena = Arena::with_capacity(64);
1079 let layout = Layout::new::<u64>();
1080 let _b = arena.bottom_handle().allocate(layout).unwrap();
1081 let _t = arena.top_handle().allocate(layout).unwrap();
1082 // SAFETY: No retained allocations.
1083 unsafe {
1084 arena.reset_bottom();
1085 }
1086 assert_eq!(arena.bottom_used(), 0);
1087 assert_eq!(arena.top_used(), 8);
1088 // SAFETY: No retained allocations.
1089 unsafe {
1090 arena.reset_top();
1091 }
1092 assert_eq!(arena.top_used(), 0);
1093 }
1094
1095 #[cfg(feature = "alloc")]
1096 #[test]
1097 fn arena_vec_integration() {
1098 let arena = Arena::with_capacity(2048);
1099 let mut v: ArenaVec<i64, _> = ArenaVec::new_in(arena.bottom_handle());
1100 for i in 0..10 {
1101 v.push(i);
1102 }
1103 assert_eq!(v.iter().sum::<i64>(), 45);
1104 assert!(arena.bottom_used() > 0);
1105 }
1106
1107 #[cfg(feature = "alloc")]
1108 #[test]
1109 fn epoch_advances_on_reset() {
1110 let mut arena = Arena::with_capacity(64);
1111 assert_eq!(arena.epoch(), 0);
1112 arena.reset().unwrap();
1113 assert_eq!(arena.epoch(), 1);
1114 arena.reset().unwrap();
1115 assert_eq!(arena.epoch(), 2);
1116 }
1117
1118 #[cfg(feature = "alloc")]
1119 #[test]
1120 fn epoch_saturates() {
1121 let mut arena = Arena::with_capacity(16);
1122 // Force the epoch to one below saturation.
1123 arena.epoch.set(u64::MAX - 1);
1124 // First reset advances to u64::MAX.
1125 arena.reset().unwrap();
1126 assert_eq!(arena.epoch(), u64::MAX);
1127 assert_eq!(arena.epoch_remaining(), 0);
1128 // Second reset must refuse.
1129 let result = arena.reset();
1130 assert!(matches!(result, Err(EpochSaturated)));
1131 }
1132
1133 #[cfg(feature = "alloc")]
1134 #[test]
1135 fn force_reset_epoch_recovers() {
1136 let mut arena = Arena::with_capacity(16);
1137 arena.epoch.set(u64::MAX);
1138 assert!(matches!(arena.reset(), Err(EpochSaturated)));
1139 // SAFETY: No `ArenaHandle` exists in this test scope.
1140 unsafe {
1141 arena.force_reset_epoch();
1142 }
1143 assert_eq!(arena.epoch(), 0);
1144 arena.reset().unwrap();
1145 assert_eq!(arena.epoch(), 1);
1146 }
1147
1148 #[cfg(feature = "alloc")]
1149 fn alloc_u64(arena: &Arena, value: u64) -> ArenaHandle<u64> {
1150 use allocator_api2::alloc::Allocator;
1151 use core::alloc::Layout;
1152 let layout = Layout::new::<u64>();
1153 let raw = arena.top_handle().allocate(layout).expect("alloc");
1154 let typed: NonNull<u64> = raw.cast();
1155 // SAFETY: `typed` is freshly allocated unique storage of the
1156 // correct layout for `u64`.
1157 unsafe { typed.as_ptr().write(value) };
1158 // SAFETY: `typed` references storage in `arena`'s top region
1159 // freshly allocated under the current epoch.
1160 unsafe { ArenaHandle::from_raw_parts(typed, arena.epoch()) }
1161 }
1162
1163 #[cfg(feature = "alloc")]
1164 #[test]
1165 fn arena_handle_from_raw_parts_roundtrip() {
1166 let arena = Arena::with_capacity(256);
1167 let handle = alloc_u64(&arena, 0xdeadbeef);
1168 assert_eq!(*handle.get(&arena).unwrap(), 0xdeadbeef);
1169 }
1170
1171 #[cfg(feature = "alloc")]
1172 #[test]
1173 fn arena_handle_stale_after_reset() {
1174 let mut arena = Arena::with_capacity(256);
1175 let handle = alloc_u64(&arena, 7);
1176 assert_eq!(*handle.get(&arena).unwrap(), 7);
1177 arena.reset().unwrap();
1178 assert!(matches!(handle.get(&arena), Err(Stale)));
1179 }
1180
1181 #[cfg(feature = "alloc")]
1182 #[test]
1183 fn arena_handle_is_copy() {
1184 let arena = Arena::with_capacity(256);
1185 let handle = alloc_u64(&arena, 99);
1186 let copy = handle;
1187 assert_eq!(*handle.get(&arena).unwrap(), 99);
1188 assert_eq!(*copy.get(&arena).unwrap(), 99);
1189 }
1190
1191 #[cfg(feature = "alloc")]
1192 #[test]
1193 fn arena_dual_vec_integration() {
1194 let arena = Arena::with_capacity(4096);
1195 let mut bot: ArenaVec<i64, _> = ArenaVec::new_in(arena.bottom_handle());
1196 let mut top: ArenaVec<i64, _> = ArenaVec::new_in(arena.top_handle());
1197 for i in 0..5 {
1198 bot.push(i);
1199 top.push(i * 100);
1200 }
1201 assert_eq!(bot.len(), 5);
1202 assert_eq!(top.len(), 5);
1203 assert!(arena.bottom_used() > 0);
1204 assert!(arena.top_used() > 0);
1205 }
1206
1207 #[cfg(feature = "alloc")]
1208 #[test]
1209 fn budget_fits() {
1210 let arena = Arena::with_capacity(1024);
1211 assert!(arena.fits_budget(&Budget::new(512, 256)));
1212 assert!(arena.fits_budget(&Budget::new(0, 0)));
1213 assert!(arena.fits_budget(&Budget::new(1024, 0)));
1214 assert!(!arena.fits_budget(&Budget::new(513, 512)));
1215 assert!(!arena.fits_budget(&Budget::new(usize::MAX, 1)));
1216 }
1217
1218 #[test]
1219 fn budget_total_saturates() {
1220 let b = Budget::new(usize::MAX, 1);
1221 assert_eq!(b.total(), usize::MAX);
1222 }
1223
1224 #[cfg(feature = "alloc")]
1225 #[test]
1226 fn arena_zero_capacity() {
1227 let arena = Arena::with_capacity(0);
1228 assert!(arena.bottom_handle().allocate(Layout::new::<u8>()).is_err());
1229 assert!(arena.fits_budget(&Budget::new(0, 0)));
1230 }
1231
1232 #[cfg(feature = "alloc")]
1233 #[test]
1234 fn arena_zero_size_layout() {
1235 let arena = Arena::with_capacity(64);
1236 let layout = Layout::new::<()>();
1237 assert!(arena.bottom_handle().allocate(layout).is_ok());
1238 assert_eq!(arena.bottom_used(), 0);
1239 }
1240
1241 #[test]
1242 fn arena_misaligned_base_produces_aligned_allocation() {
1243 // Construct an arena over a buffer whose base address is
1244 // deliberately offset by one byte from the underlying storage.
1245 // The base is therefore at most byte-aligned. The arena must
1246 // still produce u64-aligned pointers for u64 allocations.
1247 let mut backing = test_alloc::vec![0u8; 256];
1248 let raw_ptr = backing.as_mut_ptr();
1249 // SAFETY: The backing vector lives until the end of the test.
1250 // We deliberately offset by one to create a misaligned base.
1251 let arena = unsafe { Arena::from_buffer_unchecked(raw_ptr.add(1), 200) };
1252
1253 // Allocate a u64. The pointer must be 8-byte aligned regardless
1254 // of the misaligned base.
1255 let p_u64 = arena
1256 .bottom_handle()
1257 .allocate(Layout::new::<u64>())
1258 .unwrap();
1259 let addr = p_u64.as_ptr() as *const u8 as usize;
1260 assert_eq!(addr % 8, 0, "allocation must be 8-byte aligned");
1261
1262 // Allocate a u128. The pointer must be 16-byte aligned.
1263 let p_u128 = arena
1264 .bottom_handle()
1265 .allocate(Layout::new::<u128>())
1266 .unwrap();
1267 let addr = p_u128.as_ptr() as *const u8 as usize;
1268 assert_eq!(addr % 16, 0, "allocation must be 16-byte aligned");
1269
1270 // Top-end allocation also aligned.
1271 let p_top = arena.top_handle().allocate(Layout::new::<u64>()).unwrap();
1272 let addr = p_top.as_ptr() as *const u8 as usize;
1273 assert_eq!(addr % 8, 0, "top allocation must be 8-byte aligned");
1274
1275 // Keep `backing` alive until here.
1276 drop(backing);
1277 }
1278
1279 #[cfg(feature = "alloc")]
1280 #[test]
1281 fn arena_byte_allocation_packs_tightly() {
1282 // alloc_bottom_bytes does not enforce alignment. Three u8
1283 // allocations of one byte each consume exactly three bytes.
1284 let arena = Arena::with_capacity(64);
1285 let _a = arena.alloc_bottom_bytes(1).unwrap();
1286 let _b = arena.alloc_bottom_bytes(1).unwrap();
1287 let _c = arena.alloc_bottom_bytes(1).unwrap();
1288 assert_eq!(arena.bottom_used(), 3);
1289 }
1290
1291 #[cfg(feature = "alloc")]
1292 #[test]
1293 fn arena_aligned_allocation_pads() {
1294 // After one byte, an aligned u64 allocation pads to align 8.
1295 // Total used should be 8 + 8 = 16 bytes.
1296 let arena = Arena::with_capacity(64);
1297 let _a = arena.alloc_bottom_bytes(1).unwrap();
1298 assert_eq!(arena.bottom_used(), 1);
1299 let _b = arena
1300 .bottom_handle()
1301 .allocate(Layout::new::<u64>())
1302 .unwrap();
1303 assert_eq!(arena.bottom_used(), 16);
1304 }
1305
1306 #[cfg(feature = "alloc")]
1307 #[test]
1308 fn arena_top_byte_allocation() {
1309 let arena = Arena::with_capacity(64);
1310 let _a = arena.alloc_top_bytes(3).unwrap();
1311 assert_eq!(arena.top_used(), 3);
1312 }
1313
1314 #[cfg(feature = "alloc")]
1315 #[test]
1316 fn arena_byte_allocation_zero_size() {
1317 let arena = Arena::with_capacity(64);
1318 // Zero-size byte allocation is admissible and consumes nothing.
1319 let _a = arena.alloc_bottom_bytes(0).unwrap();
1320 assert_eq!(arena.bottom_used(), 0);
1321 }
1322}