Skip to main content

keleusma_arena/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3//!
4//! # API Reference
5//!
6//! ## Construction
7//!
8//! - [`Arena::with_capacity`]. Heap-backed. Requires the `alloc` feature.
9//! - [`Arena::from_static_buffer`]. Borrows a `&'static mut [u8]`. Safe.
10//! - [`Arena::from_buffer_unchecked`]. Raw pointer and length. Unsafe.
11//!
12//! ## Allocation
13//!
14//! [`BottomHandle`] and [`TopHandle`] borrow the arena and implement
15//! `allocator_api2::alloc::Allocator`. Pass them to `Vec::new_in` and
16//! similar constructors for arena-backed collections. The bottom end
17//! starts at offset zero and grows upward. The top end starts at the
18//! buffer's high address and grows downward. The arena imposes no
19//! semantic distinction between the two ends.
20//!
21//! Code that prefers a CPU-memory mental model may use the method
22//! aliases [`Arena::stack_handle`] and [`Arena::heap_handle`], which
23//! return the same `BottomHandle` and `TopHandle` types under
24//! conventional names.
25//!
26//! Aligned allocations go through the `Allocator` trait with a
27//! `Layout` that carries the desired alignment. Alignment is computed
28//! against the actual buffer base address, so any base alignment is
29//! supported. Unaligned byte allocations have direct convenience
30//! methods [`Arena::alloc_bottom_bytes`] and [`Arena::alloc_top_bytes`]
31//! that allocate `n` bytes without padding for alignment. Use the
32//! aligned form for typed values and pointers. Use the byte form for
33//! packed byte buffers.
34//!
35//! ## Reset, Rewind, and Marks
36//!
37//! [`Arena::reset`] takes `&mut self` and clears both ends safely. Each
38//! end also exposes a LIFO mark and rewind discipline. The mark
39//! accessors [`Arena::bottom_mark`] and [`Arena::top_mark`] are safe.
40//! The rewind and per-end reset operations [`Arena::rewind_bottom`],
41//! [`Arena::rewind_top`], [`Arena::reset_bottom`], and
42//! [`Arena::reset_top`] are unsafe because they invalidate the rewound
43//! region while raw pointers obtained through the `Allocator` trait may
44//! still be held by the caller.
45//!
46//! ## Observability and Budget
47//!
48//! [`Arena::bottom_peak`] and [`Arena::top_peak`] track high watermarks
49//! since arena creation or the most recent [`Arena::clear_peaks`].
50//! [`Arena::bottom_used`], [`Arena::top_used`], [`Arena::free`], and
51//! [`Arena::capacity`] report current state.
52//!
53//! [`Budget`] is a generic memory budget structure. Producers compute a
54//! budget through any analysis they choose. [`Arena::fits_budget`]
55//! checks whether the budget is admissible against the arena's capacity.
56//!
57//! ## Thread Safety
58//!
59//! Not thread-safe. Interior mutability uses `Cell<usize>` rather than
60//! atomic primitives. The arena is designed for scoped per-thread use
61//! through the `Allocator` trait. Setting it as the program's
62//! `#[global_allocator]` requires a thread-safe wrapper that this crate
63//! does not provide.
64
65#![no_std]
66#![cfg_attr(docsrs, feature(doc_cfg))]
67
68#[cfg(feature = "alloc")]
69extern crate alloc;
70
71use core::alloc::Layout;
72use core::cell::Cell;
73use core::ptr::NonNull;
74
75use allocator_api2::alloc::{AllocError, Allocator};
76
77/// A worst-case memory usage budget.
78///
79/// A producer-agnostic structure describing a worst-case stack and heap
80/// memory bound. The arena's [`Arena::fits_budget`] method checks whether
81/// the budget is admissible against the arena's capacity. The two bounds
82/// must be non-overlapping in any single state of the arena, but they
83/// represent peak usage of the two ends and so must sum within the
84/// arena's capacity.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub struct Budget {
87    /// Maximum bytes consumed at the bottom end.
88    pub bottom_bytes: usize,
89    /// Maximum bytes consumed at the top end.
90    pub top_bytes: usize,
91}
92
93impl Budget {
94    /// Construct a budget with the given bottom and top bounds.
95    pub const fn new(bottom_bytes: usize, top_bytes: usize) -> Self {
96        Self {
97            bottom_bytes,
98            top_bytes,
99        }
100    }
101
102    /// Total bytes required by this budget. Saturates at `usize::MAX` on
103    /// overflow so that an oversized budget does not silently wrap.
104    pub const fn total(&self) -> usize {
105        self.bottom_bytes.saturating_add(self.top_bytes)
106    }
107}
108
109/// A mark for the bottom end of an arena.
110///
111/// Returned by [`Arena::bottom_mark`]. Pass back to
112/// [`Arena::rewind_bottom`] to restore the bottom pointer to this
113/// position. Marks are tied to the arena that produced them; passing a
114/// mark to a different arena is a logic error and produces undefined
115/// behavior under the unsafe rewind contract.
116#[derive(Debug, Clone, Copy)]
117pub struct BottomMark(usize);
118
119/// A mark for the top end of an arena.
120///
121/// Returned by [`Arena::top_mark`]. Pass back to [`Arena::rewind_top`]
122/// to restore the top pointer to this position.
123#[derive(Debug, Clone, Copy)]
124pub struct TopMark(usize);
125
126/// Storage backing variants for an arena.
127///
128/// The arena holds the raw pointer and capacity directly in the
129/// `buffer` and `capacity` fields. The variant tracks ownership for
130/// the explicit `Drop` impl on `Arena`. Owned arenas reconstruct the
131/// `Box` via `Box::from_raw` and let it drop, releasing the buffer.
132/// External arenas leave the buffer untouched; the caller owns the
133/// storage.
134///
135/// Using a raw pointer rather than holding the `Box` directly gives
136/// the buffer "raw" provenance from the perspective of the borrow
137/// checker and miri's aliasing models. This is necessary because
138/// allocations through `BottomHandle` and `TopHandle` derive write
139/// pointers into the buffer through a shared `&Arena`; deriving
140/// through a unique-reference ancestor would make subsequent
141/// derivations from the same source aliasing-unsound under both
142/// stacked borrows and tree borrows.
143#[derive(Clone, Copy)]
144enum Storage {
145    /// Externally owned buffer. The caller is responsible for keeping
146    /// the buffer alive for the arena's lifetime.
147    External,
148    /// Owned buffer allocated through the global allocator. The arena
149    /// reconstructs the `Box` and drops it on its own `Drop`.
150    #[cfg(feature = "alloc")]
151    Owned,
152}
153
154/// A dual-end bump-allocated arena.
155///
156/// Owns or borrows a fixed-size buffer of bytes. Two bump pointers track
157/// allocation positions at each end. The bottom end grows from low
158/// addresses upward. The top end grows from high addresses downward.
159/// Allocation fails when the two pointers would meet.
160///
161/// The arena is not the program's `#[global_allocator]` and is not
162/// intended to be one. It is designed for scoped per-region or
163/// per-thread use through `BottomHandle` and `TopHandle`, which the
164/// host passes to allocator-aware collection constructors. The standard
165/// global allocator continues to handle every allocation that does not
166/// route through an arena handle. Hosts that want every allocation in
167/// the program to be arena-backed must wrap the arena in a thread-safe
168/// allocator and install it via `#[global_allocator]`; this crate does
169/// not provide such a wrapper because doing so well requires choices
170/// that depend on the host's threading and synchronization model.
171///
172/// ## Generations and stale-pointer detection
173///
174/// The arena carries an `epoch` counter that increments on [`Arena::reset`].
175/// The `ArenaHandle` family of safe wrappers captures the epoch at
176/// construction and validates it on access, returning [`Stale`] if the
177/// arena has been reset since the handle was issued. The counter is
178/// `u64` and uses checked arithmetic. A saturated counter halts the
179/// arena's reset path with [`EpochSaturated`]. Saturation requires
180/// roughly five hundred eighty four thousand years at one reset per
181/// microsecond and is documentation rather than a real failure mode in
182/// expected use.
183///
184/// In-process recovery from saturation is possible through
185/// [`Arena::force_reset_epoch`], which is unsafe and requires the
186/// caller to ensure that no `ArenaHandle` from any prior epoch is
187/// reachable. Cross-process recovery for very long-lived deployments
188/// uses checkpoint and restart against host-owned non-volatile storage.
189/// `ArenaHandle` is intentionally not serializable because its pointer
190/// is not stable across processes.
191///
192/// See the crate-level documentation for the design overview.
193pub struct Arena {
194    /// Pointer to the start of the backing buffer. Stable for the
195    /// arena's lifetime.
196    buffer: NonNull<u8>,
197    /// Total capacity of the buffer in bytes.
198    capacity: usize,
199    /// Bytes reserved at the start of the buffer for the persistent
200    /// (`.data`) region. The persistent region occupies the byte range
201    /// `[0, persistent_capacity)` and is preserved across every form
202    /// of reset on the arena. Callers configure it through
203    /// [`Arena::resize_persistent`] when assigning a pooled arena to a
204    /// specific module. The default is zero, matching the dual-headed
205    /// behaviour of earlier versions of this crate.
206    persistent_capacity: Cell<usize>,
207    /// Current bottom pointer. Allocations from the bottom end consume
208    /// the range `[persistent_capacity, bottom_top)`. The bottom
209    /// region never grows below `persistent_capacity`.
210    bottom_top: Cell<usize>,
211    /// Current top pointer. Allocations from the top end consume the
212    /// range `[top_top, capacity)`.
213    top_top: Cell<usize>,
214    /// Peak observed value of `bottom_top`. Watermark for sizing
215    /// analysis. Stored as an absolute offset from the buffer base,
216    /// not relative to `persistent_capacity`.
217    bottom_peak: Cell<usize>,
218    /// Lowest observed value of `top_top`. Combined with `capacity`
219    /// gives the peak top usage.
220    top_peak_low: Cell<usize>,
221    /// Generation counter. Incremented on [`Arena::reset`]. Captured by
222    /// [`ArenaHandle`] values and validated on access for stale-pointer
223    /// detection. Saturates at `u64::MAX`, at which point further
224    /// resets fail with [`EpochSaturated`] until the caller invokes
225    /// [`Arena::force_reset_epoch`].
226    epoch: Cell<u64>,
227    /// Storage discriminator. The field is read implicitly via `Drop`.
228    #[allow(dead_code)]
229    storage: Storage,
230}
231
232/// Hard halt error returned by [`Arena::reset`] when the epoch counter
233/// would saturate.
234///
235/// Saturation requires roughly five hundred eighty four thousand years
236/// at one reset per microsecond, but explicit refusal at saturation is
237/// the correct posture for high-assurance use. Recovery is via
238/// [`Arena::force_reset_epoch`].
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub struct EpochSaturated;
241
242/// Error returned by [`ArenaHandle::get`] when the arena has been
243/// reset since the handle was issued.
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub struct Stale;
246
247/// Error returned by [`Arena::resize_persistent`] when the requested
248/// persistent size cannot be applied.
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub enum ResizeError {
251    /// Requested persistent size exceeds the arena's total capacity.
252    /// The caller should size the pool entry larger before assignment
253    /// or pick a smaller module.
254    ExceedsCapacity,
255    /// Epoch counter saturated. The arena's stale-detection
256    /// machinery cannot advance further. Recover through
257    /// [`Arena::force_reset_epoch`].
258    EpochSaturated,
259    /// A grow requested through [`Arena::resize_persistent_capacity`]
260    /// would push the bottom dual-headed head past the top head, so the
261    /// two regions would overlap. The arena is left unchanged. The
262    /// caller should free dual-headed space or size the arena larger.
263    DualHeadedOverlap,
264}
265
266// SAFETY: The arena uses `Cell` for interior mutability of the bump
267// pointers and peaks. `Cell` is `Send` but not `Sync`. The arena itself
268// is not `Sync` for the same reason.
269
270impl Arena {
271    /// Create an arena backed by a freshly allocated heap buffer of the
272    /// given byte capacity.
273    ///
274    /// Available only with the `alloc` feature. The buffer is zeroed at
275    /// construction and is allocated with 16-byte alignment, which
276    /// covers the alignment requirements of `i64`, `f64`, `u128`, and
277    /// most platform-native pointers and primitives.
278    ///
279    /// Panics on allocation failure via the standard `handle_alloc_error`
280    /// path. A capacity of zero produces an arena that satisfies
281    /// allocation requests for zero-size layouts only; non-zero
282    /// allocations return `AllocError`.
283    ///
284    /// # Examples
285    ///
286    /// ```
287    /// use allocator_api2::vec::Vec as ArenaVec;
288    /// use keleusma_arena::Arena;
289    ///
290    /// let arena = Arena::with_capacity(1024);
291    /// let mut v: ArenaVec<i64, _> = ArenaVec::new_in(arena.stack_handle());
292    /// v.push(1);
293    /// v.push(2);
294    /// v.push(3);
295    /// assert_eq!(v.len(), 3);
296    /// assert!(arena.bottom_used() >= 24);
297    /// ```
298    #[cfg(feature = "alloc")]
299    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
300    pub fn with_capacity(capacity: usize) -> Self {
301        use alloc::alloc::{Layout as AllocLayout, handle_alloc_error};
302        // Delegate to the fallible constructor and preserve the
303        // abort-on-allocation-failure contract: a null allocation aborts
304        // through the standard handler and an invalid layout panics,
305        // exactly as the direct implementation did.
306        Self::try_with_capacity(capacity).unwrap_or_else(|_| {
307            let layout = AllocLayout::from_size_align(capacity, 16).expect("invalid arena layout");
308            handle_alloc_error(layout)
309        })
310    }
311
312    /// Create an arena backed by a freshly allocated heap buffer of the
313    /// given byte capacity, returning [`AllocError`] instead of aborting
314    /// when the allocation cannot be satisfied.
315    ///
316    /// This is the fallible counterpart to [`Arena::with_capacity`]. A host
317    /// application is the right place to surface an out-of-memory condition
318    /// as a diagnostic and a controlled exit rather than the
319    /// `handle_alloc_error` abort that `with_capacity` triggers. A
320    /// memory-constrained deployment that sizes an arena from a worst-case
321    /// bound, and so may legitimately request more than the host can
322    /// provide, should use this entry point.
323    ///
324    /// Available only with the `alloc` feature. The buffer is zeroed at
325    /// construction and allocated with 16-byte alignment, as for
326    /// `with_capacity`. A capacity of zero never fails and yields an arena
327    /// that satisfies only zero-size allocation requests.
328    ///
329    /// # Examples
330    ///
331    /// ```
332    /// use keleusma_arena::Arena;
333    ///
334    /// let arena = Arena::try_with_capacity(1024).expect("1 KiB is available");
335    /// assert_eq!(arena.capacity(), 1024);
336    /// ```
337    #[cfg(feature = "alloc")]
338    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
339    pub fn try_with_capacity(capacity: usize) -> Result<Self, AllocError> {
340        use alloc::alloc::{Layout as AllocLayout, alloc_zeroed};
341
342        let buffer = if capacity == 0 {
343            NonNull::<u8>::dangling()
344        } else {
345            // Allocate a 16-byte-aligned buffer. The alignment covers every
346            // standard primitive type and gives the arena predictable
347            // behavior across allocators that may otherwise return only
348            // minimally-aligned memory for byte allocations. An invalid
349            // layout (capacity overflowing the address space) and a null
350            // allocation both surface as `AllocError` rather than aborting.
351            let layout = AllocLayout::from_size_align(capacity, 16).map_err(|_| AllocError)?;
352            // SAFETY: `layout` has non-zero size because `capacity > 0`.
353            let raw = unsafe { alloc_zeroed(layout) };
354            NonNull::new(raw).ok_or(AllocError)?
355        };
356        Ok(Self {
357            buffer,
358            capacity,
359            persistent_capacity: Cell::new(0),
360            bottom_top: Cell::new(0),
361            top_top: Cell::new(capacity),
362            bottom_peak: Cell::new(0),
363            top_peak_low: Cell::new(capacity),
364            epoch: Cell::new(0),
365            storage: Storage::Owned,
366        })
367    }
368
369    /// Create an arena backed by a static buffer.
370    ///
371    /// The buffer must outlive the arena. The `'static mut` requirement
372    /// satisfies this for typical embedded patterns where the buffer is
373    /// a static array placed in BSS or DATA. For shorter-lived buffers,
374    /// see [`Arena::from_buffer_unchecked`].
375    pub fn from_static_buffer(buffer: &'static mut [u8]) -> Self {
376        let capacity = buffer.len();
377        // SAFETY: `&'static mut [u8]` is non-null and lives for the
378        // duration of the program.
379        let ptr = unsafe { NonNull::new_unchecked(buffer.as_mut_ptr()) };
380        Self {
381            buffer: ptr,
382            capacity,
383            persistent_capacity: Cell::new(0),
384            bottom_top: Cell::new(0),
385            top_top: Cell::new(capacity),
386            bottom_peak: Cell::new(0),
387            top_peak_low: Cell::new(capacity),
388            epoch: Cell::new(0),
389            storage: Storage::External,
390        }
391    }
392
393    /// Create an arena from a raw pointer and length.
394    ///
395    /// The buffer's base alignment does not need to match the alignment
396    /// of any particular allocation type. The arena computes alignment
397    /// against the actual buffer base address and pads as needed for
398    /// each aligned allocation.
399    ///
400    /// # Safety
401    ///
402    /// The caller must uphold the following.
403    ///
404    /// - `ptr` is non-null.
405    /// - `ptr` is valid for reads and writes of `capacity` bytes for the
406    ///   entire lifetime of the returned arena.
407    /// - No other code accesses the buffer through any path that would
408    ///   alias with the arena's allocations during the arena's lifetime.
409    ///
410    /// This constructor is the only path that admits buffers with
411    /// non-`'static` lifetimes. It exists for embedded contexts where
412    /// the lifetime is known statically through other means but the
413    /// type system cannot express it. Most callers should prefer
414    /// [`Arena::from_static_buffer`].
415    pub unsafe fn from_buffer_unchecked(ptr: *mut u8, capacity: usize) -> Self {
416        // SAFETY: Caller asserts non-null and validity. `NonNull::new_unchecked`
417        // is sound under the caller's assertion.
418        let buffer = unsafe { NonNull::new_unchecked(ptr) };
419        Self {
420            buffer,
421            capacity,
422            persistent_capacity: Cell::new(0),
423            bottom_top: Cell::new(0),
424            top_top: Cell::new(capacity),
425            bottom_peak: Cell::new(0),
426            top_peak_low: Cell::new(capacity),
427            epoch: Cell::new(0),
428            storage: Storage::External,
429        }
430    }
431
432    /// Total capacity of the arena in bytes. Equal to the sum of the
433    /// persistent capacity (returned by
434    /// [`Arena::persistent_capacity`]) and the dual-headed capacity
435    /// (returned by [`Arena::dual_headed_capacity`]).
436    pub fn capacity(&self) -> usize {
437        self.capacity
438    }
439
440    /// Size in bytes of the persistent (`.data`) region. The region
441    /// occupies offsets `[0, persistent_capacity())` in the backing
442    /// buffer and is preserved across every form of reset on the
443    /// arena. Default is zero. Hosts adjust the size via
444    /// [`Arena::resize_persistent`].
445    pub fn persistent_capacity(&self) -> usize {
446        self.persistent_capacity.get()
447    }
448
449    /// Bytes available to the dual-headed (bottom and top) regions.
450    /// Equals `capacity - persistent_capacity`.
451    pub fn dual_headed_capacity(&self) -> usize {
452        self.capacity - self.persistent_capacity.get()
453    }
454
455    /// Bytes currently allocated from the bottom region. Measured
456    /// relative to the start of the bottom region, which begins at
457    /// offset `persistent_capacity`. With `persistent_capacity == 0`
458    /// the result matches the dual-headed behaviour of earlier
459    /// versions of this crate.
460    pub fn bottom_used(&self) -> usize {
461        self.bottom_top.get() - self.persistent_capacity.get()
462    }
463
464    /// Bytes currently allocated from the top region.
465    pub fn top_used(&self) -> usize {
466        self.capacity - self.top_top.get()
467    }
468
469    /// Bytes available for either end of the dual-headed region to
470    /// consume.
471    pub fn free(&self) -> usize {
472        self.top_top.get().saturating_sub(self.bottom_top.get())
473    }
474
475    /// Highest observed bottom usage in bytes since arena creation or
476    /// the most recent [`Arena::clear_peaks`] call. Measured relative
477    /// to the start of the bottom region.
478    pub fn bottom_peak(&self) -> usize {
479        self.bottom_peak.get() - self.persistent_capacity.get()
480    }
481
482    /// Highest observed top usage in bytes since arena creation or the
483    /// most recent [`Arena::clear_peaks`] call.
484    pub fn top_peak(&self) -> usize {
485        self.capacity - self.top_peak_low.get()
486    }
487
488    /// Pointer to the start of the persistent (`.data`) region.
489    ///
490    /// Returns a non-null pointer to a contiguous region of length
491    /// [`Arena::persistent_capacity`]. Reads and writes through the
492    /// pointer must stay within that range. The returned pointer is
493    /// stable for the arena's lifetime.
494    ///
495    /// # Safety
496    ///
497    /// The caller is responsible for synchronising access. The arena
498    /// type itself is not `Sync`. Returning a raw pointer rather than
499    /// a slice reference is deliberate so the caller can manage
500    /// borrow scoping in cases where the persistent region holds
501    /// values that the bytecode VM addresses directly.
502    pub fn persistent_ptr(&self) -> NonNull<u8> {
503        self.buffer
504    }
505
506    /// Resize the persistent (`.data`) region.
507    ///
508    /// Fully resets the dual-headed region and advances the epoch so
509    /// every outstanding [`ArenaHandle`] becomes stale. Use when
510    /// assigning an oversized pool entry to a specific module that
511    /// declares a particular persistent footprint.
512    ///
513    /// The new size must satisfy `new_size <= capacity`. Larger sizes
514    /// return [`ResizeError::ExceedsCapacity`]. A saturated epoch
515    /// counter returns [`ResizeError::EpochSaturated`]; recovery is
516    /// via [`Arena::force_reset_epoch`].
517    ///
518    /// Takes `&mut self` so the borrow checker prevents calling
519    /// resize while any handle borrows the arena. This guarantees no
520    /// live allocations through `Allocator` trait users at the
521    /// moment of resize.
522    pub fn resize_persistent(&mut self, new_size: usize) -> Result<(), ResizeError> {
523        if new_size > self.capacity {
524            return Err(ResizeError::ExceedsCapacity);
525        }
526        let next = self
527            .epoch
528            .get()
529            .checked_add(1)
530            .ok_or(ResizeError::EpochSaturated)?;
531        self.persistent_capacity.set(new_size);
532        self.bottom_top.set(new_size);
533        self.top_top.set(self.capacity);
534        self.bottom_peak.set(new_size);
535        self.top_peak_low.set(self.capacity);
536        self.epoch.set(next);
537        Ok(())
538    }
539
540    /// Resize the persistent (`.data`) region in place, preserving its
541    /// contents and relocating the bottom dual-headed region.
542    ///
543    /// Unlike [`Arena::resize_persistent`], which repositions both heads
544    /// to the new partition and discards the dual-headed contents, this
545    /// call preserves the persistent prefix `[0, min(old, new_size))` and
546    /// moves the bottom dual-headed bytes by the delta so they survive the
547    /// resize. A grow pushes the bottom head up, a shrink pulls it down.
548    /// The top head is anchored at the buffer ceiling and does not move.
549    ///
550    /// Returns [`ResizeError::ExceedsCapacity`] if `new_size` exceeds the
551    /// total capacity, and [`ResizeError::DualHeadedOverlap`] if a grow
552    /// would push the bottom head past the top head. In both cases the
553    /// arena is left unchanged. A saturated epoch returns
554    /// [`ResizeError::EpochSaturated`], also leaving the arena unchanged.
555    ///
556    /// On success the epoch advances, so any [`ArenaHandle`] issued before
557    /// the call reads as [`Stale`] rather than returning relocated bytes.
558    /// The bytes newly absorbed into the persistent region on a grow,
559    /// `[old, new_size)`, are not zeroed; the caller initialises them. Use
560    /// [`Arena::zero_persistent_range`] for calloc-style semantics.
561    ///
562    /// Memory-safe unconditionally, because the epoch advance invalidates
563    /// outstanding handles. A caller that intends the relocated dual-headed
564    /// bytes to remain reachable through a fresh read must hold no live
565    /// borrow into the dual-headed region across the call.
566    pub fn resize_persistent_capacity(&mut self, new_size: usize) -> Result<(), ResizeError> {
567        if new_size > self.capacity {
568            return Err(ResizeError::ExceedsCapacity);
569        }
570        let old = self.persistent_capacity.get();
571        if new_size == old {
572            return Ok(());
573        }
574        // Invariant: the bottom head never sits below the persistent
575        // boundary (`bottom_top >= persistent_capacity`), so the live
576        // bottom length is non-negative.
577        let bottom_top = self.bottom_top.get();
578        let bottom_len = bottom_top - old;
579        let new_bottom_top = new_size + bottom_len;
580        // A grow must not push the relocated bottom head past the top head.
581        // On a shrink `new_bottom_top < bottom_top <= top_top`, so the
582        // check binds only for a grow.
583        if new_bottom_top > self.top_top.get() {
584            return Err(ResizeError::DualHeadedOverlap);
585        }
586        // Validate the epoch advance before any mutation so a saturated
587        // epoch leaves the arena unchanged.
588        let next = self
589            .epoch
590            .get()
591            .checked_add(1)
592            .ok_or(ResizeError::EpochSaturated)?;
593        if bottom_len > 0 {
594            // SAFETY: `buffer` is valid for `capacity` bytes. The source
595            // `[old, bottom_top)` and destination `[new_size,
596            // new_bottom_top)` both lie within `[0, top_top) <= [0,
597            // capacity)`, guaranteed by the overlap check above. The two
598            // ranges may overlap, so `copy` (memmove semantics) is used.
599            unsafe {
600                let base = self.buffer.as_ptr();
601                core::ptr::copy(base.add(old), base.add(new_size), bottom_len);
602            }
603        }
604        self.persistent_capacity.set(new_size);
605        self.bottom_top.set(new_bottom_top);
606        // Reset the bottom watermark to the post-relocation head, matching
607        // the reconfiguration semantics of `resize_persistent`. The top
608        // head and its watermark are untouched.
609        self.bottom_peak.set(new_bottom_top);
610        self.epoch.set(next);
611        Ok(())
612    }
613
614    /// Overwrite the persistent region with zeros. Does not touch the
615    /// dual-headed region, the bump pointers, or the epoch.
616    ///
617    /// Useful for secret hygiene before assigning a pooled arena to a
618    /// new module, and for defense-in-depth where the persistent
619    /// region is read-back without intervening writes.
620    pub fn zero_persistent(&mut self) {
621        let len = self.persistent_capacity.get();
622        if len > 0 {
623            // SAFETY: `buffer` is valid for writes of `capacity`
624            // bytes, and `len <= capacity` is invariant on
625            // `persistent_capacity`.
626            unsafe {
627                core::ptr::write_bytes(self.buffer.as_ptr(), 0, len);
628            }
629        }
630    }
631
632    /// Overwrite `[start, start + len)` of the persistent region with zeros.
633    /// Does not touch the dual-headed region, the bump pointers, or the epoch.
634    /// A range that does not lie wholly within `[0, persistent_capacity)` is
635    /// rejected, leaving the arena unchanged.
636    ///
637    /// Used on a module swap to clear the persistent composite body pool tail
638    /// that follows the private-slot array, so a surviving flat body cannot be
639    /// read after its referenced rodata bytes are freed (B28 P3 item 4). A flat
640    /// Text field in a zeroed body decodes as `(ptr=0, len=0)`, which the read
641    /// path screens as an empty string rather than dereferencing null.
642    ///
643    /// Takes `&self` because the persistent region is written through raw
644    /// pointers under shared borrow elsewhere (the VM holds the arena by shared
645    /// reference and persists composite bodies through `persistent_ptr`), so a
646    /// shared-borrow zero matches that access pattern. It is the caller's
647    /// responsibility, upheld by the single-threaded VM, that no live `&[u8]`
648    /// to the zeroed bytes is held across the write.
649    pub fn zero_persistent_range(&self, start: usize, len: usize) -> Result<(), ResizeError> {
650        let pcap = self.persistent_capacity.get();
651        match start.checked_add(len) {
652            Some(end) if end <= pcap => {
653                if len > 0 {
654                    // SAFETY: `start + len <= persistent_capacity <= capacity`,
655                    // so the range lies wholly within the buffer, which is
656                    // valid for writes of `capacity` bytes.
657                    unsafe {
658                        core::ptr::write_bytes(self.buffer.as_ptr().add(start), 0, len);
659                    }
660                }
661                Ok(())
662            }
663            _ => Err(ResizeError::ExceedsCapacity),
664        }
665    }
666
667    /// Overwrite the dual-headed region with zeros and fully reset
668    /// it. Advances the epoch.
669    ///
670    /// Equivalent to a full reset followed by zeroing the bytes
671    /// outside the persistent region. The persistent region is
672    /// untouched.
673    pub fn zero_dual_headed(&mut self) -> Result<(), EpochSaturated> {
674        let start = self.persistent_capacity.get();
675        let len = self.capacity - start;
676        if len > 0 {
677            // SAFETY: The range `[start, start + len)` lies within
678            // `[0, capacity)` and is exclusively owned by the arena.
679            unsafe {
680                core::ptr::write_bytes(self.buffer.as_ptr().add(start), 0, len);
681            }
682        }
683        let next = self.epoch.get().checked_add(1).ok_or(EpochSaturated)?;
684        self.bottom_top.set(start);
685        self.top_top.set(self.capacity);
686        self.bottom_peak.set(start);
687        self.top_peak_low.set(self.capacity);
688        self.epoch.set(next);
689        Ok(())
690    }
691
692    /// Overwrite the entire backing buffer with zeros, including the
693    /// persistent region. Resets the bump pointers and advances the
694    /// epoch. Leaves the persistent capacity unchanged.
695    pub fn zero_all(&mut self) -> Result<(), EpochSaturated> {
696        // SAFETY: The full range `[0, capacity)` lies within the
697        // arena's buffer and is exclusively owned.
698        unsafe {
699            core::ptr::write_bytes(self.buffer.as_ptr(), 0, self.capacity);
700        }
701        let next = self.epoch.get().checked_add(1).ok_or(EpochSaturated)?;
702        let start = self.persistent_capacity.get();
703        self.bottom_top.set(start);
704        self.top_top.set(self.capacity);
705        self.bottom_peak.set(start);
706        self.top_peak_low.set(self.capacity);
707        self.epoch.set(next);
708        Ok(())
709    }
710
711    /// Return a snapshot of the bottom-end bump pointer for later use
712    /// with [`Arena::rewind_bottom`].
713    pub fn bottom_mark(&self) -> BottomMark {
714        BottomMark(self.bottom_top.get())
715    }
716
717    /// Return a snapshot of the top-end bump pointer for later use with
718    /// [`Arena::rewind_top`].
719    pub fn top_mark(&self) -> TopMark {
720        TopMark(self.top_top.get())
721    }
722
723    /// Reset both ends, reclaiming all allocations.
724    ///
725    /// Constant-time. Does not zero the buffer contents because
726    /// subsequent allocations will overwrite as needed. Does not clear
727    /// peak watermarks; use [`Arena::clear_peaks`] for that.
728    ///
729    /// Advances the epoch counter, invalidating every outstanding
730    /// [`ArenaHandle`]. Returns [`EpochSaturated`] if the counter is
731    /// already at `u64::MAX`. See [`Arena::force_reset_epoch`] for
732    /// recovery.
733    ///
734    /// Takes `&mut self` so the borrow checker prevents calling reset
735    /// while any handle borrows the arena. This guarantees no live
736    /// allocations through `Allocator` trait users at the moment of
737    /// reset.
738    pub fn reset(&mut self) -> Result<(), EpochSaturated> {
739        let next = self.epoch.get().checked_add(1).ok_or(EpochSaturated)?;
740        self.bottom_top.set(self.persistent_capacity.get());
741        self.top_top.set(self.capacity);
742        self.epoch.set(next);
743        Ok(())
744    }
745
746    /// Reset both ends and advance the epoch through a shared reference.
747    ///
748    /// Companion to [`Arena::reset`] for callers that hold the arena
749    /// through a shared reference and cannot temporarily acquire
750    /// exclusive access. The interior-mutable bump pointers and epoch
751    /// counter make the implementation race-free for single-threaded
752    /// use.
753    ///
754    /// # Safety
755    ///
756    /// The caller must ensure that no allocator-bound collection
757    /// holds storage in the arena at the moment of reset. Concretely,
758    /// no `allocator_api2::vec::Vec<T, BottomHandle>` or
759    /// `allocator_api2::vec::Vec<T, TopHandle>` value may have non-zero
760    /// capacity when this is called. Outstanding [`ArenaHandle`] values
761    /// are correctly invalidated by the epoch advance and remain safe.
762    ///
763    /// Returns [`EpochSaturated`] when the epoch counter is at
764    /// `u64::MAX`. Recovery is via [`Arena::force_reset_epoch`].
765    pub unsafe fn reset_unchecked(&self) -> Result<(), EpochSaturated> {
766        let next = self.epoch.get().checked_add(1).ok_or(EpochSaturated)?;
767        self.bottom_top.set(self.persistent_capacity.get());
768        self.top_top.set(self.capacity);
769        self.epoch.set(next);
770        Ok(())
771    }
772
773    /// Reset the top end and advance the epoch through a shared
774    /// reference, leaving the bottom end untouched.
775    ///
776    /// Intended for hosts that use the bottom end for long-lived
777    /// allocator-bound collections (such as an operand stack) while
778    /// using the top end for short-lived scratch (such as dynamic
779    /// strings). The epoch advance invalidates every outstanding
780    /// [`ArenaHandle`] regardless of which end produced it. This is
781    /// the desired discipline because handles do not record which end
782    /// they came from and any handle that survives a reset is by
783    /// definition stale.
784    ///
785    /// # Safety
786    ///
787    /// The caller must ensure that no allocator-bound collection
788    /// holds storage in the top end at the moment of reset. Bottom-end
789    /// allocator-bound collections are unaffected by this call and
790    /// retain their storage. Outstanding [`ArenaHandle`] values are
791    /// correctly invalidated by the epoch advance and remain safe.
792    ///
793    /// Returns [`EpochSaturated`] when the epoch counter is at
794    /// `u64::MAX`. Recovery is via [`Arena::force_reset_epoch`].
795    pub unsafe fn reset_top_unchecked(&self) -> Result<(), EpochSaturated> {
796        let next = self.epoch.get().checked_add(1).ok_or(EpochSaturated)?;
797        self.top_top.set(self.capacity);
798        self.epoch.set(next);
799        Ok(())
800    }
801
802    /// Current epoch counter value.
803    ///
804    /// Captured by [`ArenaHandle`] at construction and compared on
805    /// access. Hosts performing long-running missions may consult this
806    /// alongside [`Arena::epoch_remaining`] to schedule a graceful
807    /// restart well before saturation.
808    pub fn epoch(&self) -> u64 {
809        self.epoch.get()
810    }
811
812    /// Number of resets remaining before the epoch counter saturates.
813    pub fn epoch_remaining(&self) -> u64 {
814        u64::MAX - self.epoch.get()
815    }
816
817    /// Whether a handle whose data pointer is `addr` and whose captured epoch
818    /// is `handle_epoch` still addresses live storage, decided by the region
819    /// the pointer falls in rather than by the epoch alone.
820    ///
821    /// A pointer into the ephemeral dual-headed region, the range
822    /// `[persistent_capacity, capacity)`, is epoch-gated, because a reset
823    /// reclaims that region and advances the epoch. A pointer into the
824    /// persistent region `[0, persistent_capacity)`, or into memory this arena
825    /// does not own at all (host data or rodata reached through a flat
826    /// composite body that points outside the arena), is always live, because
827    /// a reset never reclaims those bytes.
828    ///
829    /// This is the primitive that lets a flat composite body be a single
830    /// pointer that reads its bytes in place wherever they live, rather than
831    /// copying persistent, host, or rodata bytes into the ephemeral region to
832    /// give them an epoch (B28 P3 item 5). Ephemeral handles keep the exact
833    /// epoch-gated behaviour they had before, so existing producers are
834    /// unaffected.
835    pub(crate) fn addr_is_live(&self, addr: usize, handle_epoch: u64) -> bool {
836        let base = self.buffer.as_ptr() as usize;
837        let persistent_end = base.wrapping_add(self.persistent_capacity.get());
838        let cap_end = base.wrapping_add(self.capacity);
839        if addr >= persistent_end && addr < cap_end {
840            handle_epoch == self.epoch.get()
841        } else {
842            true
843        }
844    }
845
846    /// Whether `addr` falls in the ephemeral dual-headed region, the range
847    /// `[persistent_capacity, capacity)` that a reset reclaims.
848    ///
849    /// This is the region-membership test [`Arena::addr_is_live`] uses to
850    /// decide which pointers are epoch-gated, exposed so a caller can reject a
851    /// pointer that would be reclaimed at the next reset before it is allowed
852    /// to escape a reset boundary. A pointer into the persistent region, or
853    /// into memory this arena does not own at all (host data or rodata reached
854    /// through a flat composite body), is not ephemeral and survives a reset
855    /// (B28 P3 item 4). A null pointer is not ephemeral; it addresses no
856    /// storage and must be screened separately by the caller.
857    pub fn addr_is_ephemeral(&self, addr: usize) -> bool {
858        let base = self.buffer.as_ptr() as usize;
859        let persistent_end = base.wrapping_add(self.persistent_capacity.get());
860        let cap_end = base.wrapping_add(self.capacity);
861        addr >= persistent_end && addr < cap_end
862    }
863
864    /// Reset the epoch counter to zero.
865    ///
866    /// Recovery path for [`EpochSaturated`]. Resets bump pointers as
867    /// well so the arena is in the same observable state as a freshly
868    /// constructed arena, except for retained capacity.
869    ///
870    /// # Safety
871    ///
872    /// The caller must ensure that no [`ArenaHandle`] produced under
873    /// any prior epoch is reachable. Calling this while such handles
874    /// exist invalidates the stale-detection guarantee and may permit
875    /// use after invalidation that the type system would otherwise
876    /// catch through epoch comparison.
877    ///
878    /// The intended use is recovery after a [`Arena::reset`] call has
879    /// returned [`EpochSaturated`]. The host halts every consumer of
880    /// the arena, drains every cache that holds an [`ArenaHandle`],
881    /// and only then invokes this method.
882    pub unsafe fn force_reset_epoch(&mut self) {
883        self.bottom_top.set(self.persistent_capacity.get());
884        self.top_top.set(self.capacity);
885        self.epoch.set(0);
886    }
887
888    /// Clear the peak watermarks for both ends.
889    ///
890    /// Sets each peak to the current pointer value. After this call,
891    /// peak readings reflect only allocations made after the call.
892    pub fn clear_peaks(&mut self) {
893        self.bottom_peak.set(self.bottom_top.get());
894        self.top_peak_low.set(self.top_top.get());
895    }
896
897    /// Rewind the bottom end to a previously recorded mark.
898    ///
899    /// # Safety
900    ///
901    /// The caller must ensure that no live values reference memory in
902    /// the range `[mark.0, current_bottom_top)`. References obtained
903    /// through the `Allocator` trait, including those held by
904    /// `allocator_api2::vec::Vec` and similar collections, must be
905    /// dropped or otherwise abandoned before this call. Subsequent
906    /// allocations may overwrite the rewound region, which would alias
907    /// with any retained reference and produce undefined behavior.
908    ///
909    /// Marks from a different arena are a logic error.
910    pub unsafe fn rewind_bottom(&self, mark: BottomMark) {
911        let target = mark.0.min(self.bottom_top.get());
912        self.bottom_top.set(target);
913    }
914
915    /// Rewind the top end to a previously recorded mark.
916    ///
917    /// # Safety
918    ///
919    /// Same contract as [`Arena::rewind_bottom`].
920    pub unsafe fn rewind_top(&self, mark: TopMark) {
921        let target = mark.0.max(self.top_top.get());
922        self.top_top.set(target);
923    }
924
925    /// Clear the bottom end without checking for live references.
926    ///
927    /// # Safety
928    ///
929    /// The caller must ensure no live references into the bottom region
930    /// exist. Equivalent to [`Arena::rewind_bottom`] with a mark of
931    /// zero, with the same safety contract.
932    pub unsafe fn reset_bottom(&self) {
933        self.bottom_top.set(self.persistent_capacity.get());
934    }
935
936    /// Clear the top end without checking for live references.
937    ///
938    /// # Safety
939    ///
940    /// The caller must ensure no live references into the top region
941    /// exist. Equivalent to [`Arena::rewind_top`] with a mark of
942    /// `capacity`, with the same safety contract.
943    pub unsafe fn reset_top(&self) {
944        self.top_top.set(self.capacity);
945    }
946
947    /// Returns true if the given budget fits within the arena's
948    /// capacity. The check is `budget.bottom_bytes + budget.top_bytes
949    /// <= capacity`.
950    ///
951    /// This is the generic budget contract referenced in the crate
952    /// documentation. Producers compute a budget through whatever
953    /// analysis they choose and use this method to verify admissibility
954    /// before relying on the arena.
955    pub fn fits_budget(&self, budget: &Budget) -> bool {
956        budget.total() <= self.capacity
957    }
958
959    /// Obtain a bottom-end allocation handle.
960    pub fn bottom_handle(&self) -> BottomHandle<'_> {
961        BottomHandle(self)
962    }
963
964    /// Obtain a top-end allocation handle.
965    pub fn top_handle(&self) -> TopHandle<'_> {
966        TopHandle(self)
967    }
968
969    /// Alias for [`Arena::bottom_handle`]. Suitable for code that
970    /// treats the bottom end as a stack-like region.
971    pub fn stack_handle(&self) -> BottomHandle<'_> {
972        self.bottom_handle()
973    }
974
975    /// Alias for [`Arena::top_handle`]. Suitable for code that treats
976    /// the top end as a heap-like region whose allocations are reset
977    /// together rather than freed individually.
978    pub fn heap_handle(&self) -> TopHandle<'_> {
979        self.top_handle()
980    }
981
982    /// Allocate `n` bytes from the bottom end with no alignment
983    /// requirement. Convenience wrapper for byte buffers and similar
984    /// allocations where the caller does not care about alignment.
985    ///
986    /// Equivalent to allocating with a `Layout::from_size_align(n, 1)`
987    /// through the `BottomHandle` Allocator implementation.
988    pub fn alloc_bottom_bytes(&self, n: usize) -> Result<NonNull<[u8]>, AllocError> {
989        let layout = Layout::from_size_align(n, 1).map_err(|_| AllocError)?;
990        self.alloc_bottom(layout)
991    }
992
993    /// Allocate `n` bytes from the top end with no alignment requirement.
994    pub fn alloc_top_bytes(&self, n: usize) -> Result<NonNull<[u8]>, AllocError> {
995        let layout = Layout::from_size_align(n, 1).map_err(|_| AllocError)?;
996        self.alloc_top(layout)
997    }
998
999    /// Allocate from the bottom end.
1000    ///
1001    /// Alignment is computed against the actual buffer base address, not
1002    /// the offset within the buffer. This makes the arena correct for
1003    /// buffers with any base alignment, including buffers obtained from
1004    /// allocators that only guarantee one-byte alignment and static
1005    /// arrays declared without explicit alignment annotations.
1006    fn alloc_bottom(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
1007        let cur = self.bottom_top.get();
1008        let base_addr = self.buffer.as_ptr() as usize;
1009        let cur_addr = base_addr.checked_add(cur).ok_or(AllocError)?;
1010        let align_mask = layout.align().saturating_sub(1);
1011        let aligned_addr = cur_addr.checked_add(align_mask).ok_or(AllocError)? & !align_mask;
1012        // `aligned_addr >= cur_addr >= base_addr`, so the subtraction
1013        // does not underflow.
1014        let aligned_offset = aligned_addr - base_addr;
1015        let new_top = aligned_offset
1016            .checked_add(layout.size())
1017            .ok_or(AllocError)?;
1018        if new_top > self.top_top.get() {
1019            return Err(AllocError);
1020        }
1021        self.bottom_top.set(new_top);
1022        if new_top > self.bottom_peak.get() {
1023            self.bottom_peak.set(new_top);
1024        }
1025        // SAFETY: `aligned_offset` is within `[0, top_top)` which is a
1026        // subset of `[0, capacity)`. The reserved range
1027        // `[aligned_offset, new_top)` is exclusive to this allocation
1028        // until the next reset or rewind.
1029        let ptr = unsafe { self.buffer.as_ptr().add(aligned_offset) };
1030        let slice = core::ptr::slice_from_raw_parts_mut(ptr, layout.size());
1031        NonNull::new(slice).ok_or(AllocError)
1032    }
1033
1034    /// Allocate from the top end.
1035    ///
1036    /// Alignment is computed against the actual buffer base address.
1037    fn alloc_top(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
1038        let cur = self.top_top.get();
1039        let new_end_offset = cur.checked_sub(layout.size()).ok_or(AllocError)?;
1040        let base_addr = self.buffer.as_ptr() as usize;
1041        let new_end_addr = base_addr.checked_add(new_end_offset).ok_or(AllocError)?;
1042        let align_mask = layout.align().saturating_sub(1);
1043        // Round down to alignment. The result may be less than
1044        // `base_addr` if the buffer base is itself misaligned and the
1045        // allocation is near the bottom of the buffer; that case fails.
1046        let aligned_addr = new_end_addr & !align_mask;
1047        if aligned_addr < base_addr {
1048            return Err(AllocError);
1049        }
1050        let aligned_offset = aligned_addr - base_addr;
1051        if aligned_offset < self.bottom_top.get() {
1052            return Err(AllocError);
1053        }
1054        self.top_top.set(aligned_offset);
1055        if aligned_offset < self.top_peak_low.get() {
1056            self.top_peak_low.set(aligned_offset);
1057        }
1058        // SAFETY: `aligned_offset` is within `[bottom_top, capacity)`
1059        // and the reserved range `[aligned_offset, aligned_offset + size)`
1060        // is exclusive to this allocation until the next reset or
1061        // rewind.
1062        let ptr = unsafe { self.buffer.as_ptr().add(aligned_offset) };
1063        let slice = core::ptr::slice_from_raw_parts_mut(ptr, layout.size());
1064        NonNull::new(slice).ok_or(AllocError)
1065    }
1066}
1067
1068impl core::fmt::Debug for Arena {
1069    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1070        f.debug_struct("Arena")
1071            .field("capacity", &self.capacity)
1072            .field("bottom_used", &self.bottom_used())
1073            .field("top_used", &self.top_used())
1074            .field("free", &self.free())
1075            .field("bottom_peak", &self.bottom_peak())
1076            .field("top_peak", &self.top_peak())
1077            .finish()
1078    }
1079}
1080
1081// Soundness audit for the explicit `Drop` impl below.
1082//
1083// The arena holds a raw `NonNull<u8>` pointer to the backing storage.
1084// The `storage` field tracks ownership.
1085//
1086// - `Storage::External`: the caller owns the buffer. The `Drop` impl
1087//   does not free it. The caller's safety contracts on
1088//   `Arena::from_static_buffer` and `Arena::from_buffer_unchecked`
1089//   require the buffer to outlive the arena.
1090// - `Storage::Owned`: the arena owns the heap allocation that backs
1091//   the buffer. The `Drop` impl reconstitutes a `Box<[u8]>` from the
1092//   raw pointer and drops it, releasing the buffer.
1093//
1094// The buffer pointer has raw provenance (derived from `Box::into_raw`)
1095// so that handle allocations through a shared `&Arena` reference do
1096// not run afoul of stacked-borrows or tree-borrows aliasing rules.
1097impl Drop for Arena {
1098    fn drop(&mut self) {
1099        #[cfg(feature = "alloc")]
1100        if matches!(self.storage, Storage::Owned) && self.capacity > 0 {
1101            use alloc::alloc::{Layout as AllocLayout, dealloc};
1102            // SAFETY: When `storage` is `Owned` with non-zero capacity,
1103            // the buffer was obtained from `alloc_zeroed` with this
1104            // exact layout. The same layout is used for `dealloc`. The
1105            // arena is being dropped, so no further access to the
1106            // buffer occurs after this point.
1107            let layout = unsafe { AllocLayout::from_size_align_unchecked(self.capacity, 16) };
1108            unsafe { dealloc(self.buffer.as_ptr(), layout) };
1109        }
1110    }
1111}
1112
1113/// Allocation handle for the bottom end of an arena.
1114///
1115/// Implements `allocator_api2::alloc::Allocator`. Use with constructors
1116/// such as `allocator_api2::vec::Vec::new_in(arena.bottom_handle())`.
1117#[derive(Clone, Copy, Debug)]
1118pub struct BottomHandle<'a>(&'a Arena);
1119
1120/// Allocation handle for the top end of an arena.
1121///
1122/// Implements `allocator_api2::alloc::Allocator`. Use with constructors
1123/// such as `allocator_api2::vec::Vec::new_in(arena.top_handle())`.
1124#[derive(Clone, Copy, Debug)]
1125pub struct TopHandle<'a>(&'a Arena);
1126
1127// SAFETY: The arena's allocation methods uphold the `Allocator`
1128// contract. Returned pointers are valid for the requested layout,
1129// unique to the caller, and remain valid until the next reset or
1130// rewind. Deallocation is a no-op because the bump allocator reclaims
1131// memory in bulk.
1132unsafe impl Allocator for BottomHandle<'_> {
1133    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
1134        self.0.alloc_bottom(layout)
1135    }
1136
1137    unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: Layout) {
1138        // No-op. Bump allocator reclaims at reset.
1139    }
1140}
1141
1142// SAFETY: Same reasoning as `BottomHandle`.
1143unsafe impl Allocator for TopHandle<'_> {
1144    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
1145        self.0.alloc_top(layout)
1146    }
1147
1148    unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: Layout) {
1149        // No-op. Bump allocator reclaims at reset.
1150    }
1151}
1152
1153/// Lifetime-free safe handle to a value stored in an arena.
1154///
1155/// Stores a raw pointer to a value of type `T` together with the epoch
1156/// at which the value was allocated. Access goes through [`ArenaHandle::get`],
1157/// which takes a borrow of the arena and validates the epoch. A mismatch
1158/// returns [`Stale`].
1159///
1160/// `ArenaHandle` does not borrow the arena directly. This makes it safe
1161/// to embed inside types whose lifetime is unrelated to the arena, such
1162/// as a runtime value enum that flows through caches and channels in
1163/// the host. The trade-off is that every dereference requires explicit
1164/// arena context. The wrapper does not implement `Deref` for that
1165/// reason.
1166///
1167/// `T: ?Sized` is supported. `T = str` and `T = [U]` are the canonical
1168/// unsized cases; the wide pointer carries the slice length alongside
1169/// the data pointer. Higher-level helpers (for example a string handle
1170/// in a downstream crate) build on top of this generic mechanism by
1171/// allocating storage in the arena and wrapping the resulting pointer
1172/// through [`ArenaHandle::from_raw_parts`].
1173///
1174/// # Safety contract
1175///
1176/// The pointer must reference a region of the same arena that produced
1177/// the handle. The region must remain unmodified across resets while
1178/// the epoch is unchanged. The constructors in this crate uphold this
1179/// contract. Hand-rolled construction through public fields is not
1180/// possible because the fields are private.
1181///
1182/// # Serialization
1183///
1184/// `ArenaHandle` is intentionally not serializable. Its pointer is not
1185/// stable across processes. Long-lived deployments must convert handles
1186/// to owned bytes before checkpointing.
1187pub struct ArenaHandle<T: ?Sized> {
1188    ptr: NonNull<T>,
1189    epoch: u64,
1190}
1191
1192// SAFETY: `ArenaHandle` is `Copy` for any `T: ?Sized` because both
1193// fields are `Copy`. `NonNull<T>` is `Copy` for unsized `T`.
1194impl<T: ?Sized> Copy for ArenaHandle<T> {}
1195
1196impl<T: ?Sized> Clone for ArenaHandle<T> {
1197    fn clone(&self) -> Self {
1198        *self
1199    }
1200}
1201
1202impl<T: ?Sized> core::fmt::Debug for ArenaHandle<T> {
1203    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1204        f.debug_struct("ArenaHandle")
1205            .field("ptr", &self.ptr.as_ptr())
1206            .field("epoch", &self.epoch)
1207            .finish()
1208    }
1209}
1210
1211// `ArenaHandle` is intentionally not `Send` or `Sync` because the
1212// arena it references is single-threaded. The pointer is `*mut` under
1213// `NonNull`, which inherits the conservative auto-trait posture.
1214
1215impl<T> ArenaHandle<[T]> {
1216    /// The number of elements the handle addresses, read from the fat
1217    /// pointer's length metadata without dereferencing the storage or
1218    /// consulting the arena. Unlike resolving the bytes, this is valid even
1219    /// after a `RESET`: the length is inert metadata. Lets an arena-less
1220    /// caller (for example a display path) learn a body's size without the
1221    /// arena that would be needed to read its contents.
1222    pub fn len(&self) -> usize {
1223        self.ptr.len()
1224    }
1225
1226    /// Whether the handle addresses zero elements. See [`ArenaHandle::len`].
1227    pub fn is_empty(&self) -> bool {
1228        self.len() == 0
1229    }
1230}
1231
1232impl<T: ?Sized> ArenaHandle<T> {
1233    /// Construct a handle from raw parts.
1234    ///
1235    /// Used by higher-level helpers that allocate typed storage in the
1236    /// arena (for example a string or boxed-value helper) and want to
1237    /// wrap the resulting pointer in a stale-detecting handle.
1238    ///
1239    /// # Safety
1240    ///
1241    /// The caller must guarantee both of the following until the next
1242    /// arena reset.
1243    ///
1244    /// - `ptr` references storage that lives in the arena whose
1245    ///   `epoch()` returned `epoch` at allocation time, and the storage
1246    ///   is initialised and aligned for `T`.
1247    /// - The bytes addressed by `ptr` are not aliased by any other
1248    ///   live reference for as long as the handle is held.
1249    ///
1250    /// Mixing handles between arenas is a logic error: passing the
1251    /// wrong arena to [`ArenaHandle::get`] would dereference memory
1252    /// that belongs to a different allocator if the wrong arena's
1253    /// epoch happened to match.
1254    pub unsafe fn from_raw_parts(ptr: NonNull<T>, epoch: u64) -> Self {
1255        Self { ptr, epoch }
1256    }
1257
1258    /// Resolve the handle against the arena that produced it.
1259    ///
1260    /// Returns [`Stale`] if the arena has been reset since the handle
1261    /// was issued. The borrow of `arena` ties the returned reference's
1262    /// lifetime to the arena, preventing the reference from outliving
1263    /// the next reset.
1264    ///
1265    /// # Safety
1266    ///
1267    /// The arena must be the same arena that produced the handle. Mixing
1268    /// handles between arenas is a logic error. The arena allocations
1269    /// are uniquely owned by the arena, so passing the wrong arena will
1270    /// dereference memory that is not the original allocation. This
1271    /// would be unsound if the wrong arena's epoch happened to match.
1272    pub fn get<'a>(&self, arena: &'a Arena) -> Result<&'a T, Stale> {
1273        // Validity is decided by the region the pointer falls in, not by the
1274        // epoch alone (B28 P3 item 5). An ephemeral pointer is epoch-gated,
1275        // exactly as before; a pointer into the persistent region or into
1276        // memory outside this arena is always live because a reset never
1277        // reclaims it. The address is the thin (data) part of the pointer.
1278        let addr = self.ptr.as_ptr() as *const () as usize;
1279        if !arena.addr_is_live(addr, self.epoch) {
1280            return Err(Stale);
1281        }
1282        // SAFETY: The pointer addresses live storage per the region check
1283        // above: an ephemeral handle under the current epoch, or a persistent
1284        // or external pointer the arena never reclaims. The arena guarantees
1285        // allocated regions remain intact until reclaimed.
1286        Ok(unsafe { self.ptr.as_ref() })
1287    }
1288
1289    /// Epoch captured when the handle was issued.
1290    pub fn epoch(&self) -> u64 {
1291        self.epoch
1292    }
1293
1294    /// The raw wide pointer this handle wraps.
1295    ///
1296    /// Reading the pointer value (and its slice/str length metadata) is
1297    /// safe; dereferencing it is sound only under the handle's epoch, the
1298    /// same contract [`ArenaHandle::get`] enforces. A higher-level helper
1299    /// that stores a handle's raw `(pointer, length)` in a flat byte body
1300    /// and later rebuilds it through [`ArenaHandle::from_raw_parts`] uses
1301    /// this accessor for the store side.
1302    pub fn as_non_null(&self) -> NonNull<T> {
1303        self.ptr
1304    }
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309    extern crate alloc as test_alloc;
1310
1311    use super::*;
1312    use allocator_api2::vec::Vec as ArenaVec;
1313
1314    #[cfg(feature = "alloc")]
1315    #[test]
1316    fn arena_with_capacity() {
1317        let arena = Arena::with_capacity(1024);
1318        assert_eq!(arena.capacity(), 1024);
1319        assert_eq!(arena.bottom_used(), 0);
1320        assert_eq!(arena.top_used(), 0);
1321        assert_eq!(arena.free(), 1024);
1322        assert_eq!(arena.bottom_peak(), 0);
1323        assert_eq!(arena.top_peak(), 0);
1324    }
1325
1326    // Skipped under miri because the test deliberately leaks a Vec to
1327    // synthesize a `'static mut [u8]`. Real embedded use of
1328    // `from_static_buffer` is a `static mut` array, which has no leak.
1329    #[cfg_attr(miri, ignore)]
1330    #[test]
1331    fn arena_from_static_buffer() {
1332        // Use a leaked Box for a 'static-like buffer in tests. In real
1333        // embedded use, this would be a `static mut [u8; N]`.
1334        let leaked: &'static mut [u8] = test_alloc::vec![0u8; 256].leak();
1335        let arena = Arena::from_static_buffer(leaked);
1336        assert_eq!(arena.capacity(), 256);
1337        let layout = Layout::new::<u64>();
1338        let p = arena.bottom_handle().allocate(layout).unwrap();
1339        // The leaked Vec<u8> has alignment-of-u8 (one byte) per Rust's
1340        // contract. The arena pads as needed to satisfy the requested
1341        // u64 alignment, so usage is at least size and at most
1342        // size + alignment.
1343        assert!(arena.bottom_used() >= 8);
1344        assert!(arena.bottom_used() <= 8 + 8);
1345        let addr = p.as_ptr() as *const u8 as usize;
1346        assert_eq!(addr % 8, 0);
1347    }
1348
1349    #[test]
1350    fn arena_from_buffer_unchecked() {
1351        let mut buffer = test_alloc::vec![0u8; 128];
1352        let ptr = buffer.as_mut_ptr();
1353        let len = buffer.len();
1354        // SAFETY: `buffer` outlives the arena because we hold it until
1355        // the test ends, and we do not access it through `buffer` while
1356        // the arena is in use.
1357        let arena = unsafe { Arena::from_buffer_unchecked(ptr, len) };
1358        assert_eq!(arena.capacity(), 128);
1359        let layout = Layout::new::<u32>();
1360        let _p = arena.bottom_handle().allocate(layout).unwrap();
1361        // The buffer base may be any alignment for from_buffer_unchecked.
1362        // The arena pads to satisfy the requested alignment, so usage
1363        // is at least the layout size and at most size + alignment.
1364        assert!(arena.bottom_used() >= 4);
1365        assert!(arena.bottom_used() <= 4 + 4);
1366        drop(arena);
1367        // `buffer` is still alive here.
1368        assert_eq!(buffer.len(), 128);
1369    }
1370
1371    #[cfg(feature = "alloc")]
1372    #[test]
1373    fn arena_dual_end() {
1374        let arena = Arena::with_capacity(64);
1375        let layout = Layout::new::<u64>();
1376        let _b = arena.bottom_handle().allocate(layout).unwrap();
1377        let _t = arena.top_handle().allocate(layout).unwrap();
1378        assert_eq!(arena.bottom_used(), 8);
1379        assert_eq!(arena.top_used(), 8);
1380        assert_eq!(arena.free(), 48);
1381    }
1382
1383    #[cfg(feature = "alloc")]
1384    #[test]
1385    fn arena_alignment() {
1386        let arena = Arena::with_capacity(64);
1387        let _byte = arena.bottom_handle().allocate(Layout::new::<u8>()).unwrap();
1388        let p_u64 = arena
1389            .bottom_handle()
1390            .allocate(Layout::new::<u64>())
1391            .unwrap();
1392        let addr = p_u64.as_ptr() as *const u8 as usize;
1393        assert_eq!(addr % 8, 0);
1394    }
1395
1396    #[cfg(feature = "alloc")]
1397    #[test]
1398    fn arena_exhaustion() {
1399        let arena = Arena::with_capacity(16);
1400        let layout = Layout::new::<u64>();
1401        let _a = arena.bottom_handle().allocate(layout).unwrap();
1402        let _b = arena.bottom_handle().allocate(layout).unwrap();
1403        assert!(arena.bottom_handle().allocate(layout).is_err());
1404    }
1405
1406    #[cfg(feature = "alloc")]
1407    #[test]
1408    fn arena_reset() {
1409        let mut arena = Arena::with_capacity(64);
1410        let layout = Layout::new::<u64>();
1411        {
1412            let _b = arena.bottom_handle().allocate(layout).unwrap();
1413            let _t = arena.top_handle().allocate(layout).unwrap();
1414        }
1415        assert_eq!(arena.bottom_used(), 8);
1416        assert_eq!(arena.top_used(), 8);
1417        arena.reset().unwrap();
1418        assert_eq!(arena.bottom_used(), 0);
1419        assert_eq!(arena.top_used(), 0);
1420        assert_eq!(arena.epoch(), 1);
1421    }
1422
1423    #[cfg(feature = "alloc")]
1424    #[test]
1425    fn arena_peak_tracking() {
1426        let arena = Arena::with_capacity(128);
1427        let layout = Layout::new::<u64>();
1428        let mark = arena.bottom_mark();
1429        let _a = arena.bottom_handle().allocate(layout).unwrap();
1430        let _b = arena.bottom_handle().allocate(layout).unwrap();
1431        assert_eq!(arena.bottom_peak(), 16);
1432        // Rewind reduces current usage but not the peak.
1433        // SAFETY: Drops happen at scope end, and we are about to
1434        // re-allocate. The peak observation is from before any rewind.
1435        unsafe {
1436            arena.rewind_bottom(mark);
1437        }
1438        assert_eq!(arena.bottom_used(), 0);
1439        assert_eq!(arena.bottom_peak(), 16);
1440    }
1441
1442    #[cfg(feature = "alloc")]
1443    #[test]
1444    fn arena_clear_peaks() {
1445        let mut arena = Arena::with_capacity(64);
1446        let layout = Layout::new::<u64>();
1447        let _a = arena.bottom_handle().allocate(layout).unwrap();
1448        assert_eq!(arena.bottom_peak(), 8);
1449        arena.reset().unwrap();
1450        assert_eq!(arena.bottom_used(), 0);
1451        // Peak persists after reset.
1452        assert_eq!(arena.bottom_peak(), 8);
1453        arena.clear_peaks();
1454        assert_eq!(arena.bottom_peak(), 0);
1455    }
1456
1457    #[cfg(feature = "alloc")]
1458    #[test]
1459    fn arena_mark_rewind() {
1460        let arena = Arena::with_capacity(128);
1461        let layout = Layout::new::<u32>();
1462        let mark = arena.bottom_mark();
1463        let _a = arena.bottom_handle().allocate(layout).unwrap();
1464        let _b = arena.bottom_handle().allocate(layout).unwrap();
1465        assert_eq!(arena.bottom_used(), 8);
1466        // SAFETY: We have not retained any references to the
1467        // allocations beyond this scope. The handles' allocations are
1468        // raw pointers that we are not using past this point.
1469        unsafe {
1470            arena.rewind_bottom(mark);
1471        }
1472        assert_eq!(arena.bottom_used(), 0);
1473    }
1474
1475    #[cfg(feature = "alloc")]
1476    #[test]
1477    fn arena_per_end_reset() {
1478        let arena = Arena::with_capacity(64);
1479        let layout = Layout::new::<u64>();
1480        let _b = arena.bottom_handle().allocate(layout).unwrap();
1481        let _t = arena.top_handle().allocate(layout).unwrap();
1482        // SAFETY: No retained allocations.
1483        unsafe {
1484            arena.reset_bottom();
1485        }
1486        assert_eq!(arena.bottom_used(), 0);
1487        assert_eq!(arena.top_used(), 8);
1488        // SAFETY: No retained allocations.
1489        unsafe {
1490            arena.reset_top();
1491        }
1492        assert_eq!(arena.top_used(), 0);
1493    }
1494
1495    #[cfg(feature = "alloc")]
1496    #[test]
1497    fn arena_vec_integration() {
1498        let arena = Arena::with_capacity(2048);
1499        let mut v: ArenaVec<i64, _> = ArenaVec::new_in(arena.bottom_handle());
1500        for i in 0..10 {
1501            v.push(i);
1502        }
1503        assert_eq!(v.iter().sum::<i64>(), 45);
1504        assert!(arena.bottom_used() > 0);
1505    }
1506
1507    #[cfg(feature = "alloc")]
1508    #[test]
1509    fn epoch_advances_on_reset() {
1510        let mut arena = Arena::with_capacity(64);
1511        assert_eq!(arena.epoch(), 0);
1512        arena.reset().unwrap();
1513        assert_eq!(arena.epoch(), 1);
1514        arena.reset().unwrap();
1515        assert_eq!(arena.epoch(), 2);
1516    }
1517
1518    #[cfg(feature = "alloc")]
1519    #[test]
1520    fn epoch_saturates() {
1521        let mut arena = Arena::with_capacity(16);
1522        // Force the epoch to one below saturation.
1523        arena.epoch.set(u64::MAX - 1);
1524        // First reset advances to u64::MAX.
1525        arena.reset().unwrap();
1526        assert_eq!(arena.epoch(), u64::MAX);
1527        assert_eq!(arena.epoch_remaining(), 0);
1528        // Second reset must refuse.
1529        let result = arena.reset();
1530        assert!(matches!(result, Err(EpochSaturated)));
1531    }
1532
1533    #[cfg(feature = "alloc")]
1534    #[test]
1535    fn force_reset_epoch_recovers() {
1536        let mut arena = Arena::with_capacity(16);
1537        arena.epoch.set(u64::MAX);
1538        assert!(matches!(arena.reset(), Err(EpochSaturated)));
1539        // SAFETY: No `ArenaHandle` exists in this test scope.
1540        unsafe {
1541            arena.force_reset_epoch();
1542        }
1543        assert_eq!(arena.epoch(), 0);
1544        arena.reset().unwrap();
1545        assert_eq!(arena.epoch(), 1);
1546    }
1547
1548    #[cfg(feature = "alloc")]
1549    fn alloc_u64(arena: &Arena, value: u64) -> ArenaHandle<u64> {
1550        use allocator_api2::alloc::Allocator;
1551        use core::alloc::Layout;
1552        let layout = Layout::new::<u64>();
1553        let raw = arena.top_handle().allocate(layout).expect("alloc");
1554        let typed: NonNull<u64> = raw.cast();
1555        // SAFETY: `typed` is freshly allocated unique storage of the
1556        // correct layout for `u64`.
1557        unsafe { typed.as_ptr().write(value) };
1558        // SAFETY: `typed` references storage in `arena`'s top region
1559        // freshly allocated under the current epoch.
1560        unsafe { ArenaHandle::from_raw_parts(typed, arena.epoch()) }
1561    }
1562
1563    #[cfg(feature = "alloc")]
1564    #[test]
1565    fn arena_handle_from_raw_parts_roundtrip() {
1566        let arena = Arena::with_capacity(256);
1567        let handle = alloc_u64(&arena, 0xdeadbeef);
1568        assert_eq!(*handle.get(&arena).unwrap(), 0xdeadbeef);
1569    }
1570
1571    #[cfg(feature = "alloc")]
1572    #[test]
1573    fn arena_handle_stale_after_reset() {
1574        let mut arena = Arena::with_capacity(256);
1575        let handle = alloc_u64(&arena, 7);
1576        assert_eq!(*handle.get(&arena).unwrap(), 7);
1577        arena.reset().unwrap();
1578        assert!(matches!(handle.get(&arena), Err(Stale)));
1579    }
1580
1581    #[cfg(feature = "alloc")]
1582    #[test]
1583    fn arena_handle_is_copy() {
1584        let arena = Arena::with_capacity(256);
1585        let handle = alloc_u64(&arena, 99);
1586        let copy = handle;
1587        assert_eq!(*handle.get(&arena).unwrap(), 99);
1588        assert_eq!(*copy.get(&arena).unwrap(), 99);
1589    }
1590
1591    #[cfg(feature = "alloc")]
1592    #[test]
1593    fn arena_dual_vec_integration() {
1594        let arena = Arena::with_capacity(4096);
1595        let mut bot: ArenaVec<i64, _> = ArenaVec::new_in(arena.bottom_handle());
1596        let mut top: ArenaVec<i64, _> = ArenaVec::new_in(arena.top_handle());
1597        for i in 0..5 {
1598            bot.push(i);
1599            top.push(i * 100);
1600        }
1601        assert_eq!(bot.len(), 5);
1602        assert_eq!(top.len(), 5);
1603        assert!(arena.bottom_used() > 0);
1604        assert!(arena.top_used() > 0);
1605    }
1606
1607    #[cfg(feature = "alloc")]
1608    #[test]
1609    fn budget_fits() {
1610        let arena = Arena::with_capacity(1024);
1611        assert!(arena.fits_budget(&Budget::new(512, 256)));
1612        assert!(arena.fits_budget(&Budget::new(0, 0)));
1613        assert!(arena.fits_budget(&Budget::new(1024, 0)));
1614        assert!(!arena.fits_budget(&Budget::new(513, 512)));
1615        assert!(!arena.fits_budget(&Budget::new(usize::MAX, 1)));
1616    }
1617
1618    #[test]
1619    fn budget_total_saturates() {
1620        let b = Budget::new(usize::MAX, 1);
1621        assert_eq!(b.total(), usize::MAX);
1622    }
1623
1624    #[cfg(feature = "alloc")]
1625    #[test]
1626    fn arena_zero_capacity() {
1627        let arena = Arena::with_capacity(0);
1628        assert!(arena.bottom_handle().allocate(Layout::new::<u8>()).is_err());
1629        assert!(arena.fits_budget(&Budget::new(0, 0)));
1630    }
1631
1632    #[cfg(feature = "alloc")]
1633    #[test]
1634    fn arena_zero_size_layout() {
1635        let arena = Arena::with_capacity(64);
1636        let layout = Layout::new::<()>();
1637        assert!(arena.bottom_handle().allocate(layout).is_ok());
1638        assert_eq!(arena.bottom_used(), 0);
1639    }
1640
1641    #[test]
1642    fn arena_misaligned_base_produces_aligned_allocation() {
1643        // Construct an arena over a buffer whose base address is
1644        // deliberately offset by one byte from the underlying storage.
1645        // The base is therefore at most byte-aligned. The arena must
1646        // still produce u64-aligned pointers for u64 allocations.
1647        let mut backing = test_alloc::vec![0u8; 256];
1648        let raw_ptr = backing.as_mut_ptr();
1649        // SAFETY: The backing vector lives until the end of the test.
1650        // We deliberately offset by one to create a misaligned base.
1651        let arena = unsafe { Arena::from_buffer_unchecked(raw_ptr.add(1), 200) };
1652
1653        // Allocate a u64. The pointer must be 8-byte aligned regardless
1654        // of the misaligned base.
1655        let p_u64 = arena
1656            .bottom_handle()
1657            .allocate(Layout::new::<u64>())
1658            .unwrap();
1659        let addr = p_u64.as_ptr() as *const u8 as usize;
1660        assert_eq!(addr % 8, 0, "allocation must be 8-byte aligned");
1661
1662        // Allocate a u128. The pointer must be 16-byte aligned.
1663        let p_u128 = arena
1664            .bottom_handle()
1665            .allocate(Layout::new::<u128>())
1666            .unwrap();
1667        let addr = p_u128.as_ptr() as *const u8 as usize;
1668        assert_eq!(addr % 16, 0, "allocation must be 16-byte aligned");
1669
1670        // Top-end allocation also aligned.
1671        let p_top = arena.top_handle().allocate(Layout::new::<u64>()).unwrap();
1672        let addr = p_top.as_ptr() as *const u8 as usize;
1673        assert_eq!(addr % 8, 0, "top allocation must be 8-byte aligned");
1674
1675        // Keep `backing` alive until here.
1676        drop(backing);
1677    }
1678
1679    #[cfg(feature = "alloc")]
1680    #[test]
1681    fn arena_byte_allocation_packs_tightly() {
1682        // alloc_bottom_bytes does not enforce alignment. Three u8
1683        // allocations of one byte each consume exactly three bytes.
1684        let arena = Arena::with_capacity(64);
1685        let _a = arena.alloc_bottom_bytes(1).unwrap();
1686        let _b = arena.alloc_bottom_bytes(1).unwrap();
1687        let _c = arena.alloc_bottom_bytes(1).unwrap();
1688        assert_eq!(arena.bottom_used(), 3);
1689    }
1690
1691    #[cfg(feature = "alloc")]
1692    #[test]
1693    fn arena_aligned_allocation_pads() {
1694        // After one byte, an aligned u64 allocation pads to align 8.
1695        // Total used should be 8 + 8 = 16 bytes.
1696        let arena = Arena::with_capacity(64);
1697        let _a = arena.alloc_bottom_bytes(1).unwrap();
1698        assert_eq!(arena.bottom_used(), 1);
1699        let _b = arena
1700            .bottom_handle()
1701            .allocate(Layout::new::<u64>())
1702            .unwrap();
1703        assert_eq!(arena.bottom_used(), 16);
1704    }
1705
1706    #[cfg(feature = "alloc")]
1707    #[test]
1708    fn arena_top_byte_allocation() {
1709        let arena = Arena::with_capacity(64);
1710        let _a = arena.alloc_top_bytes(3).unwrap();
1711        assert_eq!(arena.top_used(), 3);
1712    }
1713
1714    #[cfg(feature = "alloc")]
1715    #[test]
1716    fn arena_byte_allocation_zero_size() {
1717        let arena = Arena::with_capacity(64);
1718        // Zero-size byte allocation is admissible and consumes nothing.
1719        let _a = arena.alloc_bottom_bytes(0).unwrap();
1720        assert_eq!(arena.bottom_used(), 0);
1721    }
1722
1723    // --- Persistent region tests (added in v0.3.0). ---
1724
1725    #[cfg(feature = "alloc")]
1726    #[test]
1727    fn persistent_default_is_zero() {
1728        let arena = Arena::with_capacity(64);
1729        assert_eq!(arena.persistent_capacity(), 0);
1730        assert_eq!(arena.dual_headed_capacity(), 64);
1731        assert_eq!(arena.capacity(), 64);
1732    }
1733
1734    #[cfg(feature = "alloc")]
1735    #[test]
1736    fn resize_persistent_shifts_bottom_origin() {
1737        let mut arena = Arena::with_capacity(64);
1738        arena.resize_persistent(16).unwrap();
1739        assert_eq!(arena.persistent_capacity(), 16);
1740        assert_eq!(arena.dual_headed_capacity(), 48);
1741        // Bottom usage measured relative to the bottom region's start.
1742        assert_eq!(arena.bottom_used(), 0);
1743        // Free space equals dual_headed_capacity initially.
1744        assert_eq!(arena.free(), 48);
1745        let _a = arena.alloc_bottom_bytes(8).unwrap();
1746        assert_eq!(arena.bottom_used(), 8);
1747    }
1748
1749    #[cfg(feature = "alloc")]
1750    #[test]
1751    fn resize_persistent_rejects_oversize() {
1752        let mut arena = Arena::with_capacity(64);
1753        assert!(matches!(
1754            arena.resize_persistent(65),
1755            Err(ResizeError::ExceedsCapacity)
1756        ));
1757        // State unchanged on rejection.
1758        assert_eq!(arena.persistent_capacity(), 0);
1759    }
1760
1761    #[cfg(feature = "alloc")]
1762    #[test]
1763    fn resize_persistent_capacity_grow_preserves_and_relocates() {
1764        let mut arena = Arena::with_capacity(256);
1765        // Establish a 16-byte persistent region and fill it with a marker.
1766        arena.resize_persistent(16).unwrap();
1767        let base = arena.persistent_ptr().as_ptr();
1768        unsafe { core::ptr::write_bytes(base, 0xAB, 16) };
1769        // Allocate 8 bottom bytes and stamp them with a distinct pattern.
1770        let a = arena.alloc_bottom_bytes(8).unwrap();
1771        unsafe { core::ptr::write_bytes(a.as_ptr() as *mut u8, 0xCD, 8) };
1772        assert_eq!(arena.bottom_used(), 8);
1773        let epoch_before = arena.epoch();
1774        // Grow the persistent region by 16 bytes.
1775        arena.resize_persistent_capacity(32).unwrap();
1776        assert_eq!(arena.persistent_capacity(), 32);
1777        // The persistent prefix [0, 16) is preserved.
1778        unsafe {
1779            for i in 0..16 {
1780                assert_eq!(core::ptr::read(base.add(i)), 0xAB, "persistent byte {i}");
1781            }
1782        }
1783        // Bottom usage is unchanged; the bottom region shifted up by 16.
1784        assert_eq!(arena.bottom_used(), 8);
1785        // The relocated bottom bytes now live at offset 32.
1786        unsafe {
1787            for i in 0..8 {
1788                assert_eq!(
1789                    core::ptr::read(base.add(32 + i)),
1790                    0xCD,
1791                    "relocated byte {i}"
1792                );
1793            }
1794        }
1795        // The epoch advanced once.
1796        assert_eq!(arena.epoch(), epoch_before + 1);
1797    }
1798
1799    #[cfg(feature = "alloc")]
1800    #[test]
1801    fn resize_persistent_capacity_shrink_pulls_bottom_down() {
1802        let mut arena = Arena::with_capacity(256);
1803        arena.resize_persistent(32).unwrap();
1804        let base = arena.persistent_ptr().as_ptr();
1805        let a = arena.alloc_bottom_bytes(8).unwrap();
1806        unsafe { core::ptr::write_bytes(a.as_ptr() as *mut u8, 0xEE, 8) };
1807        assert_eq!(arena.bottom_used(), 8);
1808        // Shrink the persistent region to 16 bytes; the bottom region
1809        // follows it down.
1810        arena.resize_persistent_capacity(16).unwrap();
1811        assert_eq!(arena.persistent_capacity(), 16);
1812        assert_eq!(arena.bottom_used(), 8);
1813        unsafe {
1814            for i in 0..8 {
1815                assert_eq!(core::ptr::read(base.add(16 + i)), 0xEE, "byte {i}");
1816            }
1817        }
1818    }
1819
1820    #[cfg(feature = "alloc")]
1821    #[test]
1822    fn resize_persistent_capacity_grow_overlap_errors_and_leaves_unchanged() {
1823        let mut arena = Arena::with_capacity(64);
1824        arena.resize_persistent(16).unwrap();
1825        // Consume the dual-headed region from both ends so a grow would
1826        // collide the heads: bottom uses 8, top uses 40, free = 0.
1827        let _a = arena.alloc_bottom_bytes(8).unwrap();
1828        let _t = arena.alloc_top_bytes(40).unwrap();
1829        let epoch_before = arena.epoch();
1830        assert!(matches!(
1831            arena.resize_persistent_capacity(17),
1832            Err(ResizeError::DualHeadedOverlap)
1833        ));
1834        // State unchanged on rejection.
1835        assert_eq!(arena.persistent_capacity(), 16);
1836        assert_eq!(arena.epoch(), epoch_before);
1837    }
1838
1839    #[cfg(feature = "alloc")]
1840    #[test]
1841    fn resize_persistent_capacity_rejects_oversize_unchanged() {
1842        let mut arena = Arena::with_capacity(64);
1843        arena.resize_persistent(16).unwrap();
1844        let epoch_before = arena.epoch();
1845        assert!(matches!(
1846            arena.resize_persistent_capacity(65),
1847            Err(ResizeError::ExceedsCapacity)
1848        ));
1849        assert_eq!(arena.persistent_capacity(), 16);
1850        assert_eq!(arena.epoch(), epoch_before);
1851    }
1852
1853    #[cfg(feature = "alloc")]
1854    #[test]
1855    fn resize_persistent_capacity_noop_when_unchanged_keeps_epoch() {
1856        let mut arena = Arena::with_capacity(64);
1857        arena.resize_persistent(16).unwrap();
1858        let epoch_before = arena.epoch();
1859        arena.resize_persistent_capacity(16).unwrap();
1860        assert_eq!(arena.persistent_capacity(), 16);
1861        // A no-op resize does not churn the epoch.
1862        assert_eq!(arena.epoch(), epoch_before);
1863    }
1864
1865    #[cfg(feature = "alloc")]
1866    #[test]
1867    fn resize_persistent_capacity_invalidates_outstanding_handles() {
1868        let mut arena = Arena::with_capacity(256);
1869        arena.resize_persistent(16).unwrap();
1870        let handle = alloc_u64(&arena, 9);
1871        assert_eq!(*handle.get(&arena).unwrap(), 9);
1872        // Growing the persistent region advances the epoch, so a handle
1873        // issued before the resize reads stale rather than returning
1874        // relocated bytes.
1875        arena.resize_persistent_capacity(32).unwrap();
1876        assert!(matches!(handle.get(&arena), Err(Stale)));
1877    }
1878
1879    #[cfg(feature = "alloc")]
1880    #[test]
1881    fn try_with_capacity_succeeds_for_a_reasonable_size() {
1882        let arena = Arena::try_with_capacity(4096).expect("4 KiB is available");
1883        assert_eq!(arena.capacity(), 4096);
1884        assert_eq!(arena.free(), 4096);
1885    }
1886
1887    #[cfg(feature = "alloc")]
1888    #[test]
1889    fn try_with_capacity_zero_is_always_ok() {
1890        let arena = Arena::try_with_capacity(0).expect("zero capacity never fails");
1891        assert_eq!(arena.capacity(), 0);
1892    }
1893
1894    #[cfg(feature = "alloc")]
1895    #[test]
1896    fn try_with_capacity_fails_cleanly_on_an_impossible_size() {
1897        // A capacity at the top of the address space cannot be allocated and
1898        // its layout is invalid; `try_with_capacity` returns an error rather
1899        // than aborting the process, which is the whole point of the fallible
1900        // entry point.
1901        assert!(matches!(
1902            Arena::try_with_capacity(usize::MAX),
1903            Err(AllocError)
1904        ));
1905    }
1906
1907    #[cfg(feature = "alloc")]
1908    #[test]
1909    fn addr_is_ephemeral_partitions_by_region() {
1910        let mut arena = Arena::with_capacity(64);
1911        arena.resize_persistent(16).unwrap();
1912        let base = arena.persistent_ptr().as_ptr() as usize;
1913        // A persistent-region address is not ephemeral.
1914        assert!(!arena.addr_is_ephemeral(base));
1915        assert!(!arena.addr_is_ephemeral(base + 15));
1916        // The first ephemeral byte and an interior ephemeral byte are.
1917        assert!(arena.addr_is_ephemeral(base + 16));
1918        assert!(arena.addr_is_ephemeral(base + 63));
1919        // The byte just past capacity is not ephemeral.
1920        assert!(!arena.addr_is_ephemeral(base + 64));
1921        // A null address is never ephemeral (it addresses no storage).
1922        assert!(!arena.addr_is_ephemeral(0));
1923        // An address far outside the arena (rodata-like) is not ephemeral.
1924        assert!(!arena.addr_is_ephemeral(base.wrapping_add(1 << 40)));
1925    }
1926
1927    #[cfg(feature = "alloc")]
1928    #[test]
1929    fn zero_persistent_range_clears_only_the_named_subrange() {
1930        let mut arena = Arena::with_capacity(64);
1931        arena.resize_persistent(16).unwrap();
1932        let ptr = arena.persistent_ptr();
1933        unsafe {
1934            core::ptr::write_bytes(ptr.as_ptr(), 0xFF, 16);
1935        }
1936        // Zero only [8, 16); the first eight bytes stay 0xFF.
1937        arena.zero_persistent_range(8, 8).unwrap();
1938        unsafe {
1939            for i in 0..8 {
1940                assert_eq!(core::ptr::read(ptr.as_ptr().add(i)), 0xFF, "byte {i}");
1941            }
1942            for i in 8..16 {
1943                assert_eq!(core::ptr::read(ptr.as_ptr().add(i)), 0, "byte {i}");
1944            }
1945        }
1946        // A range past the persistent capacity is rejected and writes nothing.
1947        assert!(matches!(
1948            arena.zero_persistent_range(12, 8),
1949            Err(ResizeError::ExceedsCapacity)
1950        ));
1951        unsafe {
1952            for i in 0..8 {
1953                assert_eq!(
1954                    core::ptr::read(ptr.as_ptr().add(i)),
1955                    0xFF,
1956                    "byte {i} after reject"
1957                );
1958            }
1959        }
1960    }
1961
1962    #[cfg(feature = "alloc")]
1963    #[test]
1964    fn reset_preserves_persistent_region_contents() {
1965        let mut arena = Arena::with_capacity(64);
1966        arena.resize_persistent(16).unwrap();
1967        // Write a pattern into the persistent region.
1968        let ptr = arena.persistent_ptr();
1969        unsafe {
1970            for i in 0..16 {
1971                core::ptr::write(ptr.as_ptr().add(i), 0xA0 + i as u8);
1972            }
1973        }
1974        // Allocate from the dual-headed region.
1975        let _a = arena.alloc_bottom_bytes(8).unwrap();
1976        // Reset; the persistent contents must survive.
1977        arena.reset().unwrap();
1978        assert_eq!(arena.bottom_used(), 0);
1979        unsafe {
1980            for i in 0..16 {
1981                assert_eq!(
1982                    core::ptr::read(ptr.as_ptr().add(i)),
1983                    0xA0 + i as u8,
1984                    "byte {} clobbered by reset",
1985                    i
1986                );
1987            }
1988        }
1989    }
1990
1991    #[cfg(feature = "alloc")]
1992    #[test]
1993    fn zero_persistent_clears_only_persistent_bytes() {
1994        let mut arena = Arena::with_capacity(64);
1995        arena.resize_persistent(16).unwrap();
1996        let ptr = arena.persistent_ptr();
1997        unsafe {
1998            core::ptr::write_bytes(ptr.as_ptr(), 0xFF, 16);
1999        }
2000        // Allocate from bottom and write through the pointer.
2001        let allocated = arena
2002            .bottom_handle()
2003            .allocate(Layout::from_size_align(4, 1).unwrap())
2004            .unwrap();
2005        unsafe {
2006            core::ptr::write_bytes(allocated.as_ptr() as *mut u8, 0xAB, 4);
2007        }
2008        arena.zero_persistent();
2009        // Persistent region zero.
2010        unsafe {
2011            for i in 0..16 {
2012                assert_eq!(core::ptr::read(ptr.as_ptr().add(i)), 0);
2013            }
2014        }
2015        // Dual-headed bytes still 0xAB at the allocation address.
2016        unsafe {
2017            assert_eq!(core::ptr::read(allocated.as_ptr() as *const u8), 0xAB);
2018        }
2019    }
2020
2021    #[cfg(feature = "alloc")]
2022    #[test]
2023    fn zero_dual_headed_clears_dual_headed_resets_pointers() {
2024        let mut arena = Arena::with_capacity(64);
2025        arena.resize_persistent(16).unwrap();
2026        let ptr = arena.persistent_ptr();
2027        unsafe {
2028            core::ptr::write_bytes(ptr.as_ptr(), 0xCD, 16);
2029        }
2030        let _a = arena.alloc_bottom_bytes(8).unwrap();
2031        assert_eq!(arena.bottom_used(), 8);
2032        arena.zero_dual_headed().unwrap();
2033        assert_eq!(arena.bottom_used(), 0);
2034        // Persistent untouched.
2035        unsafe {
2036            assert_eq!(core::ptr::read(ptr.as_ptr()), 0xCD);
2037        }
2038    }
2039
2040    #[cfg(feature = "alloc")]
2041    #[test]
2042    fn zero_all_clears_persistent_and_dual_headed() {
2043        let mut arena = Arena::with_capacity(64);
2044        arena.resize_persistent(16).unwrap();
2045        let ptr = arena.persistent_ptr();
2046        unsafe {
2047            core::ptr::write_bytes(ptr.as_ptr(), 0xCD, 16);
2048        }
2049        arena.zero_all().unwrap();
2050        assert_eq!(arena.bottom_used(), 0);
2051        unsafe {
2052            for i in 0..16 {
2053                assert_eq!(core::ptr::read(ptr.as_ptr().add(i)), 0);
2054            }
2055        }
2056    }
2057
2058    #[cfg(feature = "alloc")]
2059    #[test]
2060    fn resize_persistent_bumps_epoch() {
2061        let mut arena = Arena::with_capacity(64);
2062        let e0 = arena.epoch();
2063        arena.resize_persistent(16).unwrap();
2064        let e1 = arena.epoch();
2065        assert!(e1 > e0);
2066    }
2067
2068    #[cfg(feature = "alloc")]
2069    #[test]
2070    fn pooling_pattern() {
2071        // Demonstrates the pool use case: one oversized arena is
2072        // resized for each script in turn. Successive resizes work
2073        // and the dual-headed region is reset each time.
2074        let mut arena = Arena::with_capacity(256);
2075        // Script A: 32 bytes persistent.
2076        arena.resize_persistent(32).unwrap();
2077        let _a = arena.alloc_bottom_bytes(16).unwrap();
2078        assert_eq!(arena.bottom_used(), 16);
2079        // Script B: 64 bytes persistent.
2080        arena.resize_persistent(64).unwrap();
2081        assert_eq!(arena.persistent_capacity(), 64);
2082        assert_eq!(arena.bottom_used(), 0);
2083        assert_eq!(arena.dual_headed_capacity(), 192);
2084    }
2085
2086    // -- Region-aware handle validity (B28 P3 item 5) --
2087
2088    #[cfg(feature = "alloc")]
2089    #[test]
2090    fn ephemeral_handle_still_goes_stale_after_reset() {
2091        // A handle into the ephemeral top region keeps the exact epoch-gated
2092        // behaviour it had before the region-aware check: a RESET reclaims the
2093        // region and the handle is stale.
2094        let mut arena = Arena::with_capacity(128);
2095        let buffer = arena.alloc_top_bytes(8).unwrap();
2096        let handle: ArenaHandle<[u8]> =
2097            unsafe { ArenaHandle::from_raw_parts(buffer, arena.epoch()) };
2098        assert!(handle.get(&arena).is_ok());
2099        arena.reset().unwrap();
2100        assert!(matches!(handle.get(&arena), Err(Stale)));
2101    }
2102
2103    #[cfg(feature = "alloc")]
2104    #[test]
2105    fn persistent_region_handle_survives_reset() {
2106        // A handle into the persistent region survives a RESET, because the
2107        // region is never reclaimed. This is what lets a private persistent
2108        // flat composite body be read in place across iterations.
2109        let mut arena = Arena::with_capacity(128);
2110        arena.resize_persistent(16).unwrap();
2111        let ptr = core::ptr::NonNull::slice_from_raw_parts(arena.persistent_ptr(), 8);
2112        let handle: ArenaHandle<[u8]> = unsafe { ArenaHandle::from_raw_parts(ptr, arena.epoch()) };
2113        assert!(handle.get(&arena).is_ok());
2114        arena.reset().unwrap();
2115        assert!(
2116            handle.get(&arena).is_ok(),
2117            "a persistent-region handle must survive RESET"
2118        );
2119    }
2120
2121    #[cfg(feature = "alloc")]
2122    #[test]
2123    fn external_pointer_handle_is_always_live() {
2124        // A handle that points outside the arena entirely (host memory or
2125        // rodata reached through a flat composite) is always live, because a
2126        // RESET never reclaims memory the arena does not own. This is what lets
2127        // a shared or const flat composite body be read in place with no copy.
2128        let mut arena = Arena::with_capacity(128);
2129        let external: [u8; 4] = [1, 2, 3, 4];
2130        let ptr: core::ptr::NonNull<[u8]> = core::ptr::NonNull::from(&external[..]);
2131        let handle: ArenaHandle<[u8]> = unsafe { ArenaHandle::from_raw_parts(ptr, arena.epoch()) };
2132        assert_eq!(handle.get(&arena).unwrap(), &[1, 2, 3, 4]);
2133        arena.reset().unwrap();
2134        assert_eq!(
2135            handle.get(&arena).unwrap(),
2136            &[1, 2, 3, 4],
2137            "an external pointer must stay live across RESET"
2138        );
2139    }
2140}