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}