Skip to main content

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}