Skip to main content

forge_alloc/layout/
bump.rs

1//! `BumpArena<B>` — single-threaded bump arena over a [`FixedRange`] backing.
2//!
3//! Allocation is O(1) — align the cursor, bounds-check, advance. Deallocation
4//! is a no-op; reclaim happens via [`reset`](BumpArena::reset). To use with
5//! the standard collection types (`Vec<T, A>`, etc.), allocate via the arena
6//! directly and wrap with `from_raw_in` using [`BumpDeallocator<'_>`] as the
7//! deallocation token.
8//!
9//! ```
10//! use forge_alloc::InlineBacked;
11//! use forge_alloc::{Allocator, NonZeroLayout};
12//! use forge_alloc::BumpArena;
13//!
14//! let mut arena = BumpArena::new(InlineBacked::<1024>::new()).unwrap();
15//! let layout = NonZeroLayout::from_size_align(128, 16).unwrap();
16//! let _block = arena.allocate(layout).unwrap();
17//! assert_eq!(arena.allocated(), 128);
18//! arena.reset();
19//! assert_eq!(arena.allocated(), 0);
20//! ```
21//!
22//! See `docs/ARCHITECTURE.md` for the bump-arena design.
23
24use core::cell::UnsafeCell;
25use core::marker::PhantomData;
26use core::ptr::NonNull;
27
28use forge_alloc_core::{
29    AllocError, Allocator, Deallocator, FixedRange, NonZeroLayout, OsBacked, ProtectFlags,
30};
31
32/// Bump arena over any [`FixedRange`] backing.
33///
34/// The arena uses the entire address range exposed by the backing. The
35/// backing's own `allocate` is never called — `BumpArena` does all
36/// suballocation directly. When the arena drops, the backing drops, and the
37/// memory is released by whatever path the backing uses (e.g. `MmapBacked`'s
38/// `munmap`).
39///
40/// # Thread safety
41///
42/// `Send`: yes if `B: Send`. `Sync`: NO — concurrent `&self` allocators would
43/// race on the cursor. Use [`SharedBumpArena`](crate::layout::SharedBumpArena) for
44/// cross-thread access.
45pub struct BumpArena<B: FixedRange> {
46    backing: B,
47    /// Cached byte size of the backing range, captured at construction.
48    /// We do NOT cache `base` or `end` here — backings whose `base()` is
49    /// structure-relative (e.g. `InlineBacked<N>` returns `&self.storage`)
50    /// produce a different address before and after the backing has been
51    /// moved into `Self`. A pointer captured at construction would point
52    /// at the backing's pre-move location for the rest of the arena's
53    /// life, silently corrupting every subsequent `allocate`. We re-
54    /// query `backing.base()` at each `allocate` call instead; the
55    /// happy-path cost is one extra indirect load.
56    capacity: usize,
57    /// Offset from `backing.base()`. Interior mutability for `&self`
58    /// allocation; `!Sync` (via `UnsafeCell`) prevents concurrent racing.
59    cursor: UnsafeCell<usize>,
60}
61
62impl<B: FixedRange> BumpArena<B> {
63    /// Construct a bump arena that owns `backing` and bumps through its
64    /// entire address range.
65    ///
66    /// Returns an error if the backing reports a zero-byte range or if the
67    /// backing's `[base, base+size)` range would wrap past `usize::MAX`
68    /// (impossible on real 64-bit hardware but representable on small
69    /// `no_std` targets).
70    pub fn new(backing: B) -> Result<Self, AllocError> {
71        let base = backing.base();
72        let size = backing.size();
73        if size == 0 {
74            return Err(AllocError);
75        }
76        // Reject backings whose [base, base+size) range wraps past
77        // `usize::MAX`. Even though we don't cache `end` anymore, every
78        // allocate path still derives `aligned_off + size <= capacity`
79        // from this invariant; rejecting at construction surfaces the
80        // misconfigured backing once instead of on every allocate.
81        // On 64-bit this branch is unreachable in practice; on 16-/32-bit
82        // no_std it can fire.
83        let base_addr = base.as_ptr() as usize;
84        let end_addr = base_addr.checked_add(size).ok_or(AllocError)?;
85        // `end_addr == 0` would mean `base + size == 2^N exactly`, i.e. the
86        // mapping covers the top of the address space — also rejected, since
87        // we'd need a non-null `end` sentinel.
88        if end_addr == 0 {
89            return Err(AllocError);
90        }
91        Ok(Self {
92            backing,
93            capacity: size,
94            cursor: UnsafeCell::new(0),
95        })
96    }
97
98    /// Bytes currently allocated from this arena.
99    #[inline]
100    pub fn allocated(&self) -> usize {
101        // SAFETY: !Sync — no concurrent access to cursor.
102        unsafe { *self.cursor.get() }
103    }
104
105    /// Total bytes available in this arena.
106    #[inline]
107    pub fn capacity(&self) -> usize {
108        self.capacity
109    }
110
111    /// Bytes remaining for allocation.
112    #[inline]
113    pub fn remaining(&self) -> usize {
114        self.capacity() - self.allocated()
115    }
116
117    /// Borrow the underlying backing.
118    #[inline]
119    pub fn backing(&self) -> &B {
120        &self.backing
121    }
122
123    /// Mint a zero-sized [`BumpDeallocator`] tied to this arena's lifetime.
124    ///
125    /// The deallocator's `'a` lifetime is the arena's borrow, so the borrow
126    /// checker prevents the arena from being dropped or reset while any
127    /// `Box<T, BumpDeallocator<'_>>` (constructed via `Box::from_raw_in`) is
128    /// outstanding.
129    #[inline]
130    pub fn deallocator(&self) -> BumpDeallocator<'_> {
131        BumpDeallocator(PhantomData)
132    }
133}
134
135impl<B: FixedRange> BumpArena<B> {
136    /// Reset the cursor to 0, reclaiming all memory in O(1).
137    ///
138    /// Requires `&mut self`, which the borrow checker enforces: any
139    /// outstanding `Box<T, BumpDeallocator<'_>>` (whose `'_` is `&self`)
140    /// blocks `&mut self` access until dropped. Raw `allocate` callers must
141    /// observe the discipline themselves.
142    ///
143    /// # Safety
144    ///
145    /// All pointers previously issued by this arena become invalid after
146    /// `reset`. Reading or writing through them is undefined behavior.
147    #[inline]
148    pub fn reset(&mut self) {
149        // &mut self gives exclusive access.
150        *self.cursor.get_mut() = 0;
151    }
152
153    /// Rewind the cursor to a previously-recorded mark (an [`allocated`]
154    /// value), reclaiming everything allocated since. Like [`reset`] but to an
155    /// arbitrary earlier point; the engine behind [`scope`].
156    ///
157    /// [`allocated`]: BumpArena::allocated
158    /// [`reset`]: BumpArena::reset
159    /// [`scope`]: BumpArena::scope
160    ///
161    /// # Safety contract (caller-upheld, mirrors [`reset`])
162    ///
163    /// All pointers issued by this arena *after* `mark` was recorded become
164    /// invalid. The `&mut self` borrow is what makes this enforceable; the
165    /// [`scope`](BumpArena::scope) guard wraps it in a safe RAII API.
166    #[inline]
167    pub fn rewind_to(&mut self, mark: usize) {
168        debug_assert!(
169            mark <= *self.cursor.get_mut(),
170            "rewind_to mark is ahead of cursor"
171        );
172        // &mut self gives exclusive access.
173        *self.cursor.get_mut() = mark;
174    }
175
176    /// Typed bump allocation: reserve aligned, **uninitialized** storage for one
177    /// `T` and return a pointer to it. Because `size_of::<T>()` and
178    /// `align_of::<T>()` are compile-time constants, the alignment-rounding mask
179    /// and the bounds arithmetic fold to a tight branch — the typed analogue of
180    /// [`allocate`](Allocator::allocate) for the common "allocate one value"
181    /// case, without constructing a runtime [`NonZeroLayout`].
182    ///
183    /// For a zero-sized `T` this returns a well-aligned dangling pointer and
184    /// consumes no space (a successful no-op, where `allocate` rejects a
185    /// zero-size layout). The returned pointer is **uninitialized**; write a
186    /// `T` before reading.
187    #[inline]
188    pub fn alloc_uninit<T>(&self) -> Result<NonNull<T>, AllocError> {
189        let size = core::mem::size_of::<T>();
190        let align = core::mem::align_of::<T>();
191        // ZST: a dangling but aligned pointer is valid for a zero-sized read or
192        // write, and consumes no space. Folds away entirely for non-ZST `T`.
193        if size == 0 {
194            return Ok(NonNull::dangling());
195        }
196        // Re-query the live backing base (structure-relative backings move) —
197        // identical to `allocate`.
198        let base = self.backing.base();
199        let base_addr = base.as_ptr() as usize;
200        // SAFETY: !Sync — no concurrent cursor access (same contract as
201        // `allocate`; `reset`/`rewind_to`/`scope` take `&mut self`).
202        unsafe {
203            let cursor_ptr = self.cursor.get();
204            let cur = *cursor_ptr;
205            // `align` is a compile-time power of two, so `align - 1` and the
206            // mask are constants the optimizer folds.
207            let raw = base_addr.checked_add(cur).ok_or(AllocError)?;
208            let aligned = raw.checked_add(align - 1).ok_or(AllocError)? & !(align - 1);
209            let aligned_off = aligned - base_addr;
210            let end_off = aligned_off.checked_add(size).ok_or(AllocError)?;
211            if end_off > self.capacity() {
212                return Err(AllocError);
213            }
214            // Commit freshly-crossed pages on lazy backings before publishing
215            // the cursor (no-op for eager/inline backings) — as in `allocate`.
216            self.backing.commit(aligned_off, size)?;
217            *cursor_ptr = end_off;
218            let p = base.as_ptr().add(aligned_off) as *mut T;
219            Ok(NonNull::new_unchecked(p))
220        }
221    }
222
223    /// Allocate space for one `T` and move `value` into it, returning a pointer
224    /// to the initialized `T`. The typed-and-initialized companion of
225    /// [`alloc_uninit`](Self::alloc_uninit).
226    #[inline]
227    pub fn alloc<T>(&self, value: T) -> Result<NonNull<T>, AllocError> {
228        let p = self.alloc_uninit::<T>()?;
229        // SAFETY: `p` is fresh, aligned, exclusive storage for one `T`.
230        unsafe { p.as_ptr().write(value) };
231        Ok(p)
232    }
233
234    /// Copy a slice into the arena, returning a pointer to the copy. `T: Copy`
235    /// so no destructor obligations are transferred. An empty or zero-sized-`T`
236    /// slice consumes no space and yields a dangling-but-aligned slice pointer
237    /// of the same length.
238    #[inline]
239    pub fn alloc_slice_copy<T: Copy>(&self, src: &[T]) -> Result<NonNull<[T]>, AllocError> {
240        let n = src.len();
241        if n == 0 || core::mem::size_of::<T>() == 0 {
242            // A slice of `n` ZSTs (or zero elements) needs no storage; a
243            // dangling, aligned pointer with length `n` is a valid `&[T]`/`&mut`.
244            return Ok(NonNull::slice_from_raw_parts(NonNull::<T>::dangling(), n));
245        }
246        // `array` cannot overflow here: `src` already exists in memory, so
247        // `n * size_of::<T>()` fits `isize`.
248        let layout = NonZeroLayout::array::<T>(n).ok_or(AllocError)?;
249        let dst = self.allocate(layout)?.cast::<T>();
250        // SAFETY: `dst` is fresh storage for `n` `T`s, disjoint from `src`;
251        // `T: Copy` so a bytewise copy is a valid clone.
252        unsafe { core::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_ptr(), n) };
253        Ok(NonNull::slice_from_raw_parts(dst, n))
254    }
255
256    /// Copy a string slice into the arena, returning a pointer to the copy.
257    #[inline]
258    pub fn alloc_str(&self, s: &str) -> Result<NonNull<str>, AllocError> {
259        let bytes = self.alloc_slice_copy(s.as_bytes())?;
260        // SAFETY: the bytes were copied verbatim from a valid `&str`, so they
261        // remain valid UTF-8; `*mut [u8]` and `*mut str` share layout.
262        let p = bytes.as_ptr() as *mut str;
263        Ok(unsafe { NonNull::new_unchecked(p) })
264    }
265
266    /// Open a scratch [`Scope`]. Allocations made through the returned guard are
267    /// reclaimed when it is dropped, rewinding the cursor to where it was — a
268    /// **nestable, panic-safe** checkpoint.
269    ///
270    /// Soundness rests on ordinary borrow-checking, not an `unsafe` lifetime
271    /// trick:
272    /// - The `&mut self` borrow makes the arena unusable directly for the
273    ///   scope's lifetime, so no *outer* allocation can land in the region the
274    ///   scope will reclaim.
275    /// - Every reference the scope hands out borrows the guard (`&self`), so the
276    ///   borrow checker forbids it from outliving the guard — and the guard's
277    ///   `Drop` is what rewinds. A scope allocation therefore cannot dangle past
278    ///   the rewind.
279    /// - `Drop` runs on a panic unwind too, so a panicking scope body still
280    ///   rewinds (no torn cursor).
281    ///
282    /// ```
283    /// use forge_alloc::{BumpArena, InlineBacked};
284    /// let mut arena = BumpArena::new(InlineBacked::<1024>::new()).unwrap();
285    /// let before = arena.allocated();
286    /// {
287    ///     let scope = arena.scope();
288    ///     let _scratch = scope.alloc_uninit::<[u8; 64]>().unwrap();
289    ///     assert!(scope.arena_allocated() > before);
290    /// } // scope dropped: cursor rewound
291    /// assert_eq!(arena.allocated(), before);
292    /// ```
293    ///
294    /// A scope allocation cannot escape the scope:
295    /// ```compile_fail
296    /// use forge_alloc::{BumpArena, InlineBacked};
297    /// let mut arena = BumpArena::new(InlineBacked::<1024>::new()).unwrap();
298    /// let escaped;
299    /// {
300    ///     let scope = arena.scope();
301    ///     escaped = scope.alloc_uninit::<u32>().unwrap(); // borrows `scope`
302    /// } // `scope` dropped here
303    /// let _use = escaped; // ERROR: `scope` does not live long enough
304    /// ```
305    #[inline]
306    pub fn scope(&mut self) -> Scope<'_, B> {
307        let mark = self.allocated();
308        Scope { arena: self, mark }
309    }
310}
311
312unsafe impl<B: FixedRange> Deallocator for BumpArena<B> {
313    #[inline]
314    unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: NonZeroLayout) {
315        // No-op. Reclaim is via reset(&mut self).
316    }
317}
318
319unsafe impl<B: FixedRange> Allocator for BumpArena<B> {
320    #[inline]
321    fn allocate(&self, layout: NonZeroLayout) -> Result<NonNull<[u8]>, AllocError> {
322        let align = layout.align().get();
323        let size = layout.size().get();
324        // Re-query the backing's base at each allocate so structure-
325        // relative backings (e.g. `InlineBacked`) keep working after the
326        // arena has been moved.
327        let base = self.backing.base();
328        let base_addr = base.as_ptr() as usize;
329
330        // SAFETY: !Sync — no concurrent access to cursor. We hold the only
331        // path to mutating it (other than `reset(&mut self)`).
332        unsafe {
333            let cursor_ptr = self.cursor.get();
334            let cur = *cursor_ptr;
335            // Round up the absolute address to the requested alignment.
336            let raw = base_addr.checked_add(cur).ok_or(AllocError)?;
337            let aligned = raw.checked_add(align - 1).ok_or(AllocError)? & !(align - 1);
338            // `aligned >= raw >= base_addr` because masking only zeroes low
339            // bits; the subtraction never wraps.
340            let aligned_off = aligned - base_addr;
341            let end_off = aligned_off.checked_add(size).ok_or(AllocError)?;
342            if end_off > self.capacity() {
343                return Err(AllocError);
344            }
345            // Ensure the backing has the block's pages committed before we
346            // hand them out. No-op for already-writable backings
347            // (InlineBacked, eager MmapBacked, Unix mmap); on a lazy_commit
348            // MmapBacked this commits the freshly-crossed pages and can fail
349            // if the OS declines (Windows commit limit). Commit BEFORE
350            // publishing the cursor so a failure leaves the arena unchanged
351            // and surfaces as a clean AllocError rather than a fault on
352            // first write.
353            self.backing.commit(aligned_off, size)?;
354            *cursor_ptr = end_off;
355            // SAFETY: aligned_off + size <= capacity, so the resulting ptr
356            // lies within [base, end). base is non-null per FixedRange's
357            // contract; the offset preserves non-null.
358            let p = base.as_ptr().add(aligned_off);
359            Ok(NonNull::slice_from_raw_parts(
360                NonNull::new_unchecked(p),
361                size,
362            ))
363        }
364    }
365
366    #[inline]
367    fn capacity_bytes(&self) -> Option<usize> {
368        Some(self.capacity())
369    }
370
371    /// In-place grow when `ptr` is the most-recent allocation.
372    ///
373    /// If the block being grown ends exactly at the cursor (i.e. it was the last
374    /// thing allocated), the grow is just a cursor advance — **no copy**, the
375    /// same pointer is returned covering the larger size. This is the common
376    /// case for building a `Vec`/`String` in an arena. Otherwise it falls back
377    /// to allocate-new + copy (the old block is reclaimed at the next `reset`,
378    /// as for any bump allocation).
379    ///
380    /// # Safety
381    ///
382    /// Same as [`Allocator::grow`]: `ptr` is a live allocation of `old` from
383    /// this arena, `new.size() >= old.size()`, and `old.align() == new.align()`.
384    unsafe fn grow(
385        &self,
386        ptr: NonNull<u8>,
387        old: NonZeroLayout,
388        new: NonZeroLayout,
389    ) -> Result<NonNull<[u8]>, AllocError> {
390        debug_assert!(new.size() >= old.size());
391        debug_assert_eq!(old.align(), new.align());
392        let base = self.backing.base();
393        let base_addr = base.as_ptr() as usize;
394        let off = (ptr.as_ptr() as usize).wrapping_sub(base_addr);
395        // SAFETY: !Sync — exclusive cursor access (same contract as `allocate`).
396        unsafe {
397            let cursor_ptr = self.cursor.get();
398            let cur = *cursor_ptr;
399            // Fast path: `ptr`'s block ends exactly at the cursor → it is the
400            // most-recent allocation, so grow by advancing the cursor in place.
401            if off.checked_add(old.size().get()) == Some(cur) {
402                let new_end = off.checked_add(new.size().get()).ok_or(AllocError)?;
403                if new_end <= self.capacity() {
404                    // Commit the (possibly-)newly-crossed tail pages before
405                    // publishing the cursor; idempotent over the old prefix.
406                    self.backing.commit(off, new.size().get())?;
407                    *cursor_ptr = new_end;
408                    return Ok(NonNull::slice_from_raw_parts(ptr, new.size().get()));
409                }
410                // Doesn't fit in place — fall through to relocate.
411            }
412        }
413        // Fallback: fresh allocation + copy (old block leaked until `reset`).
414        let dst = self.allocate(new)?;
415        // SAFETY: caller's contract gives a valid `ptr` of `old.size()` bytes;
416        // `dst` is fresh, ≥ `old.size()`, and disjoint.
417        unsafe {
418            core::ptr::copy_nonoverlapping(
419                ptr.as_ptr(),
420                dst.cast::<u8>().as_ptr(),
421                old.size().get(),
422            );
423        }
424        Ok(dst)
425    }
426
427    /// Reset the arena via the Allocator trait.
428    ///
429    /// Returns `Ok(())` and clears the cursor.
430    #[inline]
431    fn reset(&mut self) -> Result<(), AllocError> {
432        BumpArena::reset(self);
433        Ok(())
434    }
435}
436
437impl<B: FixedRange> FixedRange for BumpArena<B> {
438    #[inline]
439    fn base(&self) -> NonNull<u8> {
440        // Forward to the live backing rather than returning a cached
441        // pointer — structure-relative backings change address on move.
442        self.backing.base()
443    }
444
445    #[inline]
446    fn size(&self) -> usize {
447        self.capacity()
448    }
449}
450
451// When the backing is OS-managed, the arena is too: it occupies the backing's
452// entire mapping, so `base_ptr` / `region_size` / `release_pages` / `protect`
453// forward straight through. The motivating use case is an arena *pool* on a
454// per-commit / per-branch workload: instead of dropping (and `munmap`-ing) an
455// arena on pool overflow, the pool can `reset()` it and `release_pages()` the
456// whole region — returning the physical pages to the OS (`madvise(DONTNEED)` /
457// `MEM_RESET`) while keeping the virtual reservation warm for reuse. That
458// removes both the `munmap` syscall and the demand-zero re-fault storm a fresh
459// `mmap` would incur on the next commit, without changing pool semantics.
460//
461// SAFETY: `base_ptr` (stable, non-null) and `region_size` (accurate page-rounded
462// length) are discharged by the `B: OsBacked` backing; the arena caches nothing
463// and delegates every call to it. For `release_pages` / `protect`, the in-region,
464// page-alignment, and no-live-overlap requirements are the caller's documented
465// `unsafe` precondition (see each method's `# Safety`); like the crate's other
466// OsBacked wrappers, neither the arena nor the backing re-validates them.
467unsafe impl<B: FixedRange + OsBacked> OsBacked for BumpArena<B> {
468    #[inline]
469    fn base_ptr(&self) -> NonNull<u8> {
470        self.backing.base_ptr()
471    }
472
473    #[inline]
474    fn region_size(&self) -> usize {
475        self.backing.region_size()
476    }
477
478    #[inline]
479    unsafe fn release_pages(&self, ptr: NonNull<u8>, size: usize) {
480        // SAFETY: forwarded; caller guarantees an in-region range with no live
481        // allocations (after `reset()` the arena has none — the pool-overflow
482        // path above). The backing clamps the reset to the committed prefix on
483        // Windows, so a full-region release of a partially-committed lazy mapping
484        // resets only the committed pages.
485        unsafe { self.backing.release_pages(ptr, size) }
486    }
487
488    #[inline]
489    unsafe fn protect(&self, ptr: NonNull<u8>, size: usize, flags: ProtectFlags) {
490        // SAFETY: forwarded; caller guarantees an in-region, page-aligned range.
491        unsafe { self.backing.protect(ptr, size, flags) }
492    }
493}
494
495// Send when B: Send. The `NonNull<u8>` fields are `!Send` by default but the
496// memory they point to is owned by `backing`, which we move along with the
497// arena. `UnsafeCell<usize>` is `Send` (cursor is just an integer).
498//
499// `!Sync` is auto-derived via `UnsafeCell`, which is the desired behaviour:
500// concurrent `&self` allocate would race on the cursor — use
501// `SharedBumpArena` for the cross-thread case.
502unsafe impl<B: FixedRange + Send> Send for BumpArena<B> {}
503
504// ============================================================================
505// Scope — RAII scratch checkpoint
506// ============================================================================
507
508/// A scratch scope over a [`BumpArena`], created by [`BumpArena::scope`].
509///
510/// Allocate through it; when it drops (normally **or on a panic unwind**) the
511/// arena's cursor rewinds to where the scope began, reclaiming everything the
512/// scope allocated. References handed out by the scope borrow it (`&self`), so
513/// the borrow checker forbids them from outliving the rewind — no `unsafe`
514/// lifetime branding is needed, and a misuse is a compile error (see the
515/// `compile_fail` example on [`BumpArena::scope`]).
516///
517/// Scopes nest: call [`scope`](Scope::scope) on a `Scope` to checkpoint again.
518///
519/// While the scope is alive the underlying arena is mutably borrowed, so it
520/// cannot be used directly — which is exactly what prevents an outer allocation
521/// from landing in the region the scope will reclaim.
522pub struct Scope<'a, B: FixedRange> {
523    arena: &'a mut BumpArena<B>,
524    /// Cursor offset at scope creation; the rewind target on drop.
525    mark: usize,
526}
527
528impl<'a, B: FixedRange> Scope<'a, B> {
529    /// Typed scratch allocation bound to this scope — see
530    /// [`BumpArena::alloc_uninit`]. The returned reference borrows the scope, so
531    /// it cannot outlive the rewind. Returns `&mut MaybeUninit<T>`; write a `T`
532    /// before assuming it initialized.
533    ///
534    /// `&mut` from `&self` is the bump-allocator idiom (cf. `bumpalo::Bump::alloc`):
535    /// each call returns a *disjoint* fresh region, so the `&mut`s never alias.
536    #[inline]
537    #[allow(clippy::mut_from_ref)]
538    pub fn alloc_uninit<T>(&self) -> Result<&mut core::mem::MaybeUninit<T>, AllocError> {
539        let p = self.arena.alloc_uninit::<T>()?;
540        // SAFETY: `alloc_uninit` returns fresh, properly-aligned, non-aliasing
541        // storage for one `T`. Binding it to `&self` ties the reference to this
542        // scope; the borrow checker then prevents it from outliving the rewind
543        // in `Drop`, which reclaims exactly this memory. For a non-ZST `T` each
544        // call returns a disjoint region, so the `&mut`s never alias. For a ZST,
545        // every call yields the same dangling-but-aligned pointer, but a
546        // `&mut MaybeUninit<ZST>` accesses zero bytes — it claims no location
547        // exclusively, so the aliasing restriction is vacuously satisfied even
548        // when several coexist. (Verified under Miri in the `miri_targets`
549        // `alloc_uninit_and_scope_round_trip` ZST case.)
550        Ok(unsafe { &mut *p.cast::<core::mem::MaybeUninit<T>>().as_ptr() })
551    }
552
553    /// Raw byte scratch allocation bound to this scope — see
554    /// [`Allocator::allocate`]. The returned slice borrows the scope.
555    ///
556    /// `&mut` from `&self` is intentional (see [`alloc_uninit`](Self::alloc_uninit)):
557    /// each call returns a disjoint fresh region.
558    #[inline]
559    #[allow(clippy::mut_from_ref)]
560    pub fn allocate(&self, layout: NonZeroLayout) -> Result<&mut [u8], AllocError> {
561        let block = Allocator::allocate(&*self.arena, layout)?;
562        // SAFETY: fresh, non-aliasing bytes from the arena, bound to `&self`
563        // (this scope) so they cannot outlive the rewind. See `alloc_uninit`.
564        Ok(unsafe { &mut *block.as_ptr() })
565    }
566
567    /// Allocate and initialize one `T` as scratch, bound to this scope — see
568    /// [`BumpArena::alloc`]. The returned `&mut T` cannot outlive the rewind.
569    #[inline]
570    pub fn alloc<T>(&self, value: T) -> Result<&mut T, AllocError> {
571        Ok(self.alloc_uninit::<T>()?.write(value))
572    }
573
574    /// Copy a slice into the scope, bound to it — see
575    /// [`BumpArena::alloc_slice_copy`]. The returned `&mut [T]` cannot outlive
576    /// the rewind.
577    #[inline]
578    #[allow(clippy::mut_from_ref)]
579    pub fn alloc_slice_copy<T: Copy>(&self, src: &[T]) -> Result<&mut [T], AllocError> {
580        let p = self.arena.alloc_slice_copy(src)?;
581        // SAFETY: fresh, non-aliasing storage bound to `&self`. See `alloc_uninit`.
582        Ok(unsafe { &mut *p.as_ptr() })
583    }
584
585    /// Copy a string into the scope, bound to it — see [`BumpArena::alloc_str`].
586    /// The returned `&mut str` cannot outlive the rewind.
587    #[inline]
588    #[allow(clippy::mut_from_ref)]
589    pub fn alloc_str(&self, s: &str) -> Result<&mut str, AllocError> {
590        let p = self.arena.alloc_str(s)?;
591        // SAFETY: fresh, non-aliasing UTF-8 storage bound to `&self`.
592        Ok(unsafe { &mut *p.as_ptr() })
593    }
594
595    /// Bytes currently allocated from the underlying arena (the absolute cursor,
596    /// not relative to this scope's mark). Useful in assertions/tests.
597    #[inline]
598    pub fn arena_allocated(&self) -> usize {
599        self.arena.allocated()
600    }
601
602    /// Open a nested scratch scope inside this one. The inner scope reclaims
603    /// only what *it* allocated when it drops; this outer scope is untouched.
604    #[inline]
605    pub fn scope(&mut self) -> Scope<'_, B> {
606        self.arena.scope()
607    }
608}
609
610impl<'a, B: FixedRange> Drop for Scope<'a, B> {
611    #[inline]
612    fn drop(&mut self) {
613        // Rewind to the mark, reclaiming everything the scope allocated. Runs on
614        // normal exit AND on a panic unwind, so a panicking scope body cannot
615        // leave the cursor advanced. Sound because `Drop` takes `&mut self`:
616        // every scope-issued reference borrows `&self`, so none can be alive
617        // here (the borrow checker enforces it), and no outer allocation
618        // occurred (the arena was mutably borrowed by this scope throughout).
619        self.arena.rewind_to(self.mark);
620    }
621}
622
623// ============================================================================
624// BumpDeallocator
625// ============================================================================
626
627/// ZST deallocator token tied to a [`BumpArena`]'s borrow.
628///
629/// Used as the `A` parameter in `Box<T, A>` / `Vec<T, A>` patterns where
630/// the box was constructed via `Box::from_raw_in` against pointers
631/// obtained from the arena directly. The `'a` lifetime ensures the arena
632/// outlives the box.
633///
634/// # Allocate-always-fails footgun
635///
636/// The [`Allocator::allocate`] impl on `BumpDeallocator` returns
637/// `Err(AllocError)` for **every** call. This is deliberate: the
638/// deallocator is a *destruction token*, not an allocation source.
639/// The correct usage pattern is:
640///
641/// ```text
642///     let arena: BumpArena<_> = ...;
643///     let ptr = arena.allocate(layout)?;       // allocate via arena
644///     unsafe { ptr.cast::<T>().write(value) }; // place a T
645///     let boxed: Box<T, BumpDeallocator<'_>> =
646///         unsafe { Box::from_raw_in(
647///             ptr.cast::<T>().as_ptr(),
648///             arena.deallocator(),
649///         )};
650/// ```
651///
652/// Plugging `BumpDeallocator` into code that *grows* a collection
653/// (`Vec::reserve`, `Vec::push` that re-allocates, `Box::new_in` —
654/// anything that calls `Allocator::allocate` on the supplied
655/// allocator) will fail at runtime. Use `BumpArena` itself as the
656/// allocator for those patterns, or pre-size the collection so it
657/// never reallocates.
658#[derive(Copy, Clone, Debug)]
659pub struct BumpDeallocator<'a>(PhantomData<&'a ()>);
660
661unsafe impl Deallocator for BumpDeallocator<'_> {
662    #[inline]
663    unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: NonZeroLayout) {
664        // No-op. Deallocation through the token is a marker that the
665        // arena-allocated value's destructor has run; reclaim happens on
666        // arena reset/drop.
667    }
668}
669
670unsafe impl Allocator for BumpDeallocator<'_> {
671    /// Always fails. Allocate through the arena, not the deallocator.
672    #[inline]
673    fn allocate(&self, _layout: NonZeroLayout) -> Result<NonNull<[u8]>, AllocError> {
674        Err(AllocError)
675    }
676}
677
678#[cfg(test)]
679mod tests {
680    use super::*;
681    use crate::backing::InlineBacked;
682
683    #[test]
684    fn allocate_advances_cursor() {
685        let arena = BumpArena::new(InlineBacked::<1024>::new()).unwrap();
686        assert_eq!(arena.allocated(), 0);
687        let layout = NonZeroLayout::from_size_align(64, 8).unwrap();
688        let _ = arena.allocate(layout).unwrap();
689        assert_eq!(arena.allocated(), 64);
690    }
691
692    #[test]
693    fn allocate_returns_aligned_pointer() {
694        let arena = BumpArena::new(InlineBacked::<1024>::new()).unwrap();
695        // Push the cursor off zero first.
696        let _ = arena
697            .allocate(NonZeroLayout::from_size_align(3, 1).unwrap())
698            .unwrap();
699        let layout = NonZeroLayout::from_size_align(8, 16).unwrap();
700        let block = arena.allocate(layout).unwrap();
701        assert_eq!(block.cast::<u8>().as_ptr() as usize % 16, 0);
702    }
703
704    #[test]
705    fn allocate_fails_when_exhausted() {
706        let arena = BumpArena::new(InlineBacked::<64>::new()).unwrap();
707        let big = NonZeroLayout::from_size_align(64, 1).unwrap();
708        let _ = arena.allocate(big).unwrap();
709        let one = NonZeroLayout::from_size_align(1, 1).unwrap();
710        assert!(arena.allocate(one).is_err());
711    }
712
713    /// Alignment padding must count toward exhaustion: the bounds check is on
714    /// the *aligned* offset + size, not the raw cursor + size. `InlineBacked`'s
715    /// base is 16-aligned, so after a 1-byte alloc (cursor = 1) a 16-aligned
716    /// request rounds the offset deterministically up to 16. A 56-byte request
717    /// then needs `16 + 56 = 72 > 64` and must fail — whereas a buggy check
718    /// using `cursor + size = 1 + 56 = 57 <= 64` would wrongly succeed.
719    #[test]
720    fn alignment_padding_counts_toward_exhaustion() {
721        let arena = BumpArena::new(InlineBacked::<64>::new()).unwrap();
722        let one = NonZeroLayout::from_size_align(1, 1).unwrap();
723        let _ = arena.allocate(one).unwrap(); // cursor = 1
724        let aligned = NonZeroLayout::from_size_align(56, 16).unwrap();
725        assert!(
726            arena.allocate(aligned).is_err(),
727            "alignment padding (offset 16) must be counted in the exhaustion check",
728        );
729    }
730
731    #[test]
732    fn reset_reclaims_all() {
733        let mut arena = BumpArena::new(InlineBacked::<64>::new()).unwrap();
734        let layout = NonZeroLayout::from_size_align(32, 8).unwrap();
735        let _ = arena.allocate(layout).unwrap();
736        assert_eq!(arena.allocated(), 32);
737        arena.reset();
738        assert_eq!(arena.allocated(), 0);
739        let _ = arena.allocate(layout).unwrap();
740    }
741
742    #[test]
743    fn deallocate_is_no_op() {
744        let arena = BumpArena::new(InlineBacked::<64>::new()).unwrap();
745        let layout = NonZeroLayout::from_size_align(32, 8).unwrap();
746        let block = arena.allocate(layout).unwrap();
747        let used_before = arena.allocated();
748        unsafe { arena.deallocate(block.cast(), layout) };
749        assert_eq!(arena.allocated(), used_before);
750    }
751
752    #[test]
753    fn fixed_range_contains_allocations() {
754        let arena = BumpArena::new(InlineBacked::<128>::new()).unwrap();
755        let layout = NonZeroLayout::from_size_align(32, 8).unwrap();
756        let block = arena.allocate(layout).unwrap();
757        assert!(arena.contains(block.cast::<u8>()));
758    }
759
760    #[test]
761    fn capacity_bytes_reports_backing_size() {
762        let arena = BumpArena::new(InlineBacked::<2048>::new()).unwrap();
763        assert_eq!(arena.capacity_bytes(), Some(2048));
764    }
765
766    /// Regression: BumpArena historically cached an absolute `base`
767    /// pointer captured BEFORE the backing was moved into Self. For
768    /// structure-relative backings (`InlineBacked` returns
769    /// `&self.storage`), that pointer became stale on every move and
770    /// silently corrupted subsequent allocates. The fix re-queries
771    /// `self.backing.base()` at each allocate. Verify the arena's
772    /// `FixedRange::base()` agrees with the backing's live `base()` and
773    /// that the first allocate lands at exactly that address.
774    #[test]
775    fn base_pointer_matches_backing_after_move() {
776        let arena = BumpArena::new(InlineBacked::<256>::new()).unwrap();
777        let arena_base = arena.base().as_ptr();
778        let backing_base = arena.backing().base().as_ptr();
779        assert_eq!(
780            arena_base, backing_base,
781            "BumpArena's base must agree with the live backing — stale-pointer regression",
782        );
783        let layout = NonZeroLayout::from_size_align(8, 8).unwrap();
784        let block = arena.allocate(layout).unwrap();
785        assert_eq!(
786            block.cast::<u8>().as_ptr() as usize,
787            backing_base as usize,
788            "first alloc must be at backing.base()",
789        );
790    }
791
792    #[test]
793    fn deallocator_compiles_and_runs() {
794        let arena = BumpArena::new(InlineBacked::<64>::new()).unwrap();
795        let d = arena.deallocator();
796        // The deallocator's allocate must always fail by contract.
797        let layout = NonZeroLayout::from_size_align(8, 8).unwrap();
798        assert!(d.allocate(layout).is_err());
799        // Calling deallocate is safe and a no-op.
800        let block = arena.allocate(layout).unwrap();
801        unsafe { d.deallocate(block.cast(), layout) };
802    }
803
804    #[test]
805    fn very_small_alignment_is_one() {
806        let arena = BumpArena::new(InlineBacked::<64>::new()).unwrap();
807        let l1 = NonZeroLayout::from_size_align(1, 1).unwrap();
808        let _ = arena.allocate(l1).unwrap();
809        let _ = arena.allocate(l1).unwrap();
810        assert_eq!(arena.allocated(), 2);
811    }
812
813    #[test]
814    fn alloc_uninit_is_aligned_and_writable() {
815        let arena = BumpArena::new(InlineBacked::<256>::new()).unwrap();
816        // Force a misaligned starting cursor, then allocate an 8-aligned type.
817        let _pad = arena.alloc_uninit::<u8>().unwrap();
818        let p = arena.alloc_uninit::<u64>().unwrap();
819        assert_eq!(p.as_ptr() as usize % core::mem::align_of::<u64>(), 0);
820        unsafe {
821            p.as_ptr().write(0x0102_0304_0506_0708);
822            assert_eq!(p.as_ptr().read(), 0x0102_0304_0506_0708);
823        }
824    }
825
826    #[test]
827    fn alloc_uninit_zst_consumes_nothing_and_is_aligned() {
828        #[repr(align(16))]
829        struct Zst;
830        let arena = BumpArena::new(InlineBacked::<64>::new()).unwrap();
831        let before = arena.allocated();
832        let p = arena.alloc_uninit::<Zst>().unwrap();
833        assert_eq!(arena.allocated(), before, "ZST must consume no space");
834        assert_eq!(p.as_ptr() as usize % core::mem::align_of::<Zst>(), 0);
835    }
836
837    #[test]
838    fn alloc_uninit_reports_oom_like_allocate() {
839        let arena = BumpArena::new(InlineBacked::<8>::new()).unwrap();
840        // 8 bytes total; a u64 fits exactly once, the second must fail.
841        assert!(arena.alloc_uninit::<u64>().is_ok());
842        assert!(arena.alloc_uninit::<u64>().is_err());
843    }
844
845    #[test]
846    fn alloc_uninit_alignment_padding_counts_toward_exhaustion() {
847        // Push the cursor off an 8-aligned boundary with a 1-byte alloc, then a
848        // u64 needs to skip to offset 8 — its end (16) exceeds the 8-byte
849        // region, so it must OOM. This pins the alignment-rounding path (the
850        // bare OOM test above starts pre-aligned and wouldn't catch a broken
851        // mask).
852        let arena = BumpArena::new(InlineBacked::<8>::new()).unwrap();
853        let _b = arena.alloc_uninit::<u8>().unwrap(); // cursor = 1
854        assert!(
855            arena.alloc_uninit::<u64>().is_err(),
856            "u64 must not fit once alignment padding pushes its end past capacity"
857        );
858    }
859
860    #[test]
861    fn alloc_writes_value_and_slice_and_str_copy() {
862        let arena = BumpArena::new(InlineBacked::<256>::new()).unwrap();
863        let v = arena.alloc(0x1122_3344u32).unwrap();
864        assert_eq!(unsafe { v.as_ptr().read() }, 0x1122_3344);
865
866        let s = arena.alloc_slice_copy(&[1u16, 2, 3, 4]).unwrap();
867        let sl = unsafe { s.as_ref() };
868        assert_eq!(sl, &[1, 2, 3, 4]);
869
870        let st = arena.alloc_str("forge").unwrap();
871        assert_eq!(unsafe { st.as_ref() }, "forge");
872    }
873
874    #[test]
875    fn alloc_slice_copy_empty_and_zst() {
876        let arena = BumpArena::new(InlineBacked::<64>::new()).unwrap();
877        let before = arena.allocated();
878        // Empty slice: no space consumed, length 0.
879        let e = arena.alloc_slice_copy::<u32>(&[]).unwrap();
880        assert_eq!(e.len(), 0);
881        // ZST slice of length 3: no space consumed, length preserved.
882        let z = arena.alloc_slice_copy(&[(), (), ()]).unwrap();
883        assert_eq!(z.len(), 3);
884        assert_eq!(
885            arena.allocated(),
886            before,
887            "empty/ZST slices consume no space"
888        );
889    }
890
891    #[test]
892    fn grow_in_place_when_last_allocation_does_not_copy() {
893        let arena = BumpArena::new(InlineBacked::<256>::new()).unwrap();
894        let l8 = NonZeroLayout::from_size_align(8, 8).unwrap();
895        let l32 = NonZeroLayout::from_size_align(32, 8).unwrap();
896        let block = arena.allocate(l8).unwrap();
897        let ptr = block.cast::<u8>();
898        unsafe { core::ptr::write_bytes(ptr.as_ptr(), 0xAB, 8) };
899        let after_first = arena.allocated();
900
901        // It's the most-recent allocation → grow advances the cursor in place,
902        // returns the SAME pointer, copies nothing.
903        let grown = unsafe { arena.grow(ptr, l8, l32).unwrap() };
904        assert_eq!(
905            grown.cast::<u8>(),
906            ptr,
907            "in-place grow keeps the same pointer"
908        );
909        assert_eq!(grown.len(), 32);
910        assert_eq!(
911            arena.allocated(),
912            after_first - 8 + 32,
913            "cursor advanced by the grow delta, no relocation"
914        );
915        // The preserved prefix is intact.
916        assert_eq!(unsafe { ptr.as_ptr().read() }, 0xAB);
917    }
918
919    #[test]
920    fn grow_relocates_when_not_last_allocation() {
921        let arena = BumpArena::new(InlineBacked::<256>::new()).unwrap();
922        let l8 = NonZeroLayout::from_size_align(8, 8).unwrap();
923        let l32 = NonZeroLayout::from_size_align(32, 8).unwrap();
924        let first = arena.allocate(l8).unwrap().cast::<u8>();
925        unsafe { core::ptr::write_bytes(first.as_ptr(), 0xCD, 8) };
926        // A second allocation makes `first` no longer the most-recent.
927        let _second = arena.allocate(l8).unwrap();
928
929        let grown = unsafe { arena.grow(first, l8, l32).unwrap() };
930        assert_ne!(grown.cast::<u8>(), first, "non-last grow must relocate");
931        assert_eq!(grown.len(), 32);
932        // Relocated copy preserves the old bytes.
933        unsafe {
934            for i in 0..8 {
935                assert_eq!(grown.cast::<u8>().as_ptr().add(i).read(), 0xCD);
936            }
937        }
938    }
939
940    #[test]
941    fn scope_alloc_value_slice_str_are_scope_bound() {
942        let mut arena = BumpArena::new(InlineBacked::<256>::new()).unwrap();
943        let before = arena.allocated();
944        {
945            let scope = arena.scope();
946            let v: &mut u64 = scope.alloc(7u64).unwrap();
947            assert_eq!(*v, 7);
948            *v = 9;
949            assert_eq!(*v, 9);
950            let sl: &mut [u8] = scope.alloc_slice_copy(&[10u8, 20, 30]).unwrap();
951            assert_eq!(sl, &[10, 20, 30]);
952            let st: &mut str = scope.alloc_str("hi").unwrap();
953            assert_eq!(&*st, "hi");
954        }
955        assert_eq!(arena.allocated(), before, "scope reclaims the scratch");
956    }
957
958    #[test]
959    fn scope_rewinds_cursor_on_drop() {
960        let mut arena = BumpArena::new(InlineBacked::<256>::new()).unwrap();
961        let _keep = arena.alloc_uninit::<u32>().unwrap();
962        let before = arena.allocated();
963        {
964            let scope = arena.scope();
965            let _a = scope.alloc_uninit::<[u8; 32]>().unwrap();
966            let _b = scope.alloc_uninit::<[u8; 32]>().unwrap();
967            assert!(scope.arena_allocated() >= before + 64);
968        }
969        assert_eq!(arena.allocated(), before, "scope must rewind to its mark");
970        // The reclaimed region is reusable by the outer arena.
971        let _reuse = arena.alloc_uninit::<[u8; 32]>().unwrap();
972        assert_eq!(arena.allocated(), before + 32);
973    }
974
975    #[test]
976    fn nested_scopes_rewind_independently() {
977        let mut arena = BumpArena::new(InlineBacked::<512>::new()).unwrap();
978        let base = arena.allocated();
979        {
980            let mut outer = arena.scope();
981            let _o = outer.alloc_uninit::<[u8; 16]>().unwrap();
982            let after_outer = outer.arena_allocated();
983            {
984                let inner = outer.scope();
985                let _i = inner.alloc_uninit::<[u8; 64]>().unwrap();
986                assert!(inner.arena_allocated() >= after_outer + 64);
987            }
988            assert_eq!(
989                outer.arena_allocated(),
990                after_outer,
991                "inner scope rewinds to its own mark, leaving outer intact"
992            );
993        }
994        assert_eq!(arena.allocated(), base, "outer scope rewinds fully");
995    }
996
997    // Panic safety: a panic inside a scope body must still rewind (Drop runs on
998    // unwind). Needs std for catch_unwind.
999    #[cfg(feature = "std")]
1000    #[test]
1001    fn scope_rewinds_on_panic() {
1002        let mut arena = BumpArena::new(InlineBacked::<256>::new()).unwrap();
1003        let before = arena.allocated();
1004        let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1005            let scope = arena.scope();
1006            let _a = scope.alloc_uninit::<[u8; 64]>().unwrap();
1007            panic!("boom inside scope");
1008        }));
1009        assert!(r.is_err(), "the panic should propagate out of catch_unwind");
1010        assert_eq!(
1011            arena.allocated(),
1012            before,
1013            "Drop must rewind the cursor even on a panic unwind"
1014        );
1015    }
1016
1017    // The `OsBacked` forward exists only for OS-managed backings, so this test
1018    // needs `MmapBacked` (std + unix/windows). It proves the surface forwards to
1019    // the backing and that the pool-overflow path — reset, release the whole
1020    // region's pages, reuse — round-trips without `munmap`.
1021    #[cfg(all(feature = "std", any(unix, windows)))]
1022    #[test]
1023    #[cfg_attr(miri, ignore = "miri can't shim mmap / VirtualAlloc")]
1024    fn osbacked_forwards_and_release_after_reset_round_trips() {
1025        use crate::backing::{page_size, MmapBacked};
1026        use forge_alloc_core::OsBacked;
1027
1028        let mut arena = BumpArena::new(MmapBacked::new(64 * 1024).unwrap()).unwrap();
1029
1030        // Cross-interface consistency: the OsBacked surface must agree with the
1031        // independent FixedRange surface and the known reservation size — not a
1032        // self-referential `arena.x() == arena.backing().x()` tautology (which
1033        // would pass even if the forward were broken).
1034        assert_eq!(arena.region_size(), 64 * 1024);
1035        assert_eq!(arena.region_size(), arena.capacity()); // OsBacked vs cached FixedRange size
1036        assert_eq!(arena.base_ptr(), arena.base()); // OsBacked vs FixedRange base
1037
1038        let layout = NonZeroLayout::from_size_align(page_size(), 8).unwrap();
1039        let block = arena.allocate(layout).unwrap();
1040        // SAFETY: freshly allocated page-sized block.
1041        unsafe { core::ptr::write_bytes(block.cast::<u8>().as_ptr(), 0xEE, page_size()) };
1042
1043        // Pool-overflow path: no live allocations after reset, so releasing the
1044        // full region is sound; the mapping stays reserved for reuse.
1045        arena.reset();
1046        let (base, size) = (arena.base_ptr(), arena.region_size());
1047        // SAFETY: full region, no live allocations after reset.
1048        unsafe { arena.release_pages(base, size) };
1049
1050        // The still-mapped arena reuses cleanly — write must not fault.
1051        let block2 = arena.allocate(layout).unwrap();
1052        // SAFETY: freshly allocated page-sized block.
1053        unsafe { core::ptr::write_bytes(block2.cast::<u8>().as_ptr(), 0x11, page_size()) };
1054    }
1055}
1056
1057// ============================================================================
1058// Kani proof harnesses
1059//
1060// Kani is a bounded model checker that verifies properties of unsafe code
1061// over the entire state space of unconstrained inputs. These harnesses run
1062// under the `kani` cfg (set by `cargo kani`) and exercise the alignment
1063// rounding + bounds-check logic in `allocate`.
1064// ============================================================================
1065
1066// Kani proofs depend on `crate::backing::InlineBacked`; the `backing` module is gated
1067// behind the `std` feature in this crate (see Cargo.toml), so the proof
1068// module must be gated similarly. Kani CI must run with the `std`
1069// feature enabled for these proofs to compile.
1070#[cfg(all(kani, feature = "std"))]
1071mod kani_proofs {
1072    use super::*;
1073    use crate::backing::InlineBacked;
1074
1075    /// Any successful `allocate(layout)` returns a pointer aligned to
1076    /// `layout.align()`. Verified over all combinations of (cursor
1077    /// position, requested size, requested alignment) that fit a
1078    /// 1 KiB arena.
1079    #[kani::proof]
1080    #[kani::unwind(4)]
1081    fn allocate_returns_aligned_pointer() {
1082        let arena = BumpArena::new(InlineBacked::<1024>::new()).unwrap();
1083        // Bounded inputs — Kani enumerates the cross product.
1084        let size_log: u32 = kani::any();
1085        kani::assume(size_log <= 8); // size in 1..=256
1086        let align_log: u32 = kani::any();
1087        kani::assume(align_log <= 4); // align in {1,2,4,8,16}
1088        let size = 1usize << size_log;
1089        let align = 1usize << align_log;
1090        let layout = NonZeroLayout::from_size_align(size, align).unwrap();
1091        if let Ok(block) = arena.allocate(layout) {
1092            let p = block.cast::<u8>().as_ptr() as usize;
1093            assert!(p % align == 0);
1094            // And the slice length covers the requested size.
1095            assert!(block.len() >= size);
1096        }
1097    }
1098
1099    /// Repeated `allocate` calls produce strictly increasing cursor
1100    /// values that never exceed capacity. Verified over a small
1101    /// number of allocations on a 256-byte arena.
1102    #[kani::proof]
1103    #[kani::unwind(4)]
1104    fn cursor_monotonic_and_bounded() {
1105        let arena = BumpArena::new(InlineBacked::<256>::new()).unwrap();
1106        let layout = NonZeroLayout::from_size_align(8, 8).unwrap();
1107        let cap = arena.capacity();
1108        let mut last = 0usize;
1109        for _ in 0..3 {
1110            let before = arena.allocated();
1111            if arena.allocate(layout).is_ok() {
1112                let after = arena.allocated();
1113                assert!(after > before);
1114                assert!(after <= cap);
1115                last = after;
1116            }
1117        }
1118        assert!(last <= cap);
1119    }
1120}