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}