diff --git a/.github/workflows/miri.yml b/.github/workflows/ci.yml similarity index 91% rename from .github/workflows/miri.yml rename to .github/workflows/ci.yml index d587c94..7d3c47f 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: miri +name: CI on: push: @@ -46,7 +46,7 @@ jobs: key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Install Rust - run: rustup toolchain install nightly --no-self-update --profile minimal --component rust-src,rustfmt,clippy,miri + run: rustup toolchain install nightly --no-self-update --profile minimal --component rust-src,rustfmt,clippy - name: Check formatting run: cargo fmt -- --check @@ -54,8 +54,5 @@ jobs: - name: Run tests run: cargo test --all-features - - name: Run miri tests - run: cargo miri test - - name: Run clippy run: cargo clippy --all-features --all-targets -- -D warnings -W clippy::pedantic diff --git a/Cargo.lock b/Cargo.lock index b801a0c..1a9ba22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "linalloc" -version = "1.2.0" +version = "2.0.0" diff --git a/Cargo.toml b/Cargo.toml index 8740d3a..28d5f92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linalloc" -version = "1.2.0" +version = "2.0.0" edition = "2024" rust-version = "1.95" description = """ @@ -13,7 +13,6 @@ categories = ["memory-management"] exclude = [".github", "rustfmt.toml", ".gitignore"] [features] -lazy = [] nightly = [] [package.metadata.docs.rs] diff --git a/README.md b/README.md index bb8ada6..72df871 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,37 @@ # linalloc (Linear Allocator) -[![miri](https://github.com/qaijuang/linalloc/actions/workflows/miri.yml/badge.svg)](https://github.com/qaijuang/linalloc/actions/workflows/miri.yml) +[![CI](https://github.com/qaijuang/linalloc/actions/workflows/ci.yml/badge.svg)](https://github.com/qaijuang/linalloc/actions/workflows/ci.yml) [![MSRV](https://img.shields.io/crates/msrv/linalloc)](https://crates.io/crates/linalloc) [![crates.io](https://img.shields.io/crates/v/linalloc)](https://crates.io/crates/linalloc) [![docs.rs](https://img.shields.io/docsrs/linalloc)](https://docs.rs/linalloc) [![license](https://img.shields.io/github/license/qaijuang/linalloc)](https://github.com/qaijuang/linalloc/blob/main/LICENSE) -Small, fixed-capacity arena allocators for single-threaded Rust programs. +Small, fixed-capacity arena allocator for single-threaded Rust programs. You pick the capacity up front. The arena capacity never grows. Addresses stay stable. When it is full, fallible allocation returns `None`. ## Choose an arena -| Type | Feature | What it gives you | Drop behavior | -| ------------------------- | ------- | ------------------------------------------------ | --------------------------------------------- | -| `BumpArena` | default | Raw byte allocation from a fixed heap buffer | Values must be dropped by the caller | -| `TypedArena` | default | Values of one type from a fixed heap buffer | Drops live values in reverse allocation order | -| `BumpArenaLazy` | `lazy` | Raw byte allocation from reserved virtual memory | Values must be dropped by the caller | -| `TypedArenaLazy` | `lazy` | Values of one type from reserved virtual memory | Drops live values in reverse allocation order | -| `TypedArenaRef<'a, T, A>` | default | Values of one type from a backing allocator | Drops live values in reverse allocation order | - -All arenas are `!Send` and `!Sync`. They are deliberately single-threaded. +| Type | What it gives you | Drop behavior | +| ---------------------- | ------------------------------------------------ | --------------------------------------------- | +| `BumpArena` | Raw byte allocation from reserved virtual memory | Values must be dropped by the caller | +| `TypedArena<'a, T, A = BumpArena>` | Values of one type from a backing allocator | Drops live values in reverse allocation order | ## Feature flags -- `lazy` enables `BumpArenaLazy` and `TypedArenaLazy` on Unix and Windows. - `nightly` requires a nightly Rust toolchain and enables the unstable - standard-library `allocator_api` implementation for `BumpArena` and - `BumpArenaLazy`. + standard-library `allocator_api` implementation for `BumpArena`. Typed + arenas backed by `BumpArena` also use that allocator for their internal + tracking storage. ## Allocation APIs -All arena types expose `try_*` for fallible allocation and `alloc` / `alloc_*` for the -panicking variant. The older inherent methods, `TypedArena::alloc_raw`, -`TypedArenaLazy::alloc_raw`, `BumpArena::alloc_uninit_slice`, and -`BumpArenaLazy::alloc_uninit_slice`, remain available for compatibility but are -deprecated. +Both arenas expose `try_*` for fallible allocation and `alloc` / `alloc_*` for the +panicking variant. The 2.0 API intentionally keeps only `BumpArena` and +`TypedArena`, the old lazy/ref arena names were removed. -## Use a bump arena +## Using bump arena `BumpArena` gives you uninitialized bytes. You choose the layout, initialize the memory, and drop any values you place there. @@ -56,14 +49,14 @@ unsafe { ptr.write(42) }; assert_eq!(unsafe { *ptr }, 42); ``` -## Use as a standard-library allocator +### With standard-library allocator -Enable `nightly` when you want an untyped arena to back standard-library +Enable `nightly` when you want bump arena to back standard-library collections that use the unstable allocator API: ```toml [dependencies] -linalloc = { version = "1", features = ["nightly"] } +linalloc = { version = "2", features = ["nightly"] } ``` ```rust @@ -84,49 +77,18 @@ linalloc = { version = "1", features = ["nightly"] } # } ``` -Use `features = ["nightly", "lazy"]` when the allocator is `BumpArenaLazy`. - -## Use a typed arena +### In typed arena as backing allocator -`TypedArena` stores initialized `T` values and drops the live values when -the arena is reset or dropped. +`TypedArena<'a, T, A = BumpArena>` stores initialized `T` values in a backing allocator +`A` that implements the `UninitAllocator` trait +and drops the live values when the arena is reset or dropped. ```rust -use linalloc::TypedArena; - -let arena = TypedArena::::new(4); -let value = arena.try_alloc("hello".to_owned()).unwrap(); - -value.push_str(" world"); -assert_eq!(value, "hello world"); -``` - -The borrow checker prevents resetting a typed arena while references into it -are still live: - -```rust,compile_fail -use linalloc::TypedArena; - -let mut arena = TypedArena::::new(1); -let value = arena.try_alloc("held".to_owned()).unwrap(); -// ----- immutable borrow occurs here -arena.reset(); -//^^^^^^^^^^^^^ mutable borrow occurs here -drop(value); -// ----- immutable borrow later used here -``` - -## Use a typed arena with a backing allocator - -`TypedArenaRef<'a, T, A>` stores initialized `T` values in a backing allocator -`A` that implements the `UninitAllocator` trait and drops the live values when the arena is reset or dropped. - -```rust -use linalloc::{BumpArena, TypedArenaRef}; +use linalloc::{BumpArena, TypedArena}; let bump = BumpArena::new(128); // Implements `UninitAllocator` -let mut foo_arena = TypedArenaRef::::new_in(&bump); -let mut bar_arena = TypedArenaRef::::new_in(&bump); +let mut foo_arena = TypedArena::::new_in(&bump); +let mut bar_arena = TypedArena::::new_in(&bump); let foo = foo_arena.try_alloc("foo".to_owned()).unwrap(); let bar = bar_arena.try_alloc("bar".to_owned()).unwrap(); @@ -135,62 +97,42 @@ assert_eq!(foo, "foo"); assert_eq!(bar, "bar"); ``` -## Use lazy arenas - -Enable `lazy` when you want to reserve a large virtual address range and -commit physical memory only as allocation advances: +## Reading OS errors -```toml -[dependencies] -linalloc = { version = "1", features = ["lazy"] } -``` - -The lazy feature is supported on Unix and Windows targets. - -## Read lazy OS errors - -Lazy arenas keep the raw OS code from the last failed reserve or commit call. +Bump arena keeps the raw OS code from the last failed reserve or commit call. Use it when `try_new` fails, or when allocation returns `None` and you need to know whether the OS refused more committed memory. ```rust -#[cfg(all(feature = "lazy", any(unix, windows), not(miri)))] -{ - use core::alloc::Layout; - - use linalloc::{BumpArenaLazy, TypedArenaLazy}; +use core::alloc::Layout; - if let Err(code) = BumpArenaLazy::try_new(usize::MAX) { - assert_eq!(Some(code), std::io::Error::last_os_error().raw_os_error()); - } +use linalloc::BumpArena; - let typed = TypedArenaLazy::::new(1); - assert!(typed.try_alloc(1).is_some()); - assert!(typed.try_alloc(2).is_none()); - assert_eq!(typed.last_os_error_code(), None); +if let Err(code) = BumpArena::try_new(usize::MAX) { + assert_eq!(Some(code), std::io::Error::last_os_error().raw_os_error()); } + + +let arena = BumpArena::new(128); +let _slot = arena.try_alloc_uninit(Layout::new::()).unwrap(); +assert_eq!(arena.last_os_error_code(), None); ``` ## Safety -Untyped arenas hand you uninitialized bytes. Do not read them until you have -written them. Values stored in untyped arenas are not dropped automatically. +Bump arenas hand you uninitialized bytes. Do not read them until you have +written them. Values stored in bump arenas are not dropped automatically. Typed arenas own initialized values. `reset` takes `&mut self`, drops live -values in reverse allocation order, and then reuses the storage. +values in reverse allocation order, and clears the typed arena’s tracking. It +does not rewind the backing allocator; reuse is governed by that allocator’s +own reset/drop lifecycle. -For untyped arenas, `reset` is unsafe. All returned slices must be dead, and +For bump arenas, `reset` is unsafe. All returned slices must be dead, and any values stored in the arena must already have been dropped. -With `nightly`, `BumpArena` and `BumpArenaLazy` implement -`core::alloc::Allocator`. Per-block `deallocate` is a no-op; memory is reclaimed +With `nightly`, `BumpArena` implements +`core::alloc::Allocator`. Per-block `deallocate` is a no-op -- memory is reclaimed only by `reset` or by dropping the arena. `grow`, `grow_zeroed`, and `shrink` resize only the most recent allocation in place. Drop all collections and values that use an arena allocator before calling `reset`. - -## Miri - -Miri covers the default eager arenas. - -The lazy arenas use platform virtual-memory calls. Miri does not currently -support every protection mode used by that path. diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 5d56faf..4029991 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,3 @@ [toolchain] -channel = "nightly" +channel = "nightly-2026-07-05" +components = ["rust-src", "rustfmt", "clippy"] diff --git a/src/bump_arena.rs b/src/bump_arena.rs index 40df6ff..47492c8 100644 --- a/src/bump_arena.rs +++ b/src/bump_arena.rs @@ -5,21 +5,32 @@ use core::mem::MaybeUninit; use core::ptr::NonNull; use core::slice; -use crate::UninitAllocator; +use crate::{UninitAllocator, sys}; -/// A fixed‑capacity, single‑threaded bump allocator. +/// A fixed‑capacity, single‑threaded bump allocator backed by lazy‑committed +/// virtual memory. /// -/// The arena hands out mutable slices of [`MaybeUninit`] that -/// are logically uninitialised. The caller must initialise the -/// memory before reading from it. The backing store is a boxed -/// slice whose capacity is set once at construction and **never -/// changes**, so addresses remain stable. For zero capacity, the -/// boxed slice may be a dangling, non‑allocated value. +/// `BumpArena` provides mutable slices of [`MaybeUninit`] that are +/// logically uninitialised. The caller must initialise the memory before +/// reading from it. The backing store is a reserved virtual‑memory region +/// whose total capacity is set once at construction and **never changes**. +/// Physical memory is committed on demand as the bump pointer advances, so +/// the arena can be created with a very large capacity without immediately +/// consuming physical memory. +/// +/// # Memory commitment strategy +/// +/// The arena uses incremental commitment: the initial physical footprint is +/// tiny, and pages are committed in chunks as allocations request more memory. +/// Committed memory is never decommitted until the entire arena is dropped. +/// This gives stable addresses, predictable performance, and minimal upfront +/// resource usage. /// /// # Thread safety /// -/// `BumpArena` is **`!Send` and `!Sync`** -- it contains a raw -/// pointer marker, which is `!Send` and `!Sync`. +/// `BumpArena` is **`!Send` and `!Sync`** -- it contains a raw‑pointer marker +/// that prevents the value from leaving the thread where it was created. The +/// arena is therefore safe to use in single‑threaded contexts only. /// /// # Examples /// @@ -32,7 +43,7 @@ use crate::UninitAllocator; /// /// // Allocate space for a `u64`. /// let layout = Layout::new::(); -/// let slice = bump.try_alloc_uninit(layout).unwrap(); +/// let slice = bump.try_alloc_uninit(layout).expect("out of memory"); /// let ptr = slice.as_mut_ptr().cast::(); /// unsafe { ptr.write(42) }; /// let val = unsafe { &*ptr }; @@ -42,30 +53,76 @@ use crate::UninitAllocator; /// ``` #[derive(Debug)] pub struct BumpArena { - base: NonNull<[MaybeUninit]>, + base: NonNull, + capacity: usize, + // pays the cost of syscall upfront. + page_size: usize, offset: Cell, + commit: Cell, + last_os_error: Cell, _invariant: PhantomData<*const ()>, } impl BumpArena { - /// Creates a bump allocator with exactly `capacity` bytes of memory. + /// Creates a bump allocator that can grow up to `capacity` bytes. /// - /// The memory is allocated from the global allocator and is - /// **uninitialised**. No zeroing or default‑initialisation is - /// performed. + /// The memory is **reserved** but not committed -- physical pages are + /// allocated only when needed, as the bump pointer moves forward. + /// If `capacity` is zero, the arena is empty and will reject all non‑zero + /// allocations. /// /// # Panics /// - /// If allocation fails, the global allocator error handler is - /// invoked (typically aborting the process). + /// Panics if the operating system cannot reserve the requested address + /// range. A zero‑capacity arena never panics. + /// + /// # Examples + /// + /// ``` + /// use linalloc::BumpArena; + /// + /// let arena = BumpArena::new(1024); + /// assert_eq!(arena.capacity(), 1024); + /// assert_eq!(arena.used(), 0); + /// ``` #[must_use] pub fn new(capacity: usize) -> Self { - Self { - // SAFETY: `Box` is guaranteed to be non-null. - base: unsafe { NonNull::new_unchecked(Box::into_raw(Box::new_uninit_slice(capacity))) }, + Self::try_new(capacity).expect("BumpArena::new failed to reserve memory") + } + + /// Like [`new`], but with no panic behaviour. + /// + /// # Errors + /// + /// Returns OS error code if reservation fails. + /// + /// [`new`]: BumpArena::new + pub fn try_new(capacity: usize) -> Result { + // saves us one unnecessary syscall. + if capacity == 0 { + return Ok(Self { + base: NonNull::dangling(), + capacity: 0, + page_size: usize::MAX, + offset: Cell::new(0), + commit: Cell::new(0), + last_os_error: Cell::new(0), + _invariant: PhantomData, + }); + } + + let base = sys::reserve(capacity)?; + let page_size = sys::page_size(); + + Ok(Self { + base, + capacity, + page_size, offset: Cell::new(0), + commit: Cell::new(0), + last_os_error: Cell::new(0), _invariant: PhantomData, - } + }) } /// Allocates a mutable slice of [`MaybeUninit`] that satisfies @@ -76,7 +133,7 @@ impl BumpArena { /// # Panics /// /// Panics if the arena does not have enough free space after accounting for - /// the requested size and alignment. + /// the requested size and alignment, or if a required memory commit fails. pub fn alloc_uninit(&self, layout: Layout) -> &mut [MaybeUninit] { self.alloc_uninit_impl(layout).expect("BumpArena allocation failed") } @@ -85,31 +142,25 @@ impl BumpArena { /// `layout`. /// /// The returned memory is **logically uninitialised** -- it must be - /// initialised (e.g. with [`core::ptr::write`]) before any reads are - /// performed. + /// initialised before any reads are performed (for example, using + /// [`core::ptr::write`]). + /// + /// The slice borrows the arena immutably (`&self`), so the arena cannot + /// be dropped or moved while the slice is alive. This guarantees that + /// multiple allocations can coexist without aliasing. /// - /// The slice borrows the arena immutably (`&self`), so the arena - /// cannot be dropped or moved while the slice is alive. The - /// backing store is never resized, so non‑zero allocations remain - /// valid until the arena is dropped or [`BumpArena::reset`] is called. A - /// zero‑size allocation returns a well‑aligned dangling slice and - /// does not advance the bump pointer. + /// A zero‑size allocation returns a well‑aligned dangling slice and does + /// **not** advance the bump pointer. /// /// # Returns /// - /// `None` if the arena does not have enough free space after - /// accounting for the requested size and alignment. + /// `None` if the arena does not have enough free space after accounting + /// for the requested size and alignment, or if a required memory commit + /// fails. pub fn try_alloc_uninit(&self, layout: Layout) -> Option<&mut [MaybeUninit]> { self.alloc_uninit_impl(layout) } - /// Allocates a mutable slice of [`MaybeUninit`] that satisfies - /// `layout`. - #[deprecated(since = "1.2.0", note = "Use `BumpArena::try_alloc_uninit` instead.")] - pub fn alloc_uninit_slice(&self, layout: Layout) -> Option<&mut [MaybeUninit]> { - self.alloc_uninit_impl(layout) - } - #[allow(clippy::mut_from_ref)] fn alloc_uninit_impl(&self, layout: Layout) -> Option<&mut [MaybeUninit]> { let size = layout.size(); @@ -120,7 +171,7 @@ impl BumpArena { let align = layout.align(); let offset = self.offset.get(); - let base = self.base.as_ptr().cast::>(); + let base = self.base.as_ptr(); let base_addr = base as usize; let addr = base_addr + offset; @@ -128,38 +179,102 @@ impl BumpArena { let aligned_addr = addr.checked_add(align_mask)? & !align_mask; let aligned = aligned_addr - base_addr; let offset = aligned.checked_add(size)?; - if offset > self.capacity() { + if offset > self.capacity { return None; } + if offset > self.commit.get() { + return self.alloc_uninit_bump(aligned, offset, size); + } self.offset.set(offset); + // Safety: [aligned, offset) lies within the reservation and is + // backed by committed memory. The bump pointer is monotonically + // advanced, so no two allocations overlap. The returned slice borrows + // `self`, tying its lifetime to the arena. + unsafe { + let ptr = base.add(aligned); + Some(slice::from_raw_parts_mut(ptr.cast(), size)) + } + } + + // With the code in `alloc_uninit_bump()` out of the way, `alloc_uninit_impl()` compiles down to some super tight assembly. + #[cold] + #[inline(never)] + #[allow(clippy::mut_from_ref)] + fn alloc_uninit_bump( + &self, + aligned: usize, + offset: usize, + size: usize, + ) -> Option<&mut [MaybeUninit]> { + let current = self.commit.get(); + + // Round offset up to the next page boundary, capped by capacity. + let needed = offset.checked_next_multiple_of(self.page_size)?.min(self.capacity); + // Safety: - // - `base` is a non‑null, heap‑allocated box -- the region - // [aligned, aligned+size) is within the allocation. - // - The bump pointer is monotonically advanced -- no two - // allocations overlap. - // - The returned reference borrows `self`, tying its lifetime - // to the arena. - unsafe { Some(slice::from_raw_parts_mut(base.add(aligned), size)) } + // > `current` is page‑aligned and within the reservation. + // > `needed - current` is a multiple of the page size. + // > The range has not been committed before, so no overlapping commit. + unsafe { + let addr = NonNull::new_unchecked(self.base.as_ptr().add(current)); + if let Err(code) = sys::commit(addr, needed - current) { + // capture the OS error code immediately + self.last_os_error.set(code); + return None; + } + } + + self.commit.set(needed); + self.offset.set(offset); + + unsafe { + let ptr = self.base.as_ptr().add(aligned); + Some(slice::from_raw_parts_mut(ptr.cast(), size)) + } + } + + /// Returns the OS error code from the last failed allocation or commit + /// operation, if any. + /// + /// The returned value is the raw platform‑specific error code: + /// - On Unix: the `errno` value (positive integer). + /// - On Windows: the `GetLastError` code. + /// + /// Returns `None` if no OS-backed reserve or commit failure has been + /// recorded for this arena. + /// + /// # Semantics + /// + /// This method behaves analogously to `std::io::Error::last_os_error` at + /// the point of the failed internal system call. The error code is stable + /// until the next failure overwrites it. + pub fn last_os_error_code(&self) -> Option { + let code = self.last_os_error.get(); + if code == 0 { None } else { Some(code) } } - /// Resets the bump pointer to the beginning, making the entire - /// capacity available for new allocations. + /// Resets the bump pointer to the beginning, reusing already‑committed + /// memory. /// /// # Safety /// /// All previously returned slices must no longer be in use. - /// This method **does not** run any destructors -- the caller is - /// responsible for dropping all values placed in the arena before - /// calling `reset`. + /// This method does **not** run any destructors -- the caller is + /// responsible for dropping all values placed in the arena before calling + /// `reset`. pub unsafe fn reset(&self) { self.offset.set(0); } /// Returns the total capacity of the backing memory, in bytes. + /// + /// This is the value passed to [`new`] and never changes. + /// + /// [`new`]: BumpArena::new pub fn capacity(&self) -> usize { - self.base.len() + self.capacity } /// Returns the number of bytes that have been allocated so far. @@ -170,8 +285,10 @@ impl BumpArena { impl Drop for BumpArena { fn drop(&mut self) { - unsafe { - drop(Box::from_raw(self.base.as_ptr())); + if self.capacity > 0 { + unsafe { + sys::release(self.base, self.capacity); + } } } } @@ -185,12 +302,8 @@ unsafe impl UninitAllocator for BumpArena { // Safety: // -// `BumpArena` provides correctly aligned, non‑overlapping memory that remains -// stable until the arena is dropped or reset. The `Allocator` contract is -// upheld: `allocate` hands out memory from the bump pointer, `deallocate` is a -// deliberate no‑op (the arena cannot reclaim individual blocks), and `grow` / -// `shrink` only resize the most recent allocation in place when it is the last -// block. +// Same contract as for `&BumpArena`, with the addition that `grow` may +// trigger a virtual‑memory commit if the new size requires it. #[cfg(feature = "nightly")] unsafe impl core::alloc::Allocator for BumpArena { fn allocate(&self, layout: Layout) -> Result, core::alloc::AllocError> { @@ -211,7 +324,7 @@ unsafe impl core::alloc::Allocator for BumpArena { new_layout: Layout, ) -> Result, core::alloc::AllocError> { let offset = self.offset.get(); - let base = self.base.as_ptr().cast::>() as usize; + let base = self.base.as_ptr() as usize; let old_ptr = ptr.as_ptr() as usize; let old_offset = old_ptr.checked_sub(base).ok_or(core::alloc::AllocError)?; let old_size = old_layout.size(); @@ -224,16 +337,20 @@ unsafe impl core::alloc::Allocator for BumpArena { } let required_offset = old_offset.checked_add(new_size).ok_or(core::alloc::AllocError)?; - if required_offset > self.capacity() { + if required_offset > self.capacity { return Err(core::alloc::AllocError); } + if required_offset > self.commit.get() { + let slice = self + .alloc_uninit_bump(old_offset, required_offset, new_size) + .ok_or(core::alloc::AllocError)?; + let ptr = unsafe { NonNull::new_unchecked(slice.as_mut_ptr().cast()) }; + return Ok(NonNull::slice_from_raw_parts(ptr, new_size)); + } + self.offset.set(required_offset); - let new_ptr = unsafe { - NonNull::new_unchecked( - self.base.as_ptr().cast::>().add(old_offset).cast::(), - ) - }; + let new_ptr = unsafe { NonNull::new_unchecked(self.base.as_ptr().add(old_offset)) }; Ok(NonNull::slice_from_raw_parts(new_ptr, new_size)) } @@ -245,7 +362,6 @@ unsafe impl core::alloc::Allocator for BumpArena { ) -> Result, core::alloc::AllocError> { let new_ptr = unsafe { self.grow(ptr, old_layout, new_layout)? }; let old_size = old_layout.size(); - // Zero the newly added tail. let new_bytes = unsafe { new_ptr.as_ptr().cast::().add(old_size) }; unsafe { core::ptr::write_bytes(new_bytes, 0, new_layout.size() - old_size) }; Ok(new_ptr) @@ -258,7 +374,7 @@ unsafe impl core::alloc::Allocator for BumpArena { new_layout: Layout, ) -> Result, core::alloc::AllocError> { let offset = self.offset.get(); - let base = self.base.as_ptr().cast::>() as usize; + let base = self.base.as_ptr() as usize; let old_ptr = ptr.as_ptr() as usize; let old_offset = old_ptr.checked_sub(base).ok_or(core::alloc::AllocError)?; let old_size = old_layout.size(); @@ -272,11 +388,7 @@ unsafe impl core::alloc::Allocator for BumpArena { let new_offset = old_offset.checked_add(new_size).ok_or(core::alloc::AllocError)?; self.offset.set(new_offset); - let new_ptr = unsafe { - NonNull::new_unchecked( - self.base.as_ptr().cast::>().add(old_offset).cast::(), - ) - }; + let new_ptr = unsafe { NonNull::new_unchecked(self.base.as_ptr().add(old_offset)) }; Ok(NonNull::slice_from_raw_parts(new_ptr, new_size)) } } @@ -314,7 +426,7 @@ mod tests { #[test] fn alloc_alignment_and_length() { let bump = BumpArena::new(128); - let base = bump.base.as_ptr().cast::>() as usize; + let base = bump.base.as_ptr() as usize; let mut prev_end = 0usize; for align in [1, 2, 4, 8, 16] { @@ -403,8 +515,7 @@ mod tests { let block = alloc.allocate(old).unwrap(); let ptr = block_ptr(block); - let start = - (ptr.as_ptr() as usize) - (bump.base.as_ptr().cast::>() as usize); + let start = (ptr.as_ptr() as usize) - (bump.base.as_ptr() as usize); unsafe { ptr.as_ptr().write_bytes(0xAB, old.size()) }; let grown_block = unsafe { alloc.grow(ptr, old, grown).unwrap() }; @@ -442,6 +553,31 @@ mod tests { } } + #[cfg(feature = "nightly")] + #[test] + fn allocator_grow_across_commit_preserves_the_block() { + let page = sys::page_size(); + let bump = BumpArena::new(page + 64); + let alloc = ≎ + let old = Layout::from_size_align(page - 8, 1).unwrap(); + let grown = Layout::from_size_align(page + 16, 1).unwrap(); + + let block = alloc.allocate(old).unwrap(); + let ptr = block_ptr(block); + unsafe { ptr.as_ptr().write_bytes(0xAB, old.size()) }; + assert_eq!(bump.used(), old.size()); + assert_eq!(bump.commit.get(), page); + + let grown_block = unsafe { alloc.grow(ptr, old, grown).unwrap() }; + let grown_ptr = block_ptr(grown_block); + + assert_eq!(grown_ptr, ptr); + assert_eq!(bump.used(), grown.size()); + assert!(bump.commit.get() >= grown.size()); + assert_eq!(unsafe { grown_ptr.as_ptr().read() }, 0xAB); + assert_eq!(unsafe { grown_ptr.as_ptr().add(old.size() - 1).read() }, 0xAB); + } + #[cfg(feature = "nightly")] #[test] fn vec_try_reserve_can_grow_inside_allocator() { @@ -455,6 +591,13 @@ mod tests { assert_eq!(&values, &[1, 2]); } + #[test] + fn try_new_reports_os_error_for_unreservable_capacity() { + let err = BumpArena::try_new(usize::MAX).unwrap_err(); + assert_eq!(Some(err), std::io::Error::last_os_error().raw_os_error()); + assert_ne!(err, 0); + } + #[cfg(feature = "nightly")] #[test] fn allocator_rejects_resize_when_pointer_does_not_fit_new_alignment() { diff --git a/src/bump_arena_lazy.rs b/src/bump_arena_lazy.rs deleted file mode 100644 index ab1bd3b..0000000 --- a/src/bump_arena_lazy.rs +++ /dev/null @@ -1,626 +0,0 @@ -use core::alloc::Layout; -use core::cell::Cell; -use core::marker::PhantomData; -use core::mem::MaybeUninit; -use core::ptr::NonNull; -use core::slice; - -use crate::{UninitAllocator, sys}; - -/// A fixed‑capacity, single‑threaded bump allocator backed by lazy‑committed -/// virtual memory. -/// -/// `BumpArenaLazy` provides mutable slices of [`MaybeUninit`] that are -/// logically uninitialised. The caller must initialise the memory before -/// reading from it. The backing store is a reserved virtual‑memory region -/// whose total capacity is set once at construction and **never changes**. -/// Physical memory is committed on demand as the bump pointer advances, so -/// the arena can be created with a very large capacity without immediately -/// consuming physical memory. -/// -/// # Memory commitment strategy -/// -/// The arena uses incremental commitment: the initial physical footprint is -/// tiny, and pages are committed in chunks as allocations request more memory. -/// Committed memory is never decommitted until the entire arena is dropped. -/// This gives stable addresses, predictable performance, and minimal upfront -/// resource usage. -/// -/// # Thread safety -/// -/// `BumpArenaLazy` is **`!Send` and `!Sync`** -- it contains a raw‑pointer marker -/// that prevents the value from leaving the thread where it was created. The -/// arena is therefore safe to use in single‑threaded contexts only. -/// -/// # Examples -/// -/// ``` -/// use core::alloc::Layout; -/// -/// use linalloc::BumpArenaLazy; -/// -/// let bump = BumpArenaLazy::new(1024); -/// -/// // Allocate space for a `u64`. -/// let layout = Layout::new::(); -/// let slice = bump.try_alloc_uninit(layout).expect("out of memory"); -/// let ptr = slice.as_mut_ptr().cast::(); -/// unsafe { ptr.write(42) }; -/// let val = unsafe { &*ptr }; -/// assert_eq!(*val, 42); -/// -/// // Memory is freed when `bump` goes out of scope. -/// ``` -#[derive(Debug)] -pub struct BumpArenaLazy { - base: NonNull, - capacity: usize, - // pays the cost of syscall upfront. - page_size: usize, - offset: Cell, - commit: Cell, - last_os_error: Cell, - _invariant: PhantomData<*const ()>, -} - -impl BumpArenaLazy { - /// Creates a bump allocator that can grow up to `capacity` bytes. - /// - /// The memory is **reserved** but not committed -- physical pages are - /// allocated only when needed, as the bump pointer moves forward. - /// If `capacity` is zero, the arena is empty and will reject all non‑zero - /// allocations. - /// - /// # Panics - /// - /// Panics if the operating system cannot reserve the requested address - /// range. A zero‑capacity arena never panics. - /// - /// # Examples - /// - /// ``` - /// use linalloc::BumpArenaLazy; - /// - /// let arena = BumpArenaLazy::new(1024); - /// assert_eq!(arena.capacity(), 1024); - /// assert_eq!(arena.used(), 0); - /// ``` - #[must_use] - pub fn new(capacity: usize) -> Self { - Self::try_new(capacity).expect("BumpArenaLazy::new failed to reserve memory") - } - - /// Like [`new`], but with no panic behaviour. - /// - /// # Errors - /// - /// Returns OS error code if reservation fails. - /// - /// [`new`]: BumpArenaLazy::new - pub fn try_new(capacity: usize) -> Result { - let page_size = sys::page_size(); - - // saves us one unnecessary syscall. - if capacity == 0 { - return Ok(Self { - base: NonNull::dangling(), - capacity: 0, - page_size, - offset: Cell::new(0), - commit: Cell::new(0), - last_os_error: Cell::new(0), - _invariant: PhantomData, - }); - } - - let base = sys::reserve(capacity)?; - - Ok(Self { - base, - capacity, - page_size, - offset: Cell::new(0), - commit: Cell::new(0), - last_os_error: Cell::new(0), - _invariant: PhantomData, - }) - } - - /// Allocates a mutable slice of [`MaybeUninit`] that satisfies - /// `layout`, panicking if the allocation fails. - /// - /// See [`BumpArenaLazy::try_alloc_uninit`] for fallible allocation semantics. - /// - /// # Panics - /// - /// Panics if the arena does not have enough free space after accounting for - /// the requested size and alignment, or if a required memory commit fails. - pub fn alloc_uninit(&self, layout: Layout) -> &mut [MaybeUninit] { - self.alloc_uninit_impl(layout).expect("BumpArenaLazy allocation failed") - } - - /// Allocates a mutable slice of [`MaybeUninit`] that satisfies - /// `layout`. - /// - /// The returned memory is **logically uninitialised** -- it must be - /// initialised before any reads are performed (for example, using - /// [`core::ptr::write`]). - /// - /// The slice borrows the arena immutably (`&self`), so the arena cannot - /// be dropped or moved while the slice is alive. This guarantees that - /// multiple allocations can coexist without aliasing. - /// - /// A zero‑size allocation returns a well‑aligned dangling slice and does - /// **not** advance the bump pointer. - /// - /// # Returns - /// - /// `None` if the arena does not have enough free space after accounting - /// for the requested size and alignment, or if a required memory commit - /// fails. - pub fn try_alloc_uninit(&self, layout: Layout) -> Option<&mut [MaybeUninit]> { - self.alloc_uninit_impl(layout) - } - - /// Allocates a mutable slice of [`MaybeUninit`] that satisfies - /// `layout`. - #[deprecated(since = "1.2.0", note = "Use `BumpArenaLazy::try_alloc_uninit` instead.")] - pub fn alloc_uninit_slice(&self, layout: Layout) -> Option<&mut [MaybeUninit]> { - self.alloc_uninit_impl(layout) - } - - #[allow(clippy::mut_from_ref)] - fn alloc_uninit_impl(&self, layout: Layout) -> Option<&mut [MaybeUninit]> { - let size = layout.size(); - if size == 0 { - let ptr = layout.dangling_ptr().as_ptr().cast::>(); - return Some(unsafe { slice::from_raw_parts_mut(ptr, 0) }); - } - - let align = layout.align(); - let offset = self.offset.get(); - let base = self.base.as_ptr(); - - let base_addr = base as usize; - let addr = base_addr + offset; - let align_mask = align - 1; - let aligned_addr = addr.checked_add(align_mask)? & !align_mask; - let aligned = aligned_addr - base_addr; - let offset = aligned.checked_add(size)?; - if offset > self.capacity { - return None; - } - - if offset > self.commit.get() { - return self.alloc_uninit_bump(aligned, offset, size); - } - self.offset.set(offset); - - // Safety: [aligned, offset) lies within the reservation and is - // backed by committed memory. The bump pointer is monotonically - // advanced, so no two allocations overlap. The returned slice borrows - // `self`, tying its lifetime to the arena. - unsafe { - let ptr = base.add(aligned); - Some(slice::from_raw_parts_mut(ptr.cast(), size)) - } - } - - // With the code in `alloc_uninit_bump()` out of the way, `alloc_uninit_impl()` compiles down to some super tight assembly. - #[cold] - #[inline(never)] - #[allow(clippy::mut_from_ref)] - fn alloc_uninit_bump( - &self, - aligned: usize, - offset: usize, - size: usize, - ) -> Option<&mut [MaybeUninit]> { - let current = self.commit.get(); - - // Round offset up to the next page boundary, capped by capacity. - let needed = offset.checked_next_multiple_of(self.page_size)?.min(self.capacity); - - // Safety: - // > `current` is page‑aligned and within the reservation. - // > `needed - current` is a multiple of the page size. - // > The range has not been committed before, so no overlapping commit. - unsafe { - let addr = NonNull::new_unchecked(self.base.as_ptr().add(current)); - if let Err(code) = sys::commit(addr, needed - current) { - // capture the OS error code immediately - self.last_os_error.set(code); - return None; - } - } - - self.commit.set(needed); - self.offset.set(offset); - - unsafe { - let ptr = self.base.as_ptr().add(aligned); - Some(slice::from_raw_parts_mut(ptr.cast(), size)) - } - } - - /// Returns the OS error code from the last failed allocation or commit - /// operation, if any. - /// - /// The returned value is the raw platform‑specific error code: - /// - On Unix: the `errno` value (positive integer). - /// - On Windows: the `GetLastError` code. - /// - /// Returns `None` if no OS-backed reserve or commit failure has been - /// recorded for this arena. - /// - /// # Semantics - /// - /// This method behaves analogously to [`std::io::Error::last_os_error`] at - /// the point of the failed internal system call. The error code is stable - /// until the next failure overwrites it. - /// - /// [`std::io::Error::last_os_error`]: std::io::Error::last_os_error - pub fn last_os_error_code(&self) -> Option { - let code = self.last_os_error.get(); - if code == 0 { None } else { Some(code) } - } - - /// Resets the bump pointer to the beginning, reusing already‑committed - /// memory. - /// - /// # Safety - /// - /// All previously returned slices must no longer be in use. - /// This method does **not** run any destructors -- the caller is - /// responsible for dropping all values placed in the arena before calling - /// `reset`. - pub unsafe fn reset(&self) { - self.offset.set(0); - } - - /// Returns the total capacity of the backing memory, in bytes. - /// - /// This is the value passed to [`new`] and never changes. - /// - /// [`new`]: BumpArenaLazy::new - pub fn capacity(&self) -> usize { - self.capacity - } - - /// Returns the number of bytes that have been allocated so far. - pub fn used(&self) -> usize { - self.offset.get() - } -} - -impl Drop for BumpArenaLazy { - fn drop(&mut self) { - if self.capacity > 0 { - unsafe { - sys::release(self.base, self.capacity); - } - } - } -} - -// Safety: all safety invariants required by `UninitAllocator` are upheld by `BumpArenaLazy`. -unsafe impl UninitAllocator for BumpArenaLazy { - fn try_alloc_uninit(&self, layout: Layout) -> Option<&mut [MaybeUninit]> { - self.alloc_uninit_impl(layout) - } -} - -// Safety: -// -// Same contract as for `&BumpArena`, with the addition that `grow` may -// trigger a virtual‑memory commit if the new size requires it. -#[cfg(feature = "nightly")] -unsafe impl core::alloc::Allocator for BumpArenaLazy { - fn allocate(&self, layout: Layout) -> Result, core::alloc::AllocError> { - let slice = self.alloc_uninit_impl(layout).ok_or(core::alloc::AllocError)?; - // SAFETY: `slice` is guaranteed to be non-null and valid for `layout.size()` bytes. - let ptr = unsafe { NonNull::new_unchecked(slice.as_mut_ptr().cast()) }; - Ok(NonNull::slice_from_raw_parts(ptr, layout.size())) - } - - unsafe fn deallocate(&self, _ptr: NonNull, _layout: Layout) { - // Bump allocator memory is reclaimed only via `reset` or `Drop`. - } - - unsafe fn grow( - &self, - ptr: NonNull, - old_layout: Layout, - new_layout: Layout, - ) -> Result, core::alloc::AllocError> { - let offset = self.offset.get(); - let base = self.base.as_ptr() as usize; - let old_ptr = ptr.as_ptr() as usize; - let old_offset = old_ptr.checked_sub(base).ok_or(core::alloc::AllocError)?; - let old_size = old_layout.size(); - let new_size = new_layout.size(); - let old_end = old_offset.checked_add(old_size).ok_or(core::alloc::AllocError)?; - let is_last = old_end == offset; - - if !is_last || new_size <= old_size || !old_ptr.is_multiple_of(new_layout.align()) { - return Err(core::alloc::AllocError); - } - - let required_offset = old_offset.checked_add(new_size).ok_or(core::alloc::AllocError)?; - if required_offset > self.capacity { - return Err(core::alloc::AllocError); - } - - if required_offset > self.commit.get() { - let slice = self - .alloc_uninit_bump(old_offset, required_offset, new_size) - .ok_or(core::alloc::AllocError)?; - let ptr = unsafe { NonNull::new_unchecked(slice.as_mut_ptr().cast()) }; - return Ok(NonNull::slice_from_raw_parts(ptr, new_size)); - } - - self.offset.set(required_offset); - let new_ptr = unsafe { NonNull::new_unchecked(self.base.as_ptr().add(old_offset)) }; - Ok(NonNull::slice_from_raw_parts(new_ptr, new_size)) - } - - unsafe fn grow_zeroed( - &self, - ptr: NonNull, - old_layout: Layout, - new_layout: Layout, - ) -> Result, core::alloc::AllocError> { - let new_ptr = unsafe { self.grow(ptr, old_layout, new_layout)? }; - let old_size = old_layout.size(); - let new_bytes = unsafe { new_ptr.as_ptr().cast::().add(old_size) }; - unsafe { core::ptr::write_bytes(new_bytes, 0, new_layout.size() - old_size) }; - Ok(new_ptr) - } - - unsafe fn shrink( - &self, - ptr: NonNull, - old_layout: Layout, - new_layout: Layout, - ) -> Result, core::alloc::AllocError> { - let offset = self.offset.get(); - let base = self.base.as_ptr() as usize; - let old_ptr = ptr.as_ptr() as usize; - let old_offset = old_ptr.checked_sub(base).ok_or(core::alloc::AllocError)?; - let old_size = old_layout.size(); - let new_size = new_layout.size(); - let old_end = old_offset.checked_add(old_size).ok_or(core::alloc::AllocError)?; - let is_last = old_end == offset; - - if !is_last || new_size > old_size || !old_ptr.is_multiple_of(new_layout.align()) { - return Err(core::alloc::AllocError); - } - - let new_offset = old_offset.checked_add(new_size).ok_or(core::alloc::AllocError)?; - self.offset.set(new_offset); - let new_ptr = unsafe { NonNull::new_unchecked(self.base.as_ptr().add(old_offset)) }; - Ok(NonNull::slice_from_raw_parts(new_ptr, new_size)) - } -} - -#[cfg(test)] -mod tests { - #[cfg(feature = "nightly")] - use core::alloc::Allocator; - - use super::*; - - #[cfg(feature = "nightly")] - fn block_ptr(block: NonNull<[u8]>) -> NonNull { - // SAFETY: `Allocator::allocate` never returns a null block pointer. - unsafe { NonNull::new_unchecked(block.as_ptr().cast::()) } - } - - #[cfg(feature = "nightly")] - fn allocate_last_block_misaligned_to( - bump: &BumpArenaLazy, - align: usize, - ) -> (NonNull, Layout) { - let layout = Layout::from_size_align(8, 1).unwrap(); - let pad = Layout::from_size_align(1, 1).unwrap(); - - for _ in 0..=align { - let block = (&bump).allocate(layout).unwrap(); - let ptr = block_ptr(block); - if !(ptr.as_ptr() as usize).is_multiple_of(align) { - return (ptr, layout); - } - (&bump).allocate(pad).unwrap(); - } - - panic!("could not create a misaligned last allocation"); - } - - #[test] - fn alloc_alignment_and_length() { - let bump = BumpArenaLazy::new(128); - let base = bump.base.as_ptr() as usize; - let mut prev_end = 0usize; - - for align in [1, 2, 4, 8, 16] { - let layout = Layout::from_size_align(3, align).unwrap(); - let slice = bump.try_alloc_uninit(layout).unwrap(); - let ptr = slice.as_ptr() as usize; - assert_eq!(ptr % align, 0); - assert_eq!(slice.len(), 3); - - let start = ptr - base; - let end = start + slice.len(); - assert!(end > prev_end); - assert_eq!(bump.used(), end); - assert!(bump.used() <= bump.capacity()); - prev_end = end; - } - } - - #[test] - fn alloc_no_overlap() { - let bump = BumpArenaLazy::new(64); - let a = bump.try_alloc_uninit(Layout::from_size_align(16, 8).unwrap()).unwrap(); - let b = bump.try_alloc_uninit(Layout::from_size_align(8, 8).unwrap()).unwrap(); - - let a_start = a.as_ptr() as usize; - let a_end = a_start + a.len(); - let b_start = b.as_ptr() as usize; - let b_end = b_start + b.len(); - - assert!(a_end <= b_start || b_end <= a_start); - } - - #[test] - fn alloc_oom_does_not_advance() { - let bump = BumpArenaLazy::new(16); - let layout = Layout::from_size_align(8, 1).unwrap(); - bump.try_alloc_uninit(layout).unwrap(); - let used_before = bump.used(); - - let too_large = Layout::from_size_align(9, 1).unwrap(); - assert!(bump.try_alloc_uninit(too_large).is_none()); - assert_eq!(bump.used(), used_before); - assert!(bump.used() <= bump.capacity()); - } - - #[test] - fn reset_reuses_base() { - let bump = BumpArenaLazy::new(32); - let layout = Layout::from_size_align(8, 4).unwrap(); - let first = bump.try_alloc_uninit(layout).unwrap(); - let first_ptr = first.as_ptr() as usize; - - unsafe { bump.reset() }; - assert_eq!(bump.used(), 0); - - let second = bump.try_alloc_uninit(layout).unwrap(); - let second_ptr = second.as_ptr() as usize; - assert_eq!(first_ptr, second_ptr); - } - - #[test] - fn zero_capacity_rejects_nonzero_alloc_uninit() { - let bump = BumpArenaLazy::new(0); - let layout = Layout::from_size_align(1, 1).unwrap(); - assert!(bump.try_alloc_uninit(layout).is_none()); - assert_eq!(bump.used(), 0); - } - - #[test] - fn zero_size_alloc_does_not_advance() { - let bump = BumpArenaLazy::new(8); - let layout = Layout::from_size_align(0, 8).unwrap(); - let slice = bump.try_alloc_uninit(layout).unwrap(); - assert_eq!(slice.len(), 0); - assert_eq!(bump.used(), 0); - } - - #[cfg(feature = "nightly")] - #[test] - fn allocator_grow_and_shrink_resize_last_allocation() { - let bump = BumpArenaLazy::new(64); - let alloc = ≎ - let old = Layout::from_size_align(8, 4).unwrap(); - let grown = Layout::from_size_align(16, 4).unwrap(); - let shrunk = Layout::from_size_align(4, 4).unwrap(); - - let block = alloc.allocate(old).unwrap(); - let ptr = block_ptr(block); - let start = (ptr.as_ptr() as usize) - (bump.base.as_ptr() as usize); - unsafe { ptr.as_ptr().write_bytes(0xAB, old.size()) }; - - let grown_block = unsafe { alloc.grow(ptr, old, grown).unwrap() }; - let grown_ptr = block_ptr(grown_block); - assert_eq!(grown_ptr, ptr); - assert_eq!(bump.used(), start + grown.size()); - assert_eq!(unsafe { grown_ptr.as_ptr().read() }, 0xAB); - assert_eq!(unsafe { grown_ptr.as_ptr().add(old.size() - 1).read() }, 0xAB); - - let shrunk_block = unsafe { alloc.shrink(grown_ptr, grown, shrunk).unwrap() }; - assert_eq!(block_ptr(shrunk_block), ptr); - assert_eq!(bump.used(), start + shrunk.size()); - } - - #[cfg(feature = "nightly")] - #[test] - fn allocator_grow_zeroed_zeroes_new_tail() { - let bump = BumpArenaLazy::new(64); - let alloc = ≎ - let old = Layout::from_size_align(4, 1).unwrap(); - let grown = Layout::from_size_align(12, 1).unwrap(); - - let block = alloc.allocate(old).unwrap(); - let ptr = block_ptr(block); - unsafe { ptr.as_ptr().write_bytes(0xAB, old.size()) }; - - let grown_block = unsafe { alloc.grow_zeroed(ptr, old, grown).unwrap() }; - let grown_ptr = block_ptr(grown_block); - assert_eq!(grown_ptr, ptr); - for index in 0..old.size() { - assert_eq!(unsafe { grown_ptr.as_ptr().add(index).read() }, 0xAB); - } - for index in old.size()..grown.size() { - assert_eq!(unsafe { grown_ptr.as_ptr().add(index).read() }, 0); - } - } - - #[cfg(feature = "nightly")] - #[test] - fn allocator_grow_across_commit_preserves_the_block() { - let page = sys::page_size(); - let bump = BumpArenaLazy::new(page + 64); - let alloc = ≎ - let old = Layout::from_size_align(page - 8, 1).unwrap(); - let grown = Layout::from_size_align(page + 16, 1).unwrap(); - - let block = alloc.allocate(old).unwrap(); - let ptr = block_ptr(block); - unsafe { ptr.as_ptr().write_bytes(0xAB, old.size()) }; - assert_eq!(bump.used(), old.size()); - assert_eq!(bump.commit.get(), page); - - let grown_block = unsafe { alloc.grow(ptr, old, grown).unwrap() }; - let grown_ptr = block_ptr(grown_block); - - assert_eq!(grown_ptr, ptr); - assert_eq!(bump.used(), grown.size()); - assert!(bump.commit.get() >= grown.size()); - assert_eq!(unsafe { grown_ptr.as_ptr().read() }, 0xAB); - assert_eq!(unsafe { grown_ptr.as_ptr().add(old.size() - 1).read() }, 0xAB); - } - - #[cfg(feature = "nightly")] - #[test] - fn vec_try_reserve_can_grow_inside_allocator() { - let bump = BumpArenaLazy::new(64); - let mut values = Vec::with_capacity_in(1, &bump); - values.push(1); - - assert!(values.try_reserve(1).is_ok()); - values.push(2); - - assert_eq!(&values, &[1, 2]); - } - - #[cfg(feature = "nightly")] - #[test] - fn allocator_rejects_resize_when_pointer_does_not_fit_new_alignment() { - let bump = BumpArenaLazy::new(256); - let (ptr, old) = allocate_last_block_misaligned_to(&bump, 8); - let used = bump.used(); - let grown = Layout::from_size_align(16, 8).unwrap(); - - assert!(unsafe { bump.grow(ptr, old, grown) }.is_err()); - assert_eq!(bump.used(), used); - - let bump = BumpArenaLazy::new(256); - let (ptr, old) = allocate_last_block_misaligned_to(&bump, 8); - let used = bump.used(); - let shrunk = Layout::from_size_align(4, 8).unwrap(); - - assert!(unsafe { bump.shrink(ptr, old, shrunk) }.is_err()); - assert_eq!(bump.used(), used); - } -} diff --git a/src/lib.rs b/src/lib.rs index 189c726..f3051db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,38 +1,28 @@ #![warn(clippy::pedantic)] -#![doc = include_str!("../README.md")] #![cfg_attr(feature = "nightly", feature(allocator_api))] +#![doc = include_str!("../README.md")] -#[cfg(all(feature = "lazy", not(any(unix, windows))))] -compile_error!("the `lazy` feature is currently supported only on Unix and Windows targets"); +#[cfg(not(any(unix, windows)))] +compile_error!("`linalloc` only supports Unix and Windows targets"); mod bump_arena; -#[cfg(all(feature = "lazy", any(unix, windows)))] -mod bump_arena_lazy; -#[cfg(all(feature = "lazy", any(unix, windows)))] -pub(crate) mod sys; mod typed_arena; -#[cfg(all(feature = "lazy", any(unix, windows)))] -mod typed_arena_lazy; -mod typed_arena_ref; + +pub(crate) mod sys; pub use bump_arena::*; -#[cfg(all(feature = "lazy", any(unix, windows)))] -pub use bump_arena_lazy::*; pub use typed_arena::*; -#[cfg(all(feature = "lazy", any(unix, windows)))] -pub use typed_arena_lazy::*; -pub use typed_arena_ref::*; /// An untyped allocator that provides mutable slices of uninitialised memory. /// /// Types implementing this trait can serve as the backing store for -/// [`TypedArenaRef`], which adds automatic destructor execution and +/// [`TypedArena`], which adds automatic destructor execution and /// type‑safe allocation on top of the raw memory. /// /// # Safety /// /// Implementors must uphold the following invariants. Violating any of them -/// will cause **undefined behaviour** in safe code that uses [`TypedArenaRef`]. +/// will cause **undefined behaviour** in safe code that uses [`TypedArena`]. /// /// - **Alignment** -- every slice returned by [`UninitAllocator::try_alloc_uninit`] is aligned /// to at least the requested `layout.align()`. @@ -50,7 +40,7 @@ pub use typed_arena_ref::*; /// so can cause data races on the allocator’s internal state and lead to /// undefined behaviour. pub unsafe trait UninitAllocator { - /// Allocates a mutable slice of [`MaybeUninit`] that satisfies + /// Allocates a mutable slice of [`core::mem::MaybeUninit`] that satisfies /// `layout`. /// /// Returns `None` if the allocator cannot satisfy the request. @@ -60,21 +50,3 @@ pub unsafe trait UninitAllocator { layout: core::alloc::Layout, ) -> Option<&mut [core::mem::MaybeUninit]>; } - -#[cfg(all(test, feature = "lazy", any(unix, windows)))] -mod tests { - use std::io::Error; - - use super::{BumpArenaLazy, TypedArenaLazy}; - - #[test] - fn try_new_reports_os_error_for_unreservable_capacity() { - let err = BumpArenaLazy::try_new(usize::MAX).unwrap_err(); - assert_eq!(Some(err), Error::last_os_error().raw_os_error()); - assert_ne!(err, 0); - - let err = TypedArenaLazy::::try_new(usize::MAX).unwrap_err(); - assert_eq!(Some(err), Error::last_os_error().raw_os_error()); - assert_ne!(err, 0); - } -} diff --git a/src/typed_arena.rs b/src/typed_arena.rs index dbea83e..d8a2d50 100644 --- a/src/typed_arena.rs +++ b/src/typed_arena.rs @@ -1,343 +1,494 @@ -use core::cell::Cell; +#[cfg(feature = "nightly")] +use core::alloc::Allocator; +use core::alloc::Layout; +use core::cell::UnsafeCell; use core::marker::PhantomData; -use core::mem::MaybeUninit; -use core::ptr::{NonNull, drop_in_place}; +use core::mem::{self, needs_drop, size_of}; +use core::ptr::{self, drop_in_place}; -/// A fixed‑capacity, single‑threaded arena that allocates values of -/// type `T` and automatically drops them in reverse allocation order. +use crate::{BumpArena, UninitAllocator}; + +/// A typed arena that allocates values of type `T` from a borrowed backing allocator. +/// +/// Multiple `TypedArena`s can share the same underlying allocator, +/// allowing different types to be allocated in the same memory region +/// while being dropped independently. The backing allocator is specified by the +/// type parameter `A`, which defaults to [`BumpArena`] and must implement +/// [`crate::UninitAllocator`]. +/// +/// With the `nightly` feature enabled, the internal allocation-tracking list +/// also uses the backing allocator through the standard allocator API. +/// +/// Values allocated in this arena are automatically dropped in reverse +/// allocation order when the `TypedArena` is dropped or [`TypedArena::reset`] is called. +/// The memory in the backing allocator is **not** freed or rewound -- only the +/// objects’ destructors are executed. Reuse of the underlying memory is +/// governed by the allocator’s own life cycle (e.g., manually reset after all +/// `TypedArena`s have been dropped). /// -/// The backing store is a `NonNull<[MaybeUninit]>` whose capacity is -/// set at construction. Each call to [`TypedArena::try_alloc`] writes a value into -/// the next free slot and returns a mutable reference. When the -/// arena is dropped (or when [`TypedArena::reset`] is called), all live values -/// are dropped and the memory is made available for reuse. +/// # Thread safety /// -/// # Invariance and thread safety +/// `TypedArena` is **`!Send` and `!Sync`** because it contains a +/// raw pointer marker that prevents the value from leaving the thread where it was created. +/// This holds regardless of whether the backing allocator `A` is `Send` or `Sync`. /// -/// `TypedArena` is **invariant** in `T` and **`!Send + !Sync`**. -/// The marker field prevents unsound subtyping and cross‑thread usage. This -/// guarantees: +/// # Invariance /// -/// - No unsound subtyping (e.g., treating a `String` arena as a -/// `dyn Display` arena, which would break `Drop`). -/// - The arena is confined to a single thread. +/// `TypedArena` is **invariant** in `T`. The internal tracking list +/// contains `*mut T` pointers, which are invariant. This forbids +/// unsound subtyping (e.g., treating a `String` arena as a `dyn Display` arena), +/// which would otherwise break `Drop`. /// /// # Examples /// /// ``` -/// use linalloc::TypedArena; +/// use linalloc::{BumpArena, TypedArena}; /// -/// let mut arena = TypedArena::::new(5); +/// let bump = BumpArena::new(4 * 1024); /// -/// let s = arena.try_alloc("hello".to_string()).unwrap(); -/// assert_eq!(s, "hello"); +/// { +/// let mut strings = TypedArena::::new_in(&bump); +/// let mut ints = TypedArena::::new_in(&bump); /// -/// // All values are dropped when `arena` goes out of scope. +/// let s = strings.try_alloc("hello".to_string()).unwrap(); +/// let i = ints.try_alloc(42).unwrap(); +/// assert_eq!(*s, "hello"); +/// assert_eq!(*i, 42); +/// // strings and ints are dropped here, values are destroyed. +/// } +/// +/// // The bump memory is still allocated, but no live objects remain. +/// unsafe { bump.reset() }; // safe because all references have ended /// ``` +#[cfg(not(feature = "nightly"))] #[derive(Debug)] -pub struct TypedArena { - base: NonNull<[MaybeUninit]>, - offset: Cell, - #[allow(clippy::type_complexity)] - _invariant: PhantomData<(*const (), fn(T) -> T)>, +pub struct TypedArena<'a, T, A: UninitAllocator = BumpArena> { + allocator: &'a A, + // Tracks the addresses of every allocated `T` in the backing allocator. + allocations: UnsafeCell>, + // Makes the struct unconditionally `!Send + !Sync`. + _marker: PhantomData<*const ()>, } -impl TypedArena { - /// Creates a new typed arena that can hold up to `capacity` - /// elements of type `T`. - /// - /// The backing memory is allocated but **uninitialised**. - /// - /// # Panics - /// - /// If allocation fails, the global allocator error handler is - /// invoked (typically aborting the process). - #[must_use] - pub fn new(capacity: usize) -> Self { - Self { - // SAFETY: `Box` is guaranteed to be non-null. - base: unsafe { NonNull::new_unchecked(Box::into_raw(Box::new_uninit_slice(capacity))) }, - offset: Cell::new(0), - _invariant: PhantomData, +#[cfg(feature = "nightly")] +#[derive(Debug)] +pub struct TypedArena<'a, T, A = BumpArena> +where + A: UninitAllocator, + &'a A: Allocator, +{ + allocator: &'a A, + // Tracks the addresses of every allocated `T` in the backing allocator. + allocations: UnsafeCell>, + // Makes the struct unconditionally `!Send + !Sync`. + _marker: PhantomData<*const ()>, +} + +macro_rules! impl_typed_arena_methods { + () => { + /// Just like [`TypedArena::try_alloc`], but panics + /// when allocation fails. + /// + /// # Panics + /// + /// if the backing allocator cannot satisfy the allocation request. + pub fn alloc(&self, value: T) -> &mut T { + self.alloc_impl(value).expect("TypedArena allocation failed") } - } - /// Allocates a new `T` by moving `value` into the arena. - /// - /// The returned mutable reference borrows the arena immutably - /// (`&self`), so the arena is frozen (cannot be dropped or reset) - /// until the reference goes out of scope. - /// - /// # Returns - /// - /// `None` if the arena is full (i.e., `len() == capacity()`). - /// - /// # Examples - /// - /// ``` - /// use linalloc::TypedArena; - /// - /// let arena = TypedArena::::new(10); - /// let x = arena.try_alloc(42).unwrap(); - /// assert_eq!(*x, 42); - /// ``` - #[deprecated(since = "1.2.0", note = "Use `TypedArena::try_alloc` instead.")] - pub fn alloc_raw(&self, value: T) -> Option<&mut T> { - self.alloc_impl(value) - } + /// Allocates a new `T` by moving `value` into the arena. + /// + /// The returned mutable reference borrows the `TypedArena` immutably + /// (`&self`), so the arena is frozen (cannot be dropped or reset) until + /// the reference goes out of scope. Multiple allocations can coexist + /// without aliasing. + /// + /// Zero‑sized types (e.g., `()`) are handled specially: they consume no + /// space in the backing allocator and always succeed. + /// + /// # Returns + /// + /// `None` if the backing allocator cannot satisfy the allocation + /// request. + /// + /// # Examples + /// + /// ``` + /// use linalloc::{BumpArena, TypedArena}; + /// + /// let bump = BumpArena::new(1024); + /// let arena = TypedArena::::new_in(&bump); + /// let x = arena.try_alloc(42).unwrap(); + /// assert_eq!(*x, 42); + /// ``` + pub fn try_alloc(&self, value: T) -> Option<&mut T> { + self.alloc_impl(value) + } - /// Just like [`TypedArena::try_alloc`], but panics - /// when the arena capacity is full. - /// - /// # Panics - /// - /// If `TypedArena::len() == TypedArena::capacity()`. - pub fn alloc(&self, value: T) -> &mut T { - self.alloc_impl(value).expect("TypedArena capacity is full") - } + #[allow(clippy::mut_from_ref)] + fn alloc_impl(&self, value: T) -> Option<&mut T> { + if size_of::() == 0 { + unsafe { + let dangling = ptr::NonNull::::dangling(); + if needs_drop::() { + let allocs = &mut *self.allocations.get(); + // cannot allocate metadata? return none instead + // of panicking + allocs.try_reserve(1).ok()?; + allocs.push(dangling.as_ptr()); + } + dangling.as_ptr().write(value); + return Some(&mut *dangling.as_ptr()); + } + } - /// A carbon copy of [`TypedArena::alloc_raw`], but with a - /// more bespoke name and will likely be the stable API going forward. - pub fn try_alloc(&self, value: T) -> Option<&mut T> { - self.alloc_impl(value) - } + // cannot allocate metadata? return none instead + // of panicking + unsafe { + (*self.allocations.get()).try_reserve(1).ok()?; + } + + let layout = Layout::new::(); + let slice = self.allocator.try_alloc_uninit(layout)?; + let ptr = slice.as_mut_ptr().cast::(); - #[allow(clippy::mut_from_ref)] - fn alloc_impl(&self, value: T) -> Option<&mut T> { - if size_of::() == 0 { unsafe { - let dangling = NonNull::::dangling(); - dangling.as_ptr().write(value); - return Some(&mut *dangling.as_ptr()); + // Push the pointer into the tracking list. Because this method + // takes `&self`, we need interior mutability -- `UnsafeCell` gives + // us a unique access path that does not alias with any &mut borrow + // of the arena (which would require `&mut self`). + (*self.allocations.get()).push(ptr); + // Initialise the freshly allocated memory after tracking succeeds. + ptr.write(value); + // Return a mutable reference that borrows `self`, freezing the + // arena while the reference is alive. + Some(&mut *ptr) } } - let idx = self.offset.get(); - if idx >= self.capacity() { - return None; + /// Returns a reference to the backing allocator. + pub fn allocator(&self) -> &A { + self.allocator } - // Safety: - // - `idx` is within the capacity -- the slot is valid. - // - The slot is uninitialised -- `write` initialises it. - // - The returned reference borrows `self` -- lifetime tied to the arena. - unsafe { - let beg = self.base.as_ptr().cast::>(); - let slot = &mut *beg.add(idx); - let r = slot.write(value); - self.offset.set(idx + 1); - Some(r) + /// Returns the number of elements currently allocated in this arena. + pub fn len(&self) -> usize { + // Safety: we only read the length, which is a plain integer access + // that does not alias with any other operation. The `UnsafeCell` + // guarantees that this is a valid read. + unsafe { (*self.allocations.get()).len() } } - } - /// Returns the number of elements currently allocated in the arena. - /// - /// # Examples - /// - /// ``` - /// use linalloc::TypedArena; - /// - /// let mut arena = TypedArena::::new(10); - /// assert_eq!(arena.len(), 0); - /// arena.try_alloc(1); - /// assert_eq!(arena.len(), 1); - /// ``` - pub fn len(&self) -> usize { - self.offset.get() - } + /// Returns `true` if the arena contains no allocated elements. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } - /// Returns `true` if the arena contains no allocated elements. - /// - /// # Examples - /// - /// ``` - /// use linalloc::TypedArena; + /// Consumes the arena and returns an iterator that drains all allocated + /// values in **allocation order** (FIFO). + /// + /// This method does **not** allocate extra memory. The backing allocator + /// remains borrowed for the iterator’s lifetime, preventing it from being + /// dropped or reset until iteration is complete. + /// + /// # Examples + /// + /// ``` + /// use linalloc::{BumpArena, TypedArena}; + /// + /// let bump = BumpArena::new(128); + /// let mut arena = TypedArena::::new_in(&bump); + /// arena.try_alloc("first".to_string()).unwrap(); + /// arena.try_alloc("second".to_string()).unwrap(); + /// + /// let mut d = arena.drain(); + /// assert_eq!(d.next(), Some("first".to_string())); + /// assert_eq!(d.next(), Some("second".to_string())); + /// assert_eq!(d.next(), None); + /// ``` + pub fn drain(self) -> DrainIter<'a, T, A> { + // Disable the arena's own Drop. + let this = mem::ManuallyDrop::new(self); + + let allocations = unsafe { ptr::read(this.allocations.get()) }; + + DrainIter { pointers: allocations.into_iter(), _allocator: this.allocator } + } + + /// Drops all live `T` values in reverse allocation order and clears the + /// tracking list. + /// + /// Because this method takes `&mut self`, the borrow checker guarantees + /// that no references to the arena’s contents are currently alive. + /// After the call, [`TypedArena::len`] returns `0`. + /// + /// The memory in the backing allocator is **not** freed or rewound -- only + /// the destructors of the allocated values are executed. Future + /// allocations request fresh memory from the backing allocator. Reuse is + /// governed by that allocator’s own reset/drop lifecycle. + /// + /// # Examples + /// + /// ``` + /// use linalloc::{BumpArena, TypedArena}; + /// + /// let bump = BumpArena::new(1024); + /// let mut arena = TypedArena::>::new_in(&bump); + /// arena.try_alloc(vec![1, 2, 3]).unwrap(); + /// // No references are alive, so reset is safe. + /// arena.reset(); + /// assert!(arena.is_empty()); + /// ``` + pub fn reset(&mut self) { + let allocs = self.allocations.get_mut(); + // Drop in reverse order, mirroring Rust’s own drop semantics. + while let Some(ptr) = allocs.pop() { + unsafe { + drop_in_place(ptr); + } + } + } + }; +} + +#[cfg(not(feature = "nightly"))] +impl<'a, T, A: UninitAllocator> TypedArena<'a, T, A> { + /// Creates a new `TypedArena` that allocates objects inside the given + /// backing allocator. /// - /// let arena = TypedArena::::new(10); - /// assert!(arena.is_empty()); - /// arena.try_alloc(1); - /// assert!(!arena.is_empty()); - /// ``` - pub fn is_empty(&self) -> bool { - self.len() == 0 + /// The allocator must outlive the `TypedArena` and all references + /// returned by [`TypedArena::try_alloc`]. + pub fn new_in(allocator: &'a A) -> Self { + Self { allocator, allocations: UnsafeCell::new(Vec::new()), _marker: PhantomData } } - /// Returns the maximum number of elements the arena can hold. - pub fn capacity(&self) -> usize { - self.base.len() - } + impl_typed_arena_methods!(); +} - /// Drops all live `T` values in reverse allocation order and - /// resets the arena for reuse. - /// - /// Because this method takes `&mut self`, the borrow checker - /// guarantees that no references to the arena's contents are - /// currently alive. After the call, `len()` returns `0`. +#[cfg(feature = "nightly")] +impl<'a, T, A> TypedArena<'a, T, A> +where + A: UninitAllocator, + &'a A: Allocator, +{ + /// Creates a new `TypedArena` that allocates objects inside the given + /// backing allocator. /// - /// # Examples - /// - /// ``` - /// use linalloc::TypedArena; - /// - /// let mut arena = TypedArena::>::new(5); - /// { - /// let v = arena.try_alloc(vec![1, 2, 3]).unwrap(); - /// } // v goes out of scope -- can call `reset` now. - /// arena.reset(); - /// assert_eq!(arena.len(), 0); - /// ``` - pub fn reset(&mut self) { - let offset = self.offset.replace(0); - unsafe { - let start = self.base.as_ptr().cast::(); - // Drop in reverse order per Rust's usual drop semantics. - for i in (0..offset).rev() { - drop_in_place(start.add(i)); - } + /// The allocator must outlive the `TypedArena` and all references + /// returned by [`TypedArena::try_alloc`]. + pub fn new_in(allocator: &'a A) -> Self { + Self { + allocator, + allocations: UnsafeCell::new(Vec::new_in(allocator)), + _marker: PhantomData, } } + + impl_typed_arena_methods!(); } -impl Drop for TypedArena { +#[cfg(not(feature = "nightly"))] +impl Drop for TypedArena<'_, T, A> { fn drop(&mut self) { - let offset = self.offset.get(); - unsafe { - let start = self.base.as_ptr().cast::(); - for i in (0..offset).rev() { - drop_in_place(start.add(i)); - } - drop(Box::from_raw(self.base.as_ptr())); - } + self.reset(); } } -#[cfg(test)] -mod tests { - use core::ptr; - - use super::*; - - // A helper that records drop order and count. - struct DropTracker<'a> { - id: u32, - order: &'a Cell>, +#[cfg(feature = "nightly")] +impl<'a, T, A> Drop for TypedArena<'a, T, A> +where + A: UninitAllocator, + &'a A: Allocator, +{ + fn drop(&mut self) { + self.reset(); } +} - impl Drop for DropTracker<'_> { - fn drop(&mut self) { - let mut v = self.order.take(); - v.push(self.id); - self.order.set(v); - } - } +/// Yields `T` in **allocation order** (FIFO). +/// Created by [`TypedArena::drain`]. +/// +/// This iterator does **not** allocate extra memory -- it reuses the arena’s +/// internal tracking list. The backing allocator remains borrowed for the +/// iterator’s lifetime, preventing premature deallocation. If dropped before +/// fully consumed, remaining elements are destroyed in **reverse allocation +/// order** (LIFO), mirroring Rust own's drop semantics. +#[cfg(not(feature = "nightly"))] +pub struct DrainIter<'a, T, A: UninitAllocator = BumpArena> { + // The remaining raw pointers, taken from the arena's tracking list. + pointers: std::vec::IntoIter<*mut T>, + // Keeps the backing allocator alive. + _allocator: &'a A, +} - #[test] - fn drop_order_reverse_allocation() { - let order = Cell::new(Vec::new()); - let arena = TypedArena::::new(10); +#[cfg(feature = "nightly")] +pub struct DrainIter<'a, T, A = BumpArena> +where + A: UninitAllocator, + &'a A: Allocator, +{ + // The remaining raw pointers, taken from the arena's tracking list. + pointers: std::vec::IntoIter<*mut T, &'a A>, + // Keeps the backing allocator alive. + _allocator: &'a A, +} - arena.try_alloc(DropTracker { id: 1, order: &order }).unwrap(); - arena.try_alloc(DropTracker { id: 2, order: &order }).unwrap(); - arena.try_alloc(DropTracker { id: 3, order: &order }).unwrap(); +macro_rules! impl_drain_iter { + ($($bounds:tt)*) => { + impl<'a, T, A> Iterator for DrainIter<'a, T, A> + where + A: UninitAllocator, + $($bounds)* + { + type Item = T; + + fn next(&mut self) -> Option { + let ptr = self.pointers.next()?; + // SAFETY: + // > `ptr` is non‑null and properly aligned for `T` (guaranteed by + // the allocator and the arena's tracking). + // > The memory pointed to is still live because `_allocator` keeps + // the allocator borrowed. + // > No other reference to this memory exists -- the arena is consumed. + Some(unsafe { ptr::read(ptr) }) + } - drop(arena); + fn size_hint(&self) -> (usize, Option) { + self.pointers.size_hint() + } + } - assert_eq!(order.take(), vec![3, 2, 1]); - } + impl<'a, T, A> ExactSizeIterator for DrainIter<'a, T, A> + where + A: UninitAllocator, + $($bounds)* + { + fn len(&self) -> usize { + self.pointers.len() + } + } - #[test] - fn reset_drops_values_and_reuses_memory() { - let order = Cell::new(Vec::new()); + impl<'a, T, A> core::iter::FusedIterator for DrainIter<'a, T, A> + where + A: UninitAllocator, + $($bounds)* + { + } - let mut arena = TypedArena::::new(10); - let ptr1 = ptr::from_mut(arena.try_alloc(DropTracker { id: 1, order: &order }).unwrap()); - let _ptr2 = ptr::from_mut(arena.try_alloc(DropTracker { id: 2, order: &order }).unwrap()); + impl<'a, T, A> Drop for DrainIter<'a, T, A> + where + A: UninitAllocator, + $($bounds)* + { + fn drop(&mut self) { + let remaining = self.pointers.as_slice(); + // Same order as `TypedArena::reset`. + for &ptr in remaining.iter().rev() { + // SAFETY: ptr is valid, unique, and the allocator is still alive. + unsafe { + drop_in_place(ptr); + } + } + } + } + }; +} - arena.reset(); +#[cfg(not(feature = "nightly"))] +impl_drain_iter!(); - // After reset, all previous values must have been dropped. - assert_eq!(order.take(), vec![2, 1]); +#[cfg(feature = "nightly")] +impl_drain_iter!(&'a A: Allocator); - // New allocation reuses the first slot. - let ptr3 = ptr::from_mut(arena.try_alloc(DropTracker { id: 3, order: &order }).unwrap()); - assert_eq!(ptr1, ptr3); +#[cfg(test)] +mod tests { + use core::sync::atomic::{AtomicUsize, Ordering}; - drop(arena); - assert_eq!(order.take(), vec![3]); - } + use super::*; + use crate::BumpArena; #[test] - fn no_double_drop_after_reset() { - struct Counter<'a>(&'a Cell); - impl Drop for Counter<'_> { + fn zst_with_drop_is_dropped_when_arena_drops() { + static DROPS: AtomicUsize = AtomicUsize::new(0); + + struct Zst; + + impl Drop for Zst { fn drop(&mut self) { - self.0.set(self.0.get() + 1); + DROPS.fetch_add(1, Ordering::Relaxed); } } - let count = Cell::new(0u32); - - let mut arena = TypedArena::::new(10); - arena.try_alloc(Counter(&count)).unwrap(); - arena.try_alloc(Counter(&count)).unwrap(); - - arena.reset(); - assert_eq!(count.get(), 2); // both dropped exactly once + DROPS.store(0, Ordering::Relaxed); + let bump = BumpArena::new(128); + { + let arena = TypedArena::::new_in(&bump); + assert!(arena.try_alloc(Zst).is_some()); + assert!(arena.try_alloc(Zst).is_some()); + assert_eq!(arena.len(), 2); + } - arena.try_alloc(Counter(&count)).unwrap(); - drop(arena); - assert_eq!(count.get(), 3); // only the new one dropped + assert_eq!(DROPS.load(Ordering::Relaxed), 2); } #[test] - fn reset_clears_len_before_dropping_values() { - struct PanicOnDrop<'a>(&'a Cell); - impl Drop for PanicOnDrop<'_> { + fn reset_removes_pointer_before_dropping_value() { + static DROPS: AtomicUsize = AtomicUsize::new(0); + + struct PanicOnFirstDrop(u8); + + impl Drop for PanicOnFirstDrop { fn drop(&mut self) { - self.0.set(self.0.get() + 1); - panic!("drop panic"); + let _ = self.0; + assert!(DROPS.fetch_add(1, Ordering::Relaxed) != 0, "drop panic"); } } - let drops = Cell::new(0u32); - let mut arena = core::mem::ManuallyDrop::new(TypedArena::::new(2)); - arena.try_alloc(PanicOnDrop(&drops)).unwrap(); - arena.try_alloc(PanicOnDrop(&drops)).unwrap(); + DROPS.store(0, Ordering::Relaxed); + let bump = BumpArena::new(128); + let mut arena = TypedArena::::new_in(&bump); + arena.try_alloc(PanicOnFirstDrop(1)).unwrap(); + arena.try_alloc(PanicOnFirstDrop(2)).unwrap(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| arena.reset())); assert!(result.is_err()); - assert_eq!(drops.get(), 1); - assert_eq!(arena.len(), 0); - unsafe { - core::mem::ManuallyDrop::drop(&mut arena); - } + assert_eq!(DROPS.load(Ordering::Relaxed), 1); + assert_eq!(arena.len(), 1); + + drop(arena); + assert_eq!(DROPS.load(Ordering::Relaxed), 2); } #[test] - fn oom_does_not_advance_offset() { - let arena = TypedArena::::new(1); // holds exactly 1 u64 - assert!(arena.try_alloc(1u64).is_some()); - assert_eq!(arena.len(), 1); - assert!(arena.try_alloc(2u64).is_none()); - assert_eq!(arena.len(), 1); + fn reset_does_not_rewind_the_backing_allocator() { + let bump = BumpArena::new(128); + let mut arena = TypedArena::::new_in(&bump); + + assert!(arena.try_alloc(1).is_some()); + let used = bump.used(); + arena.reset(); + + assert_eq!(bump.used(), used); + assert_eq!(arena.len(), 0); } #[test] - fn zst_does_not_advance_offset() { - let arena = TypedArena::<()>::new(0); - assert!(arena.try_alloc(()).is_some()); - assert_eq!(arena.len(), 0); - assert!(arena.try_alloc(()).is_some()); - assert_eq!(arena.len(), 0); + fn typed_arena_defaults_to_bump_arena_backing() { + let bump = BumpArena::new(128); + let arena = TypedArena::::new_in(&bump); + + assert_eq!(*arena.try_alloc(42).unwrap(), 42); } + #[cfg(feature = "nightly")] #[test] - fn allocated_value_is_valid() { - let arena = TypedArena::::new(1); - let s = arena.try_alloc("hello".to_string()).unwrap(); - assert_eq!(s, "hello"); - s.push_str(" world"); - assert_eq!(s, "hello world"); + fn default_bump_arena_tracking_uses_the_bump_allocator() { + let bump = BumpArena::new(128); + let arena = TypedArena::::new_in(&bump); + + arena.try_alloc(42).unwrap(); + + assert!(bump.used() > size_of::()); } } diff --git a/src/typed_arena_lazy.rs b/src/typed_arena_lazy.rs deleted file mode 100644 index d45d618..0000000 --- a/src/typed_arena_lazy.rs +++ /dev/null @@ -1,465 +0,0 @@ -use core::cell::Cell; -use core::marker::PhantomData; -use core::mem::{MaybeUninit, size_of}; -use core::ptr::{NonNull, drop_in_place}; - -use crate::sys; - -/// A fixed‑capacity, single‑threaded arena that allocates values of type `T` -/// and automatically drops them in reverse allocation order. -/// -/// The backing store is a reserved virtual‑memory region whose total capacity -/// is set at construction in *elements*. Physical memory is committed on -/// demand as elements are allocated, so the arena can be created with a very -/// large capacity without immediately consuming physical memory. -/// -/// # Invariance and thread safety -/// -/// `TypedArenaLazy` is **invariant** in `T` and **`!Send + !Sync`**. The -/// marker field prevents unsound subtyping and cross‑thread usage. This -/// guarantees: -/// -/// - No unsound subtyping (e.g. treating a `String` arena as a `dyn Display` -/// arena, which would break `Drop`). -/// - The arena is confined to a single thread. -/// -/// # Examples -/// -/// ``` -/// use linalloc::TypedArenaLazy; -/// -/// let mut arena = TypedArenaLazy::::new(5); -/// -/// let s = arena.try_alloc("hello".to_string()).unwrap(); -/// assert_eq!(s, "hello"); -/// -/// // All values are dropped when `arena` goes out of scope. -/// ``` -#[derive(Debug)] -pub struct TypedArenaLazy { - base: NonNull>, - capacity: usize, - // pays the cost of syscall upfront. - page_size: usize, - offset: Cell, - commit: Cell, - last_os_error: Cell, - #[allow(clippy::type_complexity)] - _invariant: PhantomData<(*const (), fn(T) -> T)>, -} - -impl TypedArenaLazy { - /// Creates a new typed arena that can hold up to `capacity` elements of - /// type `T`. - /// - /// The backing memory is **reserved** but not committed -- physical pages - /// are allocated only as elements are added. If `capacity` is zero, or - /// `T` is a zero‑sized type (e.g. `()`), the arena is empty and will - /// reject all non‑zero‑sized allocations. - /// - /// # Panics - /// - /// Panics if the operating system cannot reserve the requested address - /// range, or if the required byte size overflows. - #[must_use] - pub fn new(capacity: usize) -> Self { - Self::try_new(capacity).expect("TypedArenaLazy::new failed to reserve memory") - } - - /// Like [`new`], but returns a `Result`. - /// - /// # Panics - /// - /// Panics if the required byte size overflows. - /// - /// # Errors - /// - /// Returns OS error code if reservation fails. - /// - /// [`new`]: TypedArenaLazy::new - pub fn try_new(capacity: usize) -> Result { - let page_size = sys::page_size(); - - if capacity == 0 || size_of::() == 0 { - return Ok(Self { - base: NonNull::dangling(), - capacity, - page_size, - offset: Cell::new(0), - commit: Cell::new(0), - last_os_error: Cell::new(0), - _invariant: PhantomData, - }); - } - - let size_bytes = - capacity.checked_mul(size_of::()).expect("TypedArenaLazy: capacity overflow"); - let base = sys::reserve(size_bytes)?; - - Ok(Self { - base: base.cast(), - capacity, - page_size, - offset: Cell::new(0), - commit: Cell::new(0), - last_os_error: Cell::new(0), - _invariant: PhantomData, - }) - } - - /// Just like [`TypedArenaLazy::try_alloc`], but panics if the allocation - /// fails. - /// - /// # Panics - /// - /// Panics if the arena is full or a required memory commit fails. - pub fn alloc(&self, value: T) -> &mut T { - self.alloc_impl(value).expect("TypedArenaLazy allocation failed") - } - - /// Allocates a new `T` by moving `value` into the arena. - /// - /// The returned mutable reference borrows the arena immutably (`&self`), - /// so the arena is frozen (cannot be dropped or reset) until the reference - /// goes out of scope. Multiple allocations can coexist without aliasing. - /// - /// Zero‑sized types (e.g. `()`) are handled specially: they consume no - /// capacity and always succeed. - /// - /// # Returns - /// - /// `None` if the arena is full (i.e. [`len`](Self::len) == [`capacity`](Self::capacity)). - /// - /// # Examples - /// - /// ``` - /// use linalloc::TypedArenaLazy; - /// - /// let arena = TypedArenaLazy::::new(10); - /// let x = arena.try_alloc(42).unwrap(); - /// assert_eq!(*x, 42); - /// ``` - pub fn try_alloc(&self, value: T) -> Option<&mut T> { - self.alloc_impl(value) - } - - /// Allocates a new `T` by moving `value` into the arena. - #[deprecated(since = "1.2.0", note = "Use `TypedArenaLazy::try_alloc` instead.")] - pub fn alloc_raw(&self, value: T) -> Option<&mut T> { - self.alloc_impl(value) - } - - #[allow(clippy::mut_from_ref)] - fn alloc_impl(&self, value: T) -> Option<&mut T> { - // ZST's never consume capacity. - if size_of::() == 0 { - unsafe { - let dangling = NonNull::::dangling(); - dangling.as_ptr().write(value); - return Some(&mut *dangling.as_ptr()); - } - } - - let idx = self.offset.get(); - if idx >= self.capacity { - return None; - } - - // `new` guarantees `capacity * size_of::()` fits, and `idx < capacity`. - let required_bytes = (idx + 1) * size_of::(); - - // Ensure enough memory is committed. - if required_bytes > self.commit.get() { - return self.alloc_bump(idx, required_bytes, value); - } - - // Initialise the slot. - unsafe { - let slot = self.base.as_ptr().add(idx); - let r = (&mut *slot).write(value); - self.offset.set(idx + 1); - Some(r) - } - } - - // With the code in `alloc_bump()` out of the way, `alloc_impl()` compiles down to some super tight assembly. - #[cold] - #[inline(never)] - #[allow(clippy::mut_from_ref)] - fn alloc_bump(&self, idx: usize, required_bytes: usize, value: T) -> Option<&mut T> { - let current = self.commit.get(); - - // `new` guarantees `capacity * size_of::()` fits. - let total_bytes = self.capacity * size_of::(); - - // Next page rounding - let needed = required_bytes.checked_next_multiple_of(self.page_size)?.min(total_bytes); - - // Safety: - // > `current` is page‑aligned and within the reservation. - // > `needed - current` is a multiple of the page size. - unsafe { - let addr = NonNull::new_unchecked(self.base.as_ptr().cast::().add(current)); - if let Err(code) = sys::commit(addr, needed - current) { - // capture the OS error code immediately - self.last_os_error.set(code); - return None; - } - } - - self.commit.set(needed); - - // Initialise the slot. - unsafe { - let slot = self.base.as_ptr().add(idx); - let r = (&mut *slot).write(value); - self.offset.set(idx + 1); - Some(r) - } - } - - /// Returns the OS error code from the last failed allocation or commit - /// operation, if any. - /// - /// The returned value is the raw platform‑specific error code: - /// - On Unix: the `errno` value (positive integer). - /// - On Windows: the `GetLastError` code. - /// - /// Returns `None` if no OS-backed reserve or commit failure has been - /// recorded for this arena. - /// - /// # Semantics - /// - /// This method behaves analogously to [`std::io::Error::last_os_error`] at - /// the point of the failed internal system call. The error code is stable - /// until the next failure overwrites it. - /// - /// [`std::io::Error::last_os_error`]: std::io::Error::last_os_error - pub fn last_os_error_code(&self) -> Option { - let code = self.last_os_error.get(); - if code == 0 { None } else { Some(code) } - } - - /// Returns the number of elements currently allocated in the arena. - /// - /// # Examples - /// - /// ``` - /// use linalloc::TypedArenaLazy; - /// - /// let mut arena = TypedArenaLazy::::new(10); - /// assert_eq!(arena.len(), 0); - /// arena.try_alloc(1); - /// assert_eq!(arena.len(), 1); - /// ``` - pub fn len(&self) -> usize { - self.offset.get() - } - - /// Returns `true` if the arena contains no allocated elements. - /// - /// # Examples - /// - /// ``` - /// use linalloc::TypedArenaLazy; - /// - /// let arena = TypedArenaLazy::::new(10); - /// assert!(arena.is_empty()); - /// arena.try_alloc(1); - /// assert!(!arena.is_empty()); - /// ``` - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns the maximum number of elements the arena can hold. - pub fn capacity(&self) -> usize { - self.capacity - } - - /// Drops all live `T` values in reverse allocation order and resets the - /// arena for reuse. - /// - /// Because this method takes `&mut self`, the borrow checker guarantees - /// that no references to the arena's contents are currently alive. After - /// the call, [`len`](Self::len) returns `0`. - /// - /// Committed memory is not decommitted -- subsequent allocations will reuse - /// the already‑committed pages. - /// - /// # Examples - /// - /// ``` - /// use linalloc::TypedArenaLazy; - /// - /// let mut arena = TypedArenaLazy::>::new(5); - /// { - /// let v = arena.try_alloc(vec![1, 2, 3]).unwrap(); - /// } // v goes out of scope -- can call `reset` now. - /// arena.reset(); - /// assert_eq!(arena.len(), 0); - /// ``` - pub fn reset(&mut self) { - let offset = self.offset.replace(0); - unsafe { - let start = self.base.as_ptr().cast::(); - // Drop in reverse order per Rust's usual drop semantics. - for i in (0..offset).rev() { - drop_in_place(start.add(i)); - } - } - } -} - -impl Drop for TypedArenaLazy { - fn drop(&mut self) { - let offset = self.offset.get(); - unsafe { - let start = self.base.as_ptr().cast::(); - for i in (0..offset).rev() { - drop_in_place(start.add(i)); - } - if self.capacity > 0 && size_of::() > 0 { - let total_bytes = self.capacity * size_of::(); - sys::release(self.base.cast(), total_bytes); - } - } - } -} - -#[cfg(test)] -mod tests { - use core::{mem, ptr}; - - use super::*; - - // A helper that records drop order and count. - struct DropTracker<'a> { - id: u32, - order: &'a Cell>, - } - - impl Drop for DropTracker<'_> { - fn drop(&mut self) { - let mut v = self.order.take(); - v.push(self.id); - self.order.set(v); - } - } - - #[test] - fn drop_order_reverse_allocation() { - let order = Cell::new(Vec::new()); - let arena = TypedArenaLazy::::new(10); - - arena.try_alloc(DropTracker { id: 1, order: &order }).unwrap(); - arena.try_alloc(DropTracker { id: 2, order: &order }).unwrap(); - arena.try_alloc(DropTracker { id: 3, order: &order }).unwrap(); - - drop(arena); - - assert_eq!(order.take(), vec![3, 2, 1]); - } - - #[test] - fn reset_drops_values_and_reuses_memory() { - let order = Cell::new(Vec::new()); - - let mut arena = TypedArenaLazy::::new(10); - let ptr1 = ptr::from_mut(arena.try_alloc(DropTracker { id: 1, order: &order }).unwrap()); - let _ptr2 = ptr::from_mut(arena.try_alloc(DropTracker { id: 2, order: &order }).unwrap()); - - arena.reset(); - - // After reset, all previous values must have been dropped. - assert_eq!(order.take(), vec![2, 1]); - - // New allocation reuses the first slot. - let ptr3 = ptr::from_mut(arena.try_alloc(DropTracker { id: 3, order: &order }).unwrap()); - assert_eq!(ptr1, ptr3); - - drop(arena); - assert_eq!(order.take(), vec![3]); - } - - #[test] - fn no_double_drop_after_reset() { - struct Counter<'a>(&'a Cell); - impl Drop for Counter<'_> { - fn drop(&mut self) { - self.0.set(self.0.get() + 1); - } - } - - let count = Cell::new(0u32); - - let mut arena = TypedArenaLazy::::new(10); - arena.try_alloc(Counter(&count)).unwrap(); - arena.try_alloc(Counter(&count)).unwrap(); - - arena.reset(); - assert_eq!(count.get(), 2); // both dropped exactly once - - arena.try_alloc(Counter(&count)).unwrap(); - drop(arena); - assert_eq!(count.get(), 3); // only the new one dropped - } - - #[test] - fn reset_clears_len_before_dropping_values() { - struct PanicOnDrop<'a>(&'a Cell); - impl Drop for PanicOnDrop<'_> { - fn drop(&mut self) { - self.0.set(self.0.get() + 1); - panic!("drop panic"); - } - } - - let drops = Cell::new(0u32); - let mut arena = mem::ManuallyDrop::new(TypedArenaLazy::::new(2)); - arena.try_alloc(PanicOnDrop(&drops)).unwrap(); - arena.try_alloc(PanicOnDrop(&drops)).unwrap(); - - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| arena.reset())); - - assert!(result.is_err()); - assert_eq!(drops.get(), 1); - assert_eq!(arena.len(), 0); - unsafe { - mem::ManuallyDrop::drop(&mut arena); - } - } - - #[test] - fn oom_does_not_advance_offset() { - let arena = TypedArenaLazy::::new(1); // holds exactly 1 u64 - assert!(arena.try_alloc(1u64).is_some()); - assert_eq!(arena.len(), 1); - assert!(arena.try_alloc(2u64).is_none()); - assert_eq!(arena.len(), 1); - } - - #[test] - fn zst_does_not_advance_offset() { - let arena = TypedArenaLazy::<()>::new(0); - assert!(arena.try_alloc(()).is_some()); - assert_eq!(arena.len(), 0); - assert!(arena.try_alloc(()).is_some()); - assert_eq!(arena.len(), 0); - } - - #[test] - fn allocated_value_is_valid() { - let arena = TypedArenaLazy::::new(1); - let s = arena.try_alloc("hello".to_string()).unwrap(); - assert_eq!(s, "hello"); - s.push_str(" world"); - assert_eq!(s, "hello world"); - } - - #[test] - #[should_panic(expected = "TypedArenaLazy: capacity overflow")] - fn new_panics_on_capacity_overflow() { - let _arena = TypedArenaLazy::::new(usize::MAX); - } -} diff --git a/src/typed_arena_ref.rs b/src/typed_arena_ref.rs deleted file mode 100644 index 654cf11..0000000 --- a/src/typed_arena_ref.rs +++ /dev/null @@ -1,512 +0,0 @@ -use core::alloc::Layout; -use core::cell::UnsafeCell; -use core::marker::PhantomData; -use core::mem::{self, size_of}; -use core::ptr::{self, drop_in_place}; -use std::vec; - -use crate::UninitAllocator; - -/// A typed arena that allocates values of type `T` from a borrowed backing allocator. -/// -/// Multiple `TypedArenaRef`s can share the same underlying allocator, -/// allowing different types to be allocated in the same memory region -/// while being dropped independently. The backing allocator is specified by the -/// type parameter `A`, which must implement [`crate::UninitAllocator`]. -/// -/// Values allocated in this arena are automatically dropped in reverse -/// allocation order when the `TypedArenaRef` is dropped or [`TypedArenaRef::reset`] is called. -/// The memory in the backing allocator is **not** freed -- only the objects’ -/// destructors are executed. Reuse of the underlying memory is governed by the -/// allocator’s own life cycle (e.g., manually reset after all -/// `TypedArenaRef`s have been dropped). -/// -/// # Thread safety -/// -/// `TypedArenaRef` is **`!Send` and `!Sync`** because it contains a -/// raw pointer marker that prevents the value from leaving the thread where it was created. -/// This holds regardless of whether the backing allocator `A` is `Send` or `Sync`. -/// -/// # Invariance -/// -/// `TypedArenaRef` is **invariant** in `T`. The internal tracking list -/// contains `*mut T` pointers, which are invariant. This forbids -/// unsound subtyping (e.g., treating a `String` arena as a `dyn Display` arena), -/// which would otherwise break `Drop`. -/// -/// # Examples -/// -/// ``` -/// use linalloc::{BumpArena, TypedArenaRef}; -/// -/// let bump = BumpArena::new(4 * 1024); -/// -/// { -/// let mut strings = TypedArenaRef::::new_in(&bump); -/// let mut ints = TypedArenaRef::::new_in(&bump); -/// -/// let s = strings.try_alloc("hello".to_string()).unwrap(); -/// let i = ints.try_alloc(42).unwrap(); -/// assert_eq!(*s, "hello"); -/// assert_eq!(*i, 42); -/// // strings and ints are dropped here, values are destroyed. -/// } -/// -/// // The bump memory is still allocated, but no live objects remain. -/// unsafe { bump.reset() }; // safe because all references have ended -/// ``` -#[derive(Debug)] -pub struct TypedArenaRef<'a, T, A: UninitAllocator + 'a> { - allocator: &'a A, - // Tracks the addresses of every allocated `T`. Interior mutability via - // `UnsafeCell` allows pushing from `&self` during `alloc`. The list is - // only read or cleared when we have `&mut self` (in `Drop` and `reset`). - allocations: UnsafeCell>, - // Makes the struct unconditionally `!Send + !Sync`. - _marker: PhantomData<*const ()>, -} - -impl<'a, T, A: UninitAllocator + 'a> TypedArenaRef<'a, T, A> { - /// Creates a new `TypedArenaRef` that allocates objects inside the given - /// backing allocator. - /// - /// The allocator must outlive the `TypedArenaRef` and all references - /// returned by [`TypedArenaRef::try_alloc`]. - pub fn new_in(allocator: &'a A) -> Self { - Self { allocator, allocations: UnsafeCell::new(Vec::new()), _marker: PhantomData } - } - - /// Just like [`TypedArenaRef::try_alloc`], but panics - /// when allocation fails. - /// - /// # Panics - /// - /// if the backing allocator cannot satisfy the allocation request. - pub fn alloc(&self, value: T) -> &mut T { - self.alloc_impl(value).expect("TypedArenaRef allocation failed") - } - - /// Allocates a new `T` by moving `value` into the arena. - /// - /// The returned mutable reference borrows the `TypedArenaRef` immutably - /// (`&self`), so the arena is frozen (cannot be dropped or reset) until - /// the reference goes out of scope. Multiple allocations can coexist - /// without aliasing. - /// - /// Zero‑sized types (e.g., `()`) are handled specially: they consume no - /// space in the backing allocator and always succeed. - /// - /// # Returns - /// - /// `None` if the backing allocator cannot satisfy the allocation - /// request. - /// - /// # Examples - /// - /// ``` - /// use linalloc::{BumpArena, TypedArenaRef}; - /// - /// let bump = BumpArena::new(1024); - /// let arena = TypedArenaRef::::new_in(&bump); - /// let x = arena.try_alloc(42).unwrap(); - /// assert_eq!(*x, 42); - /// ``` - pub fn try_alloc(&self, value: T) -> Option<&mut T> { - self.alloc_impl(value) - } - - #[allow(clippy::mut_from_ref)] - fn alloc_impl(&self, value: T) -> Option<&mut T> { - // Zero‑sized types never consume memory -- they just write to a - // dangling pointer. No tracking is necessary because ZSTs have no - // destructors and no drop glue. - if size_of::() == 0 { - unsafe { - let dangling = ptr::NonNull::::dangling(); - dangling.as_ptr().write(value); - return Some(&mut *dangling.as_ptr()); - } - } - - let layout = Layout::new::(); - let slice = self.allocator.try_alloc_uninit(layout)?; - let ptr = slice.as_mut_ptr().cast::(); - - unsafe { - // Initialise the freshly allocated memory. - ptr.write(value); - // Push the pointer into the tracking list. Because this method - // takes `&self`, we need interior mutability -- `UnsafeCell` gives - // us a unique access path that does not alias with any &mut borrow - // of the arena (which would require `&mut self`). - (*self.allocations.get()).push(ptr); - // Return a mutable reference that borrows `self`, freezing the - // arena while the reference is alive. - Some(&mut *ptr) - } - } - - /// Returns a reference to the backing allocator. - pub fn allocator(&self) -> &A { - self.allocator - } - - /// Returns the number of elements currently allocated in this arena. - pub fn len(&self) -> usize { - // Safety: we only read the length, which is a plain integer access - // that does not alias with any other operation. The `UnsafeCell` - // guarantees that this is a valid read. - unsafe { (*self.allocations.get()).len() } - } - - /// Returns `true` if the arena contains no allocated elements. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Drops all live `T` values in reverse allocation order and clears the - /// tracking list. - /// - /// Because this method takes `&mut self`, the borrow checker guarantees - /// that no references to the arena’s contents are currently alive. - /// After the call, [`TypedArenaRef::len`] returns `0`. - /// - /// The memory in the backing allocator is **not** freed -- only the - /// destructors of the allocated values are executed. The memory can be - /// reused by further allocations through this `TypedArenaRef` (or other - /// borrows of the same allocator), but it will not be physically released until - /// the allocator is dropped or reset. - /// - /// # Examples - /// - /// ``` - /// use linalloc::{BumpArena, TypedArenaRef}; - /// - /// let bump = BumpArena::new(1024); - /// let mut arena = TypedArenaRef::, _>::new_in(&bump); - /// arena.try_alloc(vec![1, 2, 3]).unwrap(); - /// // No references are alive, so reset is safe. - /// arena.reset(); - /// assert!(arena.is_empty()); - /// ``` - pub fn reset(&mut self) { - let allocs = self.allocations.get_mut(); - // Drop in reverse order, mirroring Rust’s own drop semantics. - for &ptr in allocs.iter().rev() { - unsafe { - drop_in_place(ptr); - } - } - allocs.clear(); - } -} - -impl<'a, T, A: UninitAllocator + 'a> Drop for TypedArenaRef<'a, T, A> { - fn drop(&mut self) { - self.reset(); - } -} - -/// An iterator that yields the elements of a consumed [`TypedArenaRef`]. -/// -/// This struct is created by the [`TypedArenaRef::into_iter`] -/// (provided by the [`IntoIterator`] trait). It yields the allocated values -/// in **reverse allocation order** -- the most recently allocated element is -/// returned first. -/// -/// # Thread safety -/// -/// `IntoIter` is **`!Send` and `!Sync`** -- the arena is single‑threaded and -/// this iterator must remain on the thread where it was created. -pub struct IntoIter<'a, T, A: UninitAllocator + 'a> { - // keeps the backing memory alive - allocator: &'a A, - // the remaining raw pointers (in reverse order) - pointers: vec::IntoIter<*mut T>, -} - -impl<'a, T, A: UninitAllocator + 'a> IntoIter<'a, T, A> { - /// Returns a reference to the backing allocator. - #[must_use] - pub fn allocator(&self) -> &A { - self.allocator - } -} - -impl<'a, T, A: UninitAllocator + 'a> Iterator for IntoIter<'a, T, A> { - type Item = T; - - fn next(&mut self) -> Option { - let ptr = self.pointers.next()?; - // SAFETY: The pointer comes from the tracking list and points to a - // valid, initialised `T`. The backing memory is still alive (guaranteed - // by `allocator`). No other reference to this memory exists. - Some(unsafe { ptr::read(ptr) }) - } - - fn size_hint(&self) -> (usize, Option) { - self.pointers.size_hint() - } -} - -impl<'a, T, A: UninitAllocator + 'a> Drop for IntoIter<'a, T, A> { - fn drop(&mut self) { - // Drop any remaining elements to prevent leaks. - for ptr in self.pointers.by_ref() { - // SAFETY: The pointer is valid and points to an initialised `T`. - unsafe { - ptr::drop_in_place(ptr); - } - } - } -} - -impl<'a, T, A: UninitAllocator + 'a> IntoIterator for TypedArenaRef<'a, T, A> { - type Item = T; - type IntoIter = IntoIter<'a, T, A>; - - /// Consumes the arena and returns an iterator over its allocated values in - /// reverse allocation order. - /// - /// The iterator yields **owned** values. The memory in the backing - /// allocator is **not** freed -- it stays reserved and can be reused after - /// a manual reset of the underlying bump allocator (once all references to - /// the memory have ended). - /// - /// If the iterator is dropped before it is fully consumed, the remaining - /// elements are automatically dropped. - /// - /// # Examples - /// - /// ``` - /// use linalloc::{BumpArena, TypedArenaRef}; - /// - /// let bump = BumpArena::new(128); - /// let mut arena = TypedArenaRef::::new_in(&bump); - /// - /// arena.try_alloc("foo".to_string()).unwrap(); - /// arena.try_alloc("bar".to_string()).unwrap(); - /// - /// // Consume the arena. - /// let s = arena.into_iter().collect::>(); - /// assert_eq!(s, ["bar", "foo"]); - /// ``` - fn into_iter(self) -> IntoIter<'a, T, A> { - // Prevent the TypedArenaRef's Drop from running -- we are taking over - // ownership of the tracking list and the responsibility for dropping - // the values. - let this = mem::ManuallyDrop::new(self); - - // SAFETY: `allocations` is a `UnsafeCell>` and we have - // exclusive access to the arena (it is being consumed). Moving the - // `Vec` out of the `UnsafeCell` is sound. - let allocations = unsafe { ptr::read(this.allocations.get()) }; - - // Build a vector of pointers in reverse allocation order so that the - // iterator yields the most recently allocated element first. - let pointers: Vec<*mut T> = allocations.into_iter().rev().collect(); - - IntoIter { allocator: this.allocator, pointers: pointers.into_iter() } - } -} - -impl<'a, T, A: UninitAllocator + 'a> IntoIterator for &'a TypedArenaRef<'a, T, A> { - type Item = &'a T; - type IntoIter = Iter<'a, T>; - - /// Returns an iterator over the allocated values in reverse allocation - /// order (LIFO). - /// - /// The iterator yields `&T` and borrows the arena immutably. New - /// allocations are possible while the iterator is alive, but they will - /// not appear in this iterator because it captures a snapshot of the - /// current state. - fn into_iter(self) -> Iter<'a, T> { - self.iter() - } -} - -impl<'a, T, A: UninitAllocator + 'a> IntoIterator for &'a mut TypedArenaRef<'a, T, A> { - type Item = &'a mut T; - type IntoIter = IterMut<'a, T>; - - /// Returns a mutable iterator over the allocated values in reverse - /// allocation order (LIFO). - /// - /// The iterator yields `&mut T` and borrows the arena mutably. No other - /// allocations or resets are possible while the iterator is alive. - fn into_iter(self) -> IterMut<'a, T> { - self.iter_mut() - } -} - -impl<'a, T, A: UninitAllocator + 'a> DoubleEndedIterator for IntoIter<'a, T, A> { - /// Removes and returns an element from the back of the iterator. - /// - /// This yields the elements in **forward allocation order** -- the first - /// allocated element is returned first, which is the reverse of the - /// default iteration order (LIFO). - /// - /// # Examples - /// - /// ``` - /// use linalloc::{BumpArena, TypedArenaRef}; - /// - /// let bump = BumpArena::new(128); - /// let mut arena = TypedArenaRef::::new_in(&bump); - /// arena.try_alloc("first".to_string()).unwrap(); - /// arena.try_alloc("second".to_string()).unwrap(); - /// - /// let mut iter = arena.into_iter(); - /// assert_eq!(iter.next_back(), Some("first".to_string())); // forward - /// assert_eq!(iter.next(), Some("second".to_string())); // LIFO - /// ``` - fn next_back(&mut self) -> Option { - let ptr = self.pointers.next_back()?; - // SAFETY: The pointer is valid and points to an initialised `T`. - // See `next` for the full safety argument. - Some(unsafe { ptr::read(ptr) }) - } -} - -/// An immutable iterator over the elements of a [`TypedArenaRef`]. -/// -/// This iterator yields `&T` in **reverse allocation order** (LIFO). -/// It borrows the arena immutably, so the arena cannot be dropped while -/// the iterator exists. New allocations **are** allowed; they will not -/// appear in this iterator because it operates on a snapshot of the -/// tracking list taken at creation time. -/// -/// # Thread safety -/// -/// `Iter` is `!Send` and `!Sync` -- the contained raw pointers are neither -/// `Send` nor `Sync`, keeping the iterator confined to a single thread. -pub struct Iter<'s, T> { - // Snapshotted pointers in reverse order. - pointers: vec::IntoIter<*const T>, - // Ensures the arena cannot be dropped while the iterator exists. - _marker: PhantomData<&'s ()>, -} - -impl<'s, T: 's> Iterator for Iter<'s, T> { - type Item = &'s T; - - fn next(&mut self) -> Option<&'s T> { - let ptr = self.pointers.next()?; - // SAFETY: The pointer comes from a snapshot of the tracking list, - // which contains only valid, initialised `T`s. The arena cannot be - // dropped (we hold `&self`) and `reset` requires `unsafe` or - // `&mut self`, so the memory remains stable. - Some(unsafe { &*ptr }) - } - - fn size_hint(&self) -> (usize, Option) { - self.pointers.size_hint() - } -} - -impl<'s, T: 's> DoubleEndedIterator for Iter<'s, T> { - fn next_back(&mut self) -> Option<&'s T> { - let ptr = self.pointers.next_back()?; - Some(unsafe { &*ptr }) - } -} - -/// A mutable iterator over the elements of a [`TypedArenaRef`]. -/// -/// This iterator yields `&mut T` in **reverse allocation order** (LIFO). -/// Because it takes `&mut self` on the arena, the borrow checker prevents -/// any additional allocations or resets while the iterator is alive. -/// -/// # Thread safety -/// -/// `IterMut` is `!Send` and `!Sync` for the same reasons as [`Iter`]. -pub struct IterMut<'s, T> { - // Borrows the tracking list directly. - slice: &'s [*mut T], - // Position from the end of the slice. - pos: usize, -} - -impl<'s, T> Iterator for IterMut<'s, T> { - type Item = &'s mut T; - - fn next(&mut self) -> Option<&'s mut T> { - if self.pos == 0 { - return None; - } - self.pos -= 1; - let ptr = self.slice[self.pos]; - // SAFETY: We have exclusive access to the arena (&mut self). - // The pointer is valid and unique. - Some(unsafe { &mut *ptr }) - } - - fn size_hint(&self) -> (usize, Option) { - (self.pos, Some(self.pos)) - } -} - -impl<'a, T, A: UninitAllocator + 'a> TypedArenaRef<'a, T, A> { - /// Returns an immutable iterator over all allocated elements, in - /// **reverse allocation order** (LIFO). - /// - /// The iterator yields `&T` and borrows the arena immutably. New - /// allocations are possible while the iterator is alive, but they will - /// not appear in this iterator because it captures a snapshot of the - /// current state. - /// - /// # Examples - /// - /// ``` - /// use linalloc::{BumpArena, TypedArenaRef}; - /// - /// let bump = BumpArena::new(128); - /// let mut arena = TypedArenaRef::::new_in(&bump); - /// arena.try_alloc("foo".to_string()).unwrap(); - /// arena.try_alloc("bar".to_string()).unwrap(); - /// - /// let mut iter = arena.iter(); - /// assert_eq!(iter.next(), Some(&"bar".to_string())); // LIFO - /// assert_eq!(iter.next(), Some(&"foo".to_string())); - /// assert_eq!(iter.next(), None); - /// - /// // A new allocation does not affect `iter`. - /// let _third = arena.try_alloc("baz".to_string()).unwrap(); - /// ``` - pub fn iter(&self) -> Iter<'_, T> { - let allocations = unsafe { &*self.allocations.get() }; - // Clone the pointers and reverse to get LIFO order. - let mut pointers: Vec<*const T> = - allocations.iter().copied().map(<*mut T>::cast_const).collect(); - pointers.reverse(); - Iter { pointers: pointers.into_iter(), _marker: PhantomData } - } - - /// Returns a mutable iterator over all allocated elements, in - /// **reverse allocation order** (LIFO). - /// - /// This method requires `&mut self`, so no other allocations or resets - /// are possible while the iterator is alive. The iterator yields - /// `&mut T`, giving exclusive access to each element. - /// - /// # Examples - /// - /// ``` - /// use linalloc::{BumpArena, TypedArenaRef}; - /// - /// let bump = BumpArena::new(128); - /// let mut arena = TypedArenaRef::::new_in(&bump); - /// arena.try_alloc("foo".to_string()).unwrap(); - /// arena.try_alloc("bar".to_string()).unwrap(); - /// - /// for s in arena.iter_mut() { - /// *s = format!("new-{s}"); - /// } - /// ``` - pub fn iter_mut(&mut self) -> IterMut<'_, T> { - let allocs = self.allocations.get_mut(); - let len = allocs.len(); - IterMut { slice: allocs.as_slice(), pos: len } - } -}