#![expect(clippy::unnecessary_safety_comment)] use std::{ alloc::{self, Layout}, mem::{align_of, size_of}, ptr, slice, }; use assert_unchecked::assert_unchecked; use super::{NonNull, StackCapacity}; pub trait StackCommon: StackCapacity { // Getter + setter methods defined by implementer fn start(&self) -> NonNull; fn end(&self) -> NonNull; fn cursor(&self) -> NonNull; fn set_start(&mut self, start: NonNull); fn set_end(&mut self, end: NonNull); fn set_cursor(&mut self, cursor: NonNull); // Defined by implementer fn len(&self) -> usize; /// Make allocation of `capacity_bytes` bytes, aligned for `T`. /// /// # Panics /// Panics if out of memory (or may abort, depending on global allocator's behavior). /// /// # SAFETY /// * `capacity_bytes` must not be 0. /// * `capacity_bytes` must be a multiple of `mem::size_of::()`. /// * `capacity_bytes` must not exceed `Self::MAX_CAPACITY_BYTES`. #[inline] unsafe fn allocate(capacity_bytes: usize) -> (NonNull, NonNull) { // SAFETY: Caller guarantees `capacity_bytes` satisfies requirements let layout = Self::layout_for(capacity_bytes); let (start, end) = allocate(layout); // SAFETY: `layout_for` produces a layout with `T`'s alignment, so pointers are aligned for `T` (start.cast::(), end.cast::()) } /// Grow allocation. /// /// `start` and `end` are set to the start and end of new allocation. /// `current` is set so distance from `start` is old `capacity_bytes`. /// This is where it should be if stack was previously full to capacity. /// /// # Panics /// Panics if stack is already at maximum capacity. /// /// # SAFETY /// Stack must have already allocated. i.e. `start` is not a dangling pointer. #[inline] unsafe fn grow(&mut self) { // Grow allocation. // SAFETY: Caller guarantees stack has allocated. // `start` and `end` are boundaries of that allocation (`alloc` and `grow` ensure that). // So `old_start_ptr` and `old_layout` accurately describe the current allocation. // `grow` creates new allocation with byte size double what it currently is, or caps it // at `MAX_CAPACITY_BYTES`. // Old capacity in bytes was a multiple of `size_of::()`, so double that must be too. // `MAX_CAPACITY_BYTES` is also a multiple of `size_of::()`. // So new capacity in bytes must be a multiple of `size_of::()`. // `MAX_CAPACITY_BYTES <= isize::MAX`. let old_start_ptr = self.start().cast::(); let old_layout = Self::layout_for(self.capacity_bytes()); let (start, end, current) = grow(old_start_ptr, old_layout, Self::MAX_CAPACITY_BYTES); // Update pointers. // SAFETY: `start`, `end`, and `current` are all `NonNull` - just casting them. // All pointers returned from `grow` are aligned for `T`. // Old capacity and new capacity in bytes are both multiples of `size_of::()`, // so distances `end - start` and `current - start` are both multiples of `size_of::()`. self.set_start(start.cast::()); self.set_end(end.cast::()); self.set_cursor(current.cast::()); } /// Deallocate stack memory. /// /// Note: Does *not* drop the contents of the stack (the `T`s), only the memory allocated /// by `allocate` / `grow`. If stack is not empty, also call `drop_contents()` before calling this. /// /// # SAFETY /// Stack must have already allocated. i.e. `start` is not a dangling pointer. #[inline] unsafe fn deallocate(&self) { // SAFETY: Caller guarantees stack is allocated. // `start` and `end` are boundaries of that allocation (`allocate` and `grow` ensure that). // So `start` and `layout` accurately describe the current allocation. let layout = Self::layout_for(self.capacity_bytes()); alloc::dealloc(self.start().as_ptr().cast::(), layout); } /// Drop contents of stack. /// /// This function will be optimized out if `T` is non-drop, as `drop_in_place` calls /// `std::mem::needs_drop` internally and is a no-op if it returns true. /// /// # SAFETY /// * Stack must be allocated. /// * Stack must contain `self.len()` initialized entries, starting at `self.start()`. #[inline] unsafe fn drop_contents(&self) { // Drop contents. Next line copied from `std`'s `Vec`. // SAFETY: Caller guarantees stack contains `len` initialized entries, starting at `start`. ptr::drop_in_place(ptr::slice_from_raw_parts_mut(self.start().as_ptr(), self.len())); } /// Get layout for allocation of `capacity_bytes` bytes. /// /// # SAFETY /// * `capacity_bytes` must not be 0. /// * `capacity_bytes` must be a multiple of `mem::size_of::()`. /// * `capacity_bytes` must not exceed `Self::MAX_CAPACITY_BYTES`. #[inline] unsafe fn layout_for(capacity_bytes: usize) -> Layout { // `capacity_bytes` must not be 0 because cannot make 0-size allocations. debug_assert!(capacity_bytes > 0); // `capacity_bytes` must be a multiple of `size_of::()` so that `cursor == end` // checks in `push` methods accurately detects when full to capacity debug_assert!(capacity_bytes % size_of::() == 0); // `capacity_bytes` must not exceed `Self::MAX_CAPACITY_BYTES` to prevent creating an allocation // of illegal size debug_assert!(capacity_bytes <= Self::MAX_CAPACITY_BYTES); // SAFETY: `align_of::()` trivially satisfies alignment requirements. // Caller guarantees `capacity_bytes <= MAX_CAPACITY_BYTES`. // `MAX_CAPACITY_BYTES` takes into account the rounding-up by alignment requirement. Layout::from_size_align_unchecked(capacity_bytes, align_of::()) } /// Get offset of `cursor` in number of `T`s. /// /// # SAFETY /// * `self.cursor()` and `self.start()` must be derived from same pointer. /// * `self.cursor()` must be `>= self.start()`. /// * Byte distance between `self.cursor()` and `self.start()` must be a multiple of `size_of::()`. unsafe fn cursor_offset(&self) -> usize { // `offset_from` returns offset in units of `T`. // SAFETY: Caller guarantees `cursor` and `start` are derived from same pointer. // This implies that both pointers are always within bounds of a single allocation. // Caller guarantees `cursor >= start`. // Caller guarantees distance between pointers is a multiple of `size_of::()`. // `assert_unchecked!` is to help compiler to optimize. // See: https://doc.rust-lang.org/std/primitive.pointer.html#method.sub_ptr #[expect(clippy::cast_sign_loss)] unsafe { assert_unchecked!(self.cursor() >= self.start()); self.cursor().offset_from(self.start()) as usize } } /// Get capacity. #[inline] fn capacity(&self) -> usize { // SAFETY: `allocate` and `grow` both ensure: // * `start` and `end` are both derived from same pointer // * `start` and `end` are both within bounds of a single allocation. // * `end` is always >= `start`. // * Distance between `start` and `end` is always a multiple of `size_of::()`. // `assert_unchecked!` is to help compiler to optimize. // See: https://doc.rust-lang.org/std/primitive.pointer.html#method.sub_ptr #[expect(clippy::cast_sign_loss)] unsafe { assert_unchecked!(self.end() >= self.start()); self.end().offset_from(self.start()) as usize } } /// Get capacity in bytes. #[inline] fn capacity_bytes(&self) -> usize { // SAFETY: `allocate` and `grow` both ensure: // * `start` and `end` are both derived from same pointer // * `start` and `end` are both within bounds of a single allocation. // * `end` is always >= `start`. // * Distance between `start` and `end` is always a multiple of `size_of::()`. // `assert_unchecked!` is to help compiler to optimize. // See: https://doc.rust-lang.org/std/primitive.pointer.html#method.sub_ptr #[expect(clippy::cast_sign_loss)] unsafe { assert_unchecked!(self.end() >= self.start()); self.end().byte_offset_from(self.start()) as usize } } /// Get contents of stack as a slice `&[T]`. #[inline] fn as_slice(&self) -> &[T] { // SAFETY: Stack always contains `self.len()` entries, starting at `self.start()` unsafe { slice::from_raw_parts(self.start().as_ptr(), self.len()) } } /// Get contents of stack as a mutable slice `&mut [T]`. #[inline] fn as_mut_slice(&mut self) -> &mut [T] { // SAFETY: Stack always contains `self.len()` entries, starting at `self.start()` unsafe { slice::from_raw_parts_mut(self.start().as_ptr(), self.len()) } } } /// Make allocation of with provided layout. /// /// This is a separate non-generic function to improve compile time /// (same pattern as `std::vec::Vec` uses). /// /// # Panics /// Panics if out of memory (or may abort, depending on global allocator's behavior). /// /// # SAFETY /// `layout` must have non-zero size. #[inline] unsafe fn allocate(layout: Layout) -> (/* start */ NonNull, /* end */ NonNull) { // SAFETY: Caller guarantees `layout` has non-zero-size let ptr = alloc::alloc(layout); if ptr.is_null() { alloc::handle_alloc_error(layout); } // SAFETY: We checked `ptr` is non-null let start = NonNull::new_unchecked(ptr); // SAFETY: We allocated `layout.size()` bytes, so `end` is end of allocation let end = start.add(layout.size()); (start, end) } /// Grow existing allocation. /// /// Grow by doubling size, with high bound of `max_capacity_bytes`. /// /// # SAFETY /// * `old_start` and `old_layout` must describe an existing allocation. /// * `max_capacity_bytes` must be `>= old_layout.size()`. /// * `max_capacity_bytes` must be `<= isize::MAX`. unsafe fn grow( old_start: NonNull, old_layout: Layout, max_capacity_bytes: usize, ) -> (/* start */ NonNull, /* end */ NonNull, /* current */ NonNull) { // Get new capacity // Capacity in bytes cannot be larger than `isize::MAX`, so `* 2` cannot overflow let old_capacity_bytes = old_layout.size(); let mut new_capacity_bytes = old_capacity_bytes * 2; if new_capacity_bytes > max_capacity_bytes { assert!(old_capacity_bytes < max_capacity_bytes, "Cannot grow beyond `Self::MAX_CAPACITY`"); new_capacity_bytes = max_capacity_bytes; } debug_assert!(new_capacity_bytes > old_capacity_bytes); // Reallocate. // SAFETY: Caller guarantees `old_start` and `old_layout` describe an existing allocation. // Caller guarantees that `max_capacity_bytes <= isize::MAX`. // `new_capacity_bytes` is capped above at `max_capacity_bytes`, so is a legal allocation size. // `start` and `end` are boundaries of that allocation (`alloc` and `grow` ensure that). // So `start` and `old_layout` accurately describe the current allocation. // `old_capacity_bytes` was a multiple of `size_of::()`, so double that must be too. // `MAX_CAPACITY_BYTES` is also a multiple of `size_of::()`. // So `new_capacity_bytes` must be a multiple of `size_of::()`. // `new_capacity_bytes` is `<= MAX_CAPACITY_BYTES`, so is a legal allocation size. // `layout_for` produces a layout with `T`'s alignment, so `new_ptr` is aligned for `T`. let new_start = unsafe { let old_ptr = old_start.as_ptr(); let new_ptr = alloc::realloc(old_ptr, old_layout, new_capacity_bytes); if new_ptr.is_null() { let new_layout = Layout::from_size_align_unchecked(old_capacity_bytes, old_layout.align()); alloc::handle_alloc_error(new_layout); } NonNull::new_unchecked(new_ptr) }; // Update pointers. // // Stack was full to capacity, so new last index after push is the old capacity. // i.e. `new_cursor - new_start == old_end - old_start`. // Note: All pointers need to be updated even if allocation grew in place. // From docs for `GlobalAlloc::realloc`: // "Any access to the old `ptr` is Undefined Behavior, even if the allocation remained in-place." // // `end` changes whatever happens, so always need to be updated. // `cursor` needs to be derived from `start` to make `offset_from` valid, so also needs updating. // // SAFETY: We checked that `new_ptr` is non-null. // `old_capacity_bytes < new_capacity_bytes` (ensured above), so `new_cursor` must be in bounds. let new_end = new_start.add(new_capacity_bytes); let new_cursor = new_start.add(old_capacity_bytes); (new_start, new_end, new_cursor) }