From 7360c017cf444c34fa888a3b709b69fe3fcce52b Mon Sep 17 00:00:00 2001 From: williamwutq Date: Fri, 7 Aug 2026 15:30:09 -0700 Subject: [PATCH 1/9] Add new slice APIs and tests --- src/alloc/slice.rs | 562 +++++++++++++++++++++++++++++++++++++++++++++ src/test.rs | 338 +++++++++++++++++++++++++++ 2 files changed, 900 insertions(+) diff --git a/src/alloc/slice.rs b/src/alloc/slice.rs index f28fe79..b5c4fca 100644 --- a/src/alloc/slice.rs +++ b/src/alloc/slice.rs @@ -344,6 +344,134 @@ impl<'a> BStackSlice<'a> { } } + /// Split into two sub-views at `mid`, relative to this slice's start. + /// + /// Equivalent to `(self.subslice(0, mid), self.subslice(mid, self.len()))`, + /// following `std` slice naming. + /// + /// # Panics + /// + /// Panics if `mid > self.len()`. + #[inline] + pub fn split_at(&self, mid: u64) -> (BStackSlice<'a>, BStackSlice<'a>) { + assert!(mid <= self.len(), "split_at: mid must be <= slice length"); + (self.subslice(0, mid), self.subslice(mid, self.len())) + } + + /// Split into two independent sub-views at `mid`, relative to this slice's + /// start. + /// + /// The returned slices are independent — like [`subslice`](Self::subslice), + /// they carry the original `&'a BStack` lifetime rather than borrowing from + /// `self`. + /// + /// # Panics + /// + /// Panics if `mid > self.len()`. + #[inline] + pub fn split_at_mut(&mut self, mid: u64) -> (BStackSlice<'a>, BStackSlice<'a>) { + assert!( + mid <= self.len(), + "split_at_mut: mid must be <= slice length" + ); + (self.subslice(0, mid), self.subslice(mid, self.len())) + } + + /// Return a sub-view of the first `n` bytes. + /// + /// The returned slice has length `min(n, self.len())`. + #[inline] + pub fn head(&self, n: u64) -> BStackSlice<'a> { + let n = n.min(self.len()); + self.subslice(0, n) + } + + /// Return a sub-view of the last `n` bytes. + /// + /// The returned slice has length `min(n, self.len())`. + #[inline] + pub fn tail(&self, n: u64) -> BStackSlice<'a> { + let n = n.min(self.len()); + self.subslice(self.len() - n, self.len()) + } + + /// Read the byte at `index`, or `None` if out of bounds. + #[inline] + pub fn get(&self, index: u64) -> io::Result> { + if index >= self.len() { + return Ok(None); + } + let mut buf = [0u8; 1]; + self.stack.get_into(self.start() + index, &mut buf)?; + Ok(Some(buf[0])) + } + + /// Returns `true` if the slice contains `needle`. + #[inline] + pub fn contains(&self, needle: u8) -> io::Result { + Ok(self.read()?.contains(&needle)) + } + + /// Returns `true` if the slice begins with `prefix`. + pub fn starts_with(&self, prefix: &[u8]) -> io::Result { + let n = prefix.len() as u64; + if n > self.len() { + return Ok(false); + } + Ok(self.head(n).read()? == prefix) + } + + /// Returns `true` if the slice ends with `suffix`. + pub fn ends_with(&self, suffix: &[u8]) -> io::Result { + let n = suffix.len() as u64; + if n > self.len() { + return Ok(false); + } + Ok(self.tail(n).read()? == suffix) + } + + /// Returns the index of the first occurrence of `needle`, or `None` if not + /// found. + #[inline] + pub fn find(&self, needle: u8) -> io::Result> { + Ok(self + .read()? + .iter() + .position(|&b| b == needle) + .map(|i| i as u64)) + } + + /// Returns the index of the last occurrence of `needle`, or `None` if not + /// found. + #[inline] + pub fn rfind(&self, needle: u8) -> io::Result> { + Ok(self + .read()? + .iter() + .rposition(|&b| b == needle) + .map(|i| i as u64)) + } + + /// Returns the index of the first byte satisfying `predicate`, or `None`. + #[inline] + pub fn position(&self, predicate: impl Fn(u8) -> bool) -> io::Result> { + Ok(self + .read()? + .iter() + .position(|&b| predicate(b)) + .map(|i| i as u64)) + } + + /// Returns the index of the last byte satisfying `predicate`, or `None`. + #[inline] + pub fn rposition(&self, predicate: impl Fn(u8) -> bool) -> io::Result> { + Ok(self + .read()? + .iter() + .rposition(|&b| predicate(b)) + .map(|i| i as u64)) + } + /// Read the entire slice into a new `Vec`. #[inline] pub fn read(&self) -> io::Result> { @@ -439,6 +567,206 @@ impl<'a> BStackSlice<'a> { self.stack.zero(self.start() + start, n) } + /// Fill the entire slice with `value`. + /// + /// A single crash-atomic [`BStack::repeat`] call. + /// + /// Requires the `set` feature. + #[cfg(feature = "set")] + #[inline] + pub fn fill(&mut self, value: u8) -> io::Result<()> { + self.stack.repeat(self.start(), [value], self.len()) + } + + /// Fill the slice by calling `f` once per byte. + /// + /// The generated bytes are staged in memory and committed with a single + /// crash-atomic [`write`](Self::write) call. + /// + /// Requires the `set` feature. + #[cfg(feature = "set")] + #[inline] + pub fn fill_with(&mut self, mut f: impl FnMut() -> u8) -> io::Result<()> { + let buf: Vec = (0..self.len()).map(|_| f()).collect(); + self.write(buf) + } + + /// Copy `src` into this slice. + /// + /// A single crash-atomic [`BStack::set`] call. + /// + /// Requires the `set` feature. + /// + /// # Panics + /// + /// Panics if `src.len() != self.len()`. + #[cfg(feature = "set")] + #[inline] + pub fn copy_from_slice(&mut self, src: &[u8]) -> io::Result<()> { + assert_eq!( + src.len() as u64, + self.len(), + "copy_from_slice: length mismatch" + ); + self.stack.set(self.start(), src) + } + + /// Copy the contents of `src` into this slice. + /// + /// A single crash-atomic [`BStack::copy`] call. `src` and `self` may + /// overlap or refer to the same region. + /// + /// Requires the `set` and `atomic` features. + /// + /// # Panics + /// + /// Panics if `src.len() != self.len()`. + /// + /// # Errors + /// + /// Returns [`io::ErrorKind::InvalidInput`] if `src` is backed by a + /// different [`BStack`]. + #[cfg(all(feature = "set", feature = "atomic"))] + pub fn copy_from_bstack_slice(&mut self, src: &BStackSlice<'_>) -> io::Result<()> { + assert_eq!( + src.len(), + self.len(), + "copy_from_bstack_slice: length mismatch" + ); + if !std::ptr::eq(src.stack(), self.stack) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "BStackSlice::copy_from_bstack_slice: source belongs to a different BStack", + )); + } + if self.is_empty() { + return Ok(()); + } + self.stack.copy(src.start(), self.start(), self.len()) + } + + /// Copy `src_range` (relative to this slice) to `dest` (relative to this + /// slice), within this slice. + /// + /// A single crash-atomic [`BStack::copy`] call; overlapping source and + /// destination are handled correctly. + /// + /// Requires the `set` and `atomic` features. + /// + /// # Panics + /// + /// Panics if `src_range.start > src_range.end`, if `src_range.end > + /// self.len()`, or if `dest + src_range.len()` overflows `u64` or exceeds + /// `self.len()`. + #[cfg(all(feature = "set", feature = "atomic"))] + pub fn copy_within(&mut self, src_range: Range, dest: u64) -> io::Result<()> { + assert!( + src_range.start <= src_range.end, + "copy_within: range start must be <= end" + ); + assert!( + src_range.end <= self.len(), + "copy_within: range end must be <= slice length" + ); + let n = src_range.end - src_range.start; + let dest_end = dest + .checked_add(n) + .expect("copy_within: dest + len overflows u64"); + assert!( + dest_end <= self.len(), + "copy_within: dest range exceeds slice length" + ); + if n == 0 { + return Ok(()); + } + self.stack + .copy(self.start() + src_range.start, self.start() + dest, n) + } + + /// Swap the contents of this slice with `other`. + /// + /// A single crash-atomic [`BStack::cross_exchange`] call. + /// + /// Requires the `set` and `atomic` features. + /// + /// # Panics + /// + /// Panics if `self.len() != other.len()`. + /// + /// # Errors + /// + /// Returns [`io::ErrorKind::InvalidInput`] if `other` is backed by a + /// different [`BStack`]. + #[cfg(all(feature = "set", feature = "atomic"))] + pub fn swap(&mut self, other: &mut BStackSlice<'_>) -> io::Result<()> { + assert_eq!(self.len(), other.len(), "swap: length mismatch"); + if !std::ptr::eq(self.stack, other.stack()) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "BStackSlice::swap: slices belong to different BStacks", + )); + } + if self.is_empty() || self.start() == other.start() { + return Ok(()); + } + self.stack + .cross_exchange(self.start(), other.start(), self.len()) + } + + /// Reverse the byte order of this slice in place. + /// + /// A single crash-atomic [`BStack::process`] call: the bytes are read, + /// reversed in memory, then committed in one write. + /// + /// Requires the `set` and `atomic` features. + // TODO: lower memory usage by reversing in chunks for large slices. + #[cfg(all(feature = "set", feature = "atomic"))] + #[inline] + pub fn reverse(&mut self) -> io::Result<()> { + self.stack + .process(self.start(), self.end(), |buf| buf.reverse()) + } + + /// Rotate the slice in place such that the bytes at `[mid, len)` move to + /// the front. + /// + /// A single crash-atomic [`BStack::process`] call. + /// + /// Requires the `set` and `atomic` features. + /// + /// # Panics + /// + /// Panics if `mid > self.len()`. + // TODO: lower memory usage for large slices + #[cfg(all(feature = "set", feature = "atomic"))] + pub fn rotate_left(&mut self, mid: u64) -> io::Result<()> { + assert!( + mid <= self.len(), + "rotate_left: mid must be <= slice length" + ); + self.stack.process(self.start(), self.end(), |buf| { + buf.rotate_left(mid as usize) + }) + } + + /// Rotate the slice in place such that the last `k` bytes move to the + /// front. + /// + /// A single crash-atomic [`BStack::process`] call. + /// + /// Requires the `set` and `atomic` features. + /// + /// # Panics + /// + /// Panics if `k > self.len()`. + // TODO: lower memory usage for large slices + #[cfg(all(feature = "set", feature = "atomic"))] + pub fn rotate_right(&mut self, k: u64) -> io::Result<()> { + assert!(k <= self.len(), "rotate_right: k must be <= slice length"); + self.stack + .process(self.start(), self.end(), |buf| buf.rotate_right(k as usize)) + } + /// Create a cursor-based reader positioned at the start of this slice. /// /// This clones the slice; the reader and the original slice are independent. @@ -767,6 +1095,117 @@ impl<'a, A: BStackAllocator> BStackOwnedSlice<'a, A> { self.as_slice().read_range_into(start, buf) } + /// Return a sub-view of the first `n` bytes. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) + /// and delegates to [`BStackSlice::head`]. + #[inline] + pub fn head<'s>(&'s self, n: u64) -> BStackSlice<'s> { + self.as_slice().head(n) + } + + /// Return a sub-view of the last `n` bytes. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) + /// and delegates to [`BStackSlice::tail`]. + #[inline] + pub fn tail<'s>(&'s self, n: u64) -> BStackSlice<'s> { + self.as_slice().tail(n) + } + + /// Split into two sub-views at `mid`. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) + /// and delegates to [`BStackSlice::split_at`]. + #[inline] + pub fn split_at<'s>(&'s self, mid: u64) -> (BStackSlice<'s>, BStackSlice<'s>) { + self.as_slice().split_at(mid) + } + + /// Split into two independent sub-views at `mid`. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice_mut`](Self::as_slice_mut) + /// and delegates to [`BStackSlice::split_at_mut`]. + #[inline] + pub fn split_at_mut<'s>(&'s mut self, mid: u64) -> (BStackSlice<'s>, BStackSlice<'s>) { + let mut view = self.as_slice_mut(); + view.split_at_mut(mid) + } + + /// Read the byte at `index`, or `None` if out of bounds. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) + /// and delegates to [`BStackSlice::get`]. + #[inline] + pub fn get(&self, index: u64) -> io::Result> { + self.as_slice().get(index) + } + + /// Returns `true` if the allocation contains `needle`. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) + /// and delegates to [`BStackSlice::contains`]. + #[inline] + pub fn contains(&self, needle: u8) -> io::Result { + self.as_slice().contains(needle) + } + + /// Returns `true` if the allocation begins with `prefix`. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) + /// and delegates to [`BStackSlice::starts_with`]. + #[inline] + pub fn starts_with(&self, prefix: &[u8]) -> io::Result { + self.as_slice().starts_with(prefix) + } + + /// Returns `true` if the allocation ends with `suffix`. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) + /// and delegates to [`BStackSlice::ends_with`]. + #[inline] + pub fn ends_with(&self, suffix: &[u8]) -> io::Result { + self.as_slice().ends_with(suffix) + } + + /// Returns the index of the first occurrence of `needle`, or `None` if not + /// found. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) + /// and delegates to [`BStackSlice::find`]. + #[inline] + pub fn find(&self, needle: u8) -> io::Result> { + self.as_slice().find(needle) + } + + /// Returns the index of the last occurrence of `needle`, or `None` if not + /// found. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) + /// and delegates to [`BStackSlice::rfind`]. + #[inline] + pub fn rfind(&self, needle: u8) -> io::Result> { + self.as_slice().rfind(needle) + } + + /// Returns the index of the first byte satisfying `predicate`, or `None`. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) + /// and delegates to [`BStackSlice::position`]. + #[inline] + pub fn position(&self, predicate: impl Fn(u8) -> bool) -> io::Result> { + self.as_slice().position(predicate) + } + + /// Returns the index of the last byte satisfying `predicate`, or `None`. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) + /// and delegates to [`BStackSlice::rposition`]. + #[inline] + pub fn rposition(&self, predicate: impl Fn(u8) -> bool) -> io::Result> { + self.as_slice().rposition(predicate) + } + /// Overwrite the beginning of this allocation with `data`. /// /// Internally borrows a [`BStackSlice`] via [`as_slice_mut`](Self::as_slice_mut) @@ -815,6 +1254,129 @@ impl<'a, A: BStackAllocator> BStackOwnedSlice<'a, A> { self.as_slice_mut().zero_range(start, n) } + /// Fill the entire allocation with `value`. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice_mut`](Self::as_slice_mut) + /// and delegates to [`BStackSlice::fill`]. + /// + /// Requires the `set` feature. + #[cfg(feature = "set")] + #[inline] + pub fn fill(&mut self, value: u8) -> io::Result<()> { + self.as_slice_mut().fill(value) + } + + /// Fill the allocation by calling `f` once per byte. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice_mut`](Self::as_slice_mut) + /// and delegates to [`BStackSlice::fill_with`]. + /// + /// Requires the `set` feature. + #[cfg(feature = "set")] + #[inline] + pub fn fill_with(&mut self, f: impl FnMut() -> u8) -> io::Result<()> { + self.as_slice_mut().fill_with(f) + } + + /// Copy `src` into this allocation. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice_mut`](Self::as_slice_mut) + /// and delegates to [`BStackSlice::copy_from_slice`]. + /// + /// Requires the `set` feature. + /// + /// # Panics + /// + /// Panics if `src.len() != self.len()`. + #[cfg(feature = "set")] + #[inline] + pub fn copy_from_slice(&mut self, src: &[u8]) -> io::Result<()> { + self.as_slice_mut().copy_from_slice(src) + } + + /// Copy the contents of `src` into this allocation. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice_mut`](Self::as_slice_mut) + /// and delegates to [`BStackSlice::copy_from_bstack_slice`]. + /// + /// Requires the `set` and `atomic` features. + /// + /// # Panics + /// + /// Panics if `src.len() != self.len()`. + #[cfg(all(feature = "set", feature = "atomic"))] + #[inline] + pub fn copy_from_bstack_slice(&mut self, src: &BStackSlice<'_>) -> io::Result<()> { + self.as_slice_mut().copy_from_bstack_slice(src) + } + + /// Copy `src_range` (relative to this allocation) to `dest`, within this + /// allocation. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice_mut`](Self::as_slice_mut) + /// and delegates to [`BStackSlice::copy_within`]. + /// + /// Requires the `set` and `atomic` features. + #[cfg(all(feature = "set", feature = "atomic"))] + #[inline] + pub fn copy_within(&mut self, src_range: Range, dest: u64) -> io::Result<()> { + self.as_slice_mut().copy_within(src_range, dest) + } + + /// Swap the contents of this allocation with `other`. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice_mut`](Self::as_slice_mut) + /// and delegates to [`BStackSlice::swap`]. + /// + /// Requires the `set` and `atomic` features. + /// + /// # Panics + /// + /// Panics if `self.len() != other.len()`. + #[cfg(all(feature = "set", feature = "atomic"))] + #[inline] + pub fn swap(&mut self, other: &mut BStackSlice<'_>) -> io::Result<()> { + self.as_slice_mut().swap(other) + } + + /// Reverse the byte order of this allocation in place. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice_mut`](Self::as_slice_mut) + /// and delegates to [`BStackSlice::reverse`]. + /// + /// Requires the `set` and `atomic` features. + #[cfg(all(feature = "set", feature = "atomic"))] + #[inline] + pub fn reverse(&mut self) -> io::Result<()> { + self.as_slice_mut().reverse() + } + + /// Rotate the allocation in place such that the bytes at `[mid, len)` move + /// to the front. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice_mut`](Self::as_slice_mut) + /// and delegates to [`BStackSlice::rotate_left`]. + /// + /// Requires the `set` and `atomic` features. + #[cfg(all(feature = "set", feature = "atomic"))] + #[inline] + pub fn rotate_left(&mut self, mid: u64) -> io::Result<()> { + self.as_slice_mut().rotate_left(mid) + } + + /// Rotate the allocation in place such that the last `k` bytes move to the + /// front. + /// + /// Internally borrows a [`BStackSlice`] via [`as_slice_mut`](Self::as_slice_mut) + /// and delegates to [`BStackSlice::rotate_right`]. + /// + /// Requires the `set` and `atomic` features. + #[cfg(all(feature = "set", feature = "atomic"))] + #[inline] + pub fn rotate_right(&mut self, k: u64) -> io::Result<()> { + self.as_slice_mut().rotate_right(k) + } + /// Create a cursor-based reader over this allocation. /// /// Internally borrows a [`BStackSlice`] via [`as_slice`](Self::as_slice) diff --git a/src/test.rs b/src/test.rs index ccb3850..4f1a074 100644 --- a/src/test.rs +++ b/src/test.rs @@ -2831,6 +2831,344 @@ mod alloc_tests { assert!(r2 < r15); } + // ---- BStackSlice: ergonomic query methods ------------------------------- + + #[test] + fn slice_get_in_and_out_of_bounds() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + #[cfg_attr(not(feature = "set"), allow(unused_mut))] + let mut s = alloc.alloc(4).unwrap(); + #[cfg(feature = "set")] + s.as_slice_mut().write([10u8, 20, 30, 40]).unwrap(); + let view = s.as_slice(); + #[cfg(feature = "set")] + { + assert_eq!(view.get(0).unwrap(), Some(10)); + assert_eq!(view.get(3).unwrap(), Some(40)); + } + assert_eq!(view.get(4).unwrap(), None); + assert_eq!(view.get(100).unwrap(), None); + } + + #[test] + fn slice_head_caps_at_len() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let s = alloc.alloc(5).unwrap(); + let view = s.as_slice(); + assert_eq!(view.head(2).len(), 2); + assert_eq!(view.head(2).start(), view.start()); + assert_eq!(view.head(100).len(), 5); + } + + #[test] + fn slice_tail_caps_at_len() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let s = alloc.alloc(5).unwrap(); + let view = s.as_slice(); + let t = view.tail(2); + assert_eq!(t.len(), 2); + assert_eq!(t.start(), view.start() + 3); + assert_eq!(view.tail(100).len(), 5); + } + + #[cfg(feature = "set")] + #[test] + fn slice_contains() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(4).unwrap(); + s.write([1u8, 2, 3, 4]).unwrap(); + let view = s.as_slice(); + assert!(view.contains(3).unwrap()); + assert!(!view.contains(9).unwrap()); + } + + #[cfg(feature = "set")] + #[test] + fn slice_starts_and_ends_with() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(4).unwrap(); + s.write([1u8, 2, 3, 4]).unwrap(); + let view = s.as_slice(); + assert!(view.starts_with(&[1, 2]).unwrap()); + assert!(!view.starts_with(&[2, 3]).unwrap()); + assert!(view.ends_with(&[3, 4]).unwrap()); + assert!(!view.ends_with(&[1, 2]).unwrap()); + assert!(!view.starts_with(&[1, 2, 3, 4, 5]).unwrap()); + assert!(!view.ends_with(&[1, 2, 3, 4, 5]).unwrap()); + } + + #[cfg(feature = "set")] + #[test] + fn slice_find_and_rfind() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(5).unwrap(); + s.write([1u8, 2, 3, 2, 1]).unwrap(); + let view = s.as_slice(); + assert_eq!(view.find(2).unwrap(), Some(1)); + assert_eq!(view.rfind(2).unwrap(), Some(3)); + assert_eq!(view.find(9).unwrap(), None); + assert_eq!(view.rfind(9).unwrap(), None); + } + + #[cfg(feature = "set")] + #[test] + fn slice_position_and_rposition() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(5).unwrap(); + s.write([1u8, 2, 3, 4, 5]).unwrap(); + let view = s.as_slice(); + assert_eq!(view.position(|b| b > 2).unwrap(), Some(2)); + assert_eq!(view.rposition(|b| b > 2).unwrap(), Some(4)); + assert_eq!(view.position(|b| b > 10).unwrap(), None); + } + + #[test] + fn slice_split_at() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let s = alloc.alloc(10).unwrap(); + let (a, b) = s.as_slice().split_at(4); + assert_eq!(a.range(), 0..4); + assert_eq!(b.range(), 4..10); + } + + #[test] + #[should_panic(expected = "mid must be <= slice length")] + fn slice_split_at_out_of_bounds() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let s = alloc.alloc(4).unwrap(); + let _ = s.as_slice().split_at(5); + } + + #[test] + fn slice_split_at_mut() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(10).unwrap(); + let mut view = s.as_slice_mut(); + let (a, b) = view.split_at_mut(4); + assert_eq!(a.range(), 0..4); + assert_eq!(b.range(), 4..10); + } + + // ---- BStackSlice: ergonomic write methods (feature `set`) -------------- + + #[cfg(feature = "set")] + #[test] + fn slice_fill() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(4).unwrap(); + s.as_slice_mut().fill(7).unwrap(); + assert_eq!(s.read().unwrap(), [7, 7, 7, 7]); + } + + #[cfg(feature = "set")] + #[test] + fn slice_fill_with() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(4).unwrap(); + let mut next = 0u8; + s.as_slice_mut() + .fill_with(|| { + next += 1; + next + }) + .unwrap(); + assert_eq!(s.read().unwrap(), [1, 2, 3, 4]); + } + + #[cfg(feature = "set")] + #[test] + fn slice_copy_from_slice() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(3).unwrap(); + s.as_slice_mut().copy_from_slice(&[9, 8, 7]).unwrap(); + assert_eq!(s.read().unwrap(), [9, 8, 7]); + } + + #[cfg(feature = "set")] + #[test] + #[should_panic(expected = "length mismatch")] + fn slice_copy_from_slice_length_mismatch() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(3).unwrap(); + let _ = s.as_slice_mut().copy_from_slice(&[9, 8]); + } + + // ---- BStackSlice: atomic write methods (features `set` + `atomic`) ----- + + #[cfg(all(feature = "set", feature = "atomic"))] + #[test] + fn slice_copy_from_bstack_slice_same_stack() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut src = alloc.alloc(3).unwrap(); + src.write([1u8, 2, 3]).unwrap(); + let mut dst = alloc.alloc(3).unwrap(); + dst.as_slice_mut() + .copy_from_bstack_slice(&src.as_slice()) + .unwrap(); + assert_eq!(dst.read().unwrap(), [1, 2, 3]); + } + + #[cfg(all(feature = "set", feature = "atomic"))] + #[test] + fn slice_copy_from_bstack_slice_cross_stack_errors() { + let (alloc_a, path_a) = mk_alloc(); + let _g_a = Guard(path_a); + let (alloc_b, path_b) = mk_alloc(); + let _g_b = Guard(path_b); + let src = alloc_a.alloc(3).unwrap(); + let mut dst = alloc_b.alloc(3).unwrap(); + let err = dst + .as_slice_mut() + .copy_from_bstack_slice(&src.as_slice()) + .unwrap_err(); + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + } + + #[cfg(all(feature = "set", feature = "atomic"))] + #[test] + fn slice_copy_within_overlapping() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(5).unwrap(); + s.write([1u8, 2, 3, 4, 5]).unwrap(); + s.as_slice_mut().copy_within(0..3, 2).unwrap(); + assert_eq!(s.read().unwrap(), [1, 2, 1, 2, 3]); + } + + #[cfg(all(feature = "set", feature = "atomic"))] + #[test] + fn slice_swap_exchanges_contents() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut a = alloc.alloc(3).unwrap(); + a.write([1u8, 2, 3]).unwrap(); + let mut b = alloc.alloc(3).unwrap(); + b.write([4u8, 5, 6]).unwrap(); + let mut a_view = a.as_slice_mut(); + let mut b_view = b.as_slice_mut(); + a_view.swap(&mut b_view).unwrap(); + assert_eq!(a.read().unwrap(), [4, 5, 6]); + assert_eq!(b.read().unwrap(), [1, 2, 3]); + } + + #[cfg(all(feature = "set", feature = "atomic"))] + #[test] + fn slice_swap_cross_stack_errors() { + let (alloc_a, path_a) = mk_alloc(); + let _g_a = Guard(path_a); + let (alloc_b, path_b) = mk_alloc(); + let _g_b = Guard(path_b); + let mut a = alloc_a.alloc(3).unwrap(); + let mut b = alloc_b.alloc(3).unwrap(); + let mut a_view = a.as_slice_mut(); + let mut b_view = b.as_slice_mut(); + let err = a_view.swap(&mut b_view).unwrap_err(); + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + } + + #[cfg(all(feature = "set", feature = "atomic"))] + #[test] + fn slice_reverse() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(5).unwrap(); + s.write([1u8, 2, 3, 4, 5]).unwrap(); + s.as_slice_mut().reverse().unwrap(); + assert_eq!(s.read().unwrap(), [5, 4, 3, 2, 1]); + } + + #[cfg(all(feature = "set", feature = "atomic"))] + #[test] + fn slice_rotate_left() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(5).unwrap(); + s.write([1u8, 2, 3, 4, 5]).unwrap(); + s.as_slice_mut().rotate_left(2).unwrap(); + assert_eq!(s.read().unwrap(), [3, 4, 5, 1, 2]); + } + + #[cfg(all(feature = "set", feature = "atomic"))] + #[test] + fn slice_rotate_right() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(5).unwrap(); + s.write([1u8, 2, 3, 4, 5]).unwrap(); + s.as_slice_mut().rotate_right(2).unwrap(); + assert_eq!(s.read().unwrap(), [4, 5, 1, 2, 3]); + } + + // ---- BStackOwnedSlice: ergonomic method mirrors ------------------------- + + #[cfg(all(feature = "set", feature = "atomic"))] + #[test] + fn owned_slice_ergonomic_mirrors() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let mut s = alloc.alloc(5).unwrap(); + s.write([1u8, 2, 3, 4, 5]).unwrap(); + assert_eq!(s.get(0).unwrap(), Some(1)); + assert!(s.contains(3).unwrap()); + assert!(s.starts_with(&[1, 2]).unwrap()); + assert!(s.ends_with(&[4, 5]).unwrap()); + assert_eq!(s.find(3).unwrap(), Some(2)); + assert_eq!(s.rfind(3).unwrap(), Some(2)); + assert_eq!(s.position(|b| b == 4).unwrap(), Some(3)); + assert_eq!(s.rposition(|b| b == 4).unwrap(), Some(3)); + assert_eq!(s.head(2).len(), 2); + assert_eq!(s.tail(2).len(), 2); + let (a, b) = s.split_at(2); + assert_eq!(a.len(), 2); + assert_eq!(b.len(), 3); + + s.fill(0).unwrap(); + assert_eq!(s.read().unwrap(), [0, 0, 0, 0, 0]); + s.fill_with(|| 9).unwrap(); + assert_eq!(s.read().unwrap(), [9, 9, 9, 9, 9]); + s.copy_from_slice(&[1, 2, 3, 4, 5]).unwrap(); + assert_eq!(s.read().unwrap(), [1, 2, 3, 4, 5]); + s.reverse().unwrap(); + assert_eq!(s.read().unwrap(), [5, 4, 3, 2, 1]); + s.rotate_left(1).unwrap(); + assert_eq!(s.read().unwrap(), [4, 3, 2, 1, 5]); + s.rotate_right(1).unwrap(); + assert_eq!(s.read().unwrap(), [5, 4, 3, 2, 1]); + + let mut other = alloc.alloc(5).unwrap(); + other.write([10u8, 20, 30, 40, 50]).unwrap(); + s.copy_from_bstack_slice(&other.as_slice()).unwrap(); + assert_eq!(s.read().unwrap(), [10, 20, 30, 40, 50]); + s.copy_within(0..2, 3).unwrap(); + assert_eq!(s.read().unwrap(), [10, 20, 30, 10, 20]); + + let mut third = alloc.alloc(5).unwrap(); + third.write([1u8, 1, 1, 1, 1]).unwrap(); + s.swap(&mut third.as_slice_mut()).unwrap(); + assert_eq!(s.read().unwrap(), [1, 1, 1, 1, 1]); + assert_eq!(third.read().unwrap(), [10, 20, 30, 10, 20]); + + let mut split_owned = alloc.alloc(4).unwrap(); + let (a, b) = split_owned.split_at_mut(1); + assert_eq!(a.len(), 1); + assert_eq!(b.len(), 3); + } + // ---- BStackBulkAllocator: alloc_bulk ------------------------------------ // 1. Empty lengths → empty Vec, stack unchanged. From 21599be25e1506846469ea41711a632b6279ae72 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Fri, 7 Aug 2026 15:39:13 -0700 Subject: [PATCH 2/9] Vec location equality --- src/alloc/vec.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/alloc/vec.rs b/src/alloc/vec.rs index bc07b7b..608712e 100644 --- a/src/alloc/vec.rs +++ b/src/alloc/vec.rs @@ -123,6 +123,35 @@ impl<'a, A: BStackOwnedSliceAllocator> fmt::Debug for BStackByteVec<'a, A> { } } +/// Location equality: whether two vecs occupy the same backing block. +/// +/// Compares the backing allocation's coordinates (header included), not +/// contents — a vec that has been reallocated (e.g. by [`push`](Self::push) +/// triggering growth) is no longer equal to its former self even if the bytes +/// are identical. +impl<'a, A: BStackOwnedSliceAllocator> PartialEq for BStackByteVec<'a, A> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.slice.as_range() == other.slice.as_range() + } +} + +/// Location equality against a [`BStackSlice`]: whether the vec's backing +/// block and the slice refer to the same coordinates. +impl<'a, A: BStackOwnedSliceAllocator> PartialEq> for BStackByteVec<'a, A> { + #[inline] + fn eq(&self, other: &BStackSlice<'a>) -> bool { + self.slice.as_range() == other.as_range() + } +} + +impl<'a, A: BStackOwnedSliceAllocator> PartialEq> for BStackSlice<'a> { + #[inline] + fn eq(&self, other: &BStackByteVec<'a, A>) -> bool { + other == self + } +} + impl<'a, A: BStackOwnedSliceAllocator> BStackByteVec<'a, A> { fn block_size(capacity: u64) -> io::Result { capacity.checked_add(HEADER_LEN).ok_or_else(|| { @@ -1649,4 +1678,35 @@ mod tests { assert_eq!(v.move_tail_into(&mut too_big).unwrap(), None); assert_eq!(v.read_bytes().unwrap(), [1, 2, 3]); } + + #[test] + fn partial_eq_vec_vec_is_by_location_not_content() { + let (alloc, path) = make_alloc(); + let _g = Guard(path); + let v1 = BStackByteVec::from_slice(&[1, 2, 3], &alloc).unwrap(); + let v2 = BStackByteVec::from_slice(&[1, 2, 3], &alloc).unwrap(); + // Identical content, distinct backing blocks: unequal by location. + assert_ne!(v1, v2); + // A reallocated vec no longer equals a slice snapshot of its old block. + let mut v3 = BStackByteVec::with_capacity(1, &alloc).unwrap(); + let old_block = unsafe { v3.raw_block() }; + v3.push(1).unwrap(); + v3.push(2).unwrap(); // triggers growth: v3 now lives at a new block + assert_ne!(v3, old_block); + } + + #[test] + fn partial_eq_vec_slice_same_location() { + let (alloc, path) = make_alloc(); + let _g = Guard(path); + let v = BStackByteVec::from_slice(&[1, 2, 3], &alloc).unwrap(); + // A vec is equal (by location) to a raw slice view of its own block. + let block = unsafe { v.raw_block() }; + assert_eq!(v, block); + assert_eq!(block, v); + // But not to a slice over a different vec's block. + let other = BStackByteVec::from_slice(&[1, 2, 3], &alloc).unwrap(); + let other_block = unsafe { other.raw_block() }; + assert_ne!(v, other_block); + } } From 439e52f1819cba65aa9c37cd0bbaa23be479f84e Mon Sep 17 00:00:00 2001 From: williamwutq Date: Fri, 7 Aug 2026 15:41:41 -0700 Subject: [PATCH 3/9] Add cross type slice equality --- src/alloc/slice.rs | 42 +++++++++++++++++++++++++++ src/test.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/src/alloc/slice.rs b/src/alloc/slice.rs index b5c4fca..31236c7 100644 --- a/src/alloc/slice.rs +++ b/src/alloc/slice.rs @@ -844,6 +844,20 @@ impl<'a> Ord for BStackSlice<'a> { } } +impl<'a> PartialEq for BStackSlice<'a> { + #[inline] + fn eq(&self, other: &BStackRange) -> bool { + self.range == *other + } +} + +impl<'a> PartialEq> for BStackRange { + #[inline] + fn eq(&self, other: &BStackSlice<'a>) -> bool { + *self == other.range + } +} + impl<'a> From> for BStackRange { #[inline] fn from(s: BStackSlice<'a>) -> BStackRange { @@ -1450,6 +1464,34 @@ impl<'a, A: BStackAllocator> Ord for BStackOwnedSlice<'a, A> { } } +impl<'a, A: BStackAllocator> PartialEq> for BStackOwnedSlice<'a, A> { + #[inline] + fn eq(&self, other: &BStackSlice<'a>) -> bool { + self.range == other.range + } +} + +impl<'a, A: BStackAllocator> PartialEq> for BStackSlice<'a> { + #[inline] + fn eq(&self, other: &BStackOwnedSlice<'a, A>) -> bool { + self.range == other.range + } +} + +impl<'a, A: BStackAllocator> PartialEq for BStackOwnedSlice<'a, A> { + #[inline] + fn eq(&self, other: &BStackRange) -> bool { + self.range == *other + } +} + +impl<'a, A: BStackAllocator> PartialEq> for BStackRange { + #[inline] + fn eq(&self, other: &BStackOwnedSlice<'a, A>) -> bool { + *self == other.range + } +} + impl<'a, A: BStackAllocator> From> for BStackRange { #[inline] fn from(s: BStackOwnedSlice<'a, A>) -> BStackRange { diff --git a/src/test.rs b/src/test.rs index 4f1a074..83a8921 100644 --- a/src/test.rs +++ b/src/test.rs @@ -3169,6 +3169,78 @@ mod alloc_tests { assert_eq!(b.len(), 3); } + // ---- Location equality: BStackSlice / BStackOwnedSlice ------------------ + + // 1. BStackSlice == BStackSlice compares coordinates, not identity of the + // borrowed BStack reference. + #[test] + fn slice_eq_slice_by_location() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let a = alloc.alloc(4).unwrap(); + let b = alloc.alloc(4).unwrap(); + assert_eq!(a.as_slice(), a.as_slice()); + assert_ne!(a.as_slice(), b.as_slice()); + } + + // 2. BStackOwnedSlice == BStackOwnedSlice compares coordinates, not + // handle identity: a second handle built over the same range compares + // equal, while a handle over a different range does not. + #[test] + fn owned_eq_owned_by_location() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let a = alloc.alloc(4).unwrap(); + let b = alloc.alloc(4).unwrap(); + let a_view = + unsafe { crate::alloc::BStackOwnedSlice::from_raw_range(&alloc, a.as_range()) }; + assert_eq!(a, a_view); + assert_ne!(a, b); + } + + // 3. BStackSlice == BStackOwnedSlice (both directions) compares coordinates. + #[test] + fn slice_eq_owned_cross_type() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let owned = alloc.alloc(4).unwrap(); + let matching = owned.as_slice(); + assert_eq!(owned, matching); + assert_eq!(matching, owned); + let other = alloc.alloc(4).unwrap(); + assert_ne!(owned, other.as_slice()); + assert_ne!(other.as_slice(), owned); + } + + // 4. BStackRange == BStackSlice (both directions) compares coordinates. + #[test] + fn range_eq_slice_cross_type() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let s = alloc.alloc(4).unwrap(); + let view = s.as_slice(); + let range = view.as_range(); + assert_eq!(range, view); + assert_eq!(view, range); + let other = alloc.alloc(4).unwrap(); + assert_ne!(range, other.as_slice()); + assert_ne!(other.as_slice(), range); + } + + // 5. BStackRange == BStackOwnedSlice (both directions) compares coordinates. + #[test] + fn range_eq_owned_cross_type() { + let (alloc, path) = mk_alloc(); + let _g = Guard(path); + let owned = alloc.alloc(4).unwrap(); + let range = owned.as_range(); + assert_eq!(range, owned); + assert_eq!(owned, range); + let other = alloc.alloc(4).unwrap(); + assert_ne!(range, other); + assert_ne!(other, range); + } + // ---- BStackBulkAllocator: alloc_bulk ------------------------------------ // 1. Empty lengths → empty Vec, stack unchanged. From b03d86cbc76755f427926db836bae22995070831 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Fri, 7 Aug 2026 15:55:49 -0700 Subject: [PATCH 4/9] Remove vec slice location eq --- src/alloc/vec.rs | 59 ------------------------------------------------ 1 file changed, 59 deletions(-) diff --git a/src/alloc/vec.rs b/src/alloc/vec.rs index 608712e..50db14c 100644 --- a/src/alloc/vec.rs +++ b/src/alloc/vec.rs @@ -123,35 +123,6 @@ impl<'a, A: BStackOwnedSliceAllocator> fmt::Debug for BStackByteVec<'a, A> { } } -/// Location equality: whether two vecs occupy the same backing block. -/// -/// Compares the backing allocation's coordinates (header included), not -/// contents — a vec that has been reallocated (e.g. by [`push`](Self::push) -/// triggering growth) is no longer equal to its former self even if the bytes -/// are identical. -impl<'a, A: BStackOwnedSliceAllocator> PartialEq for BStackByteVec<'a, A> { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.slice.as_range() == other.slice.as_range() - } -} - -/// Location equality against a [`BStackSlice`]: whether the vec's backing -/// block and the slice refer to the same coordinates. -impl<'a, A: BStackOwnedSliceAllocator> PartialEq> for BStackByteVec<'a, A> { - #[inline] - fn eq(&self, other: &BStackSlice<'a>) -> bool { - self.slice.as_range() == other.as_range() - } -} - -impl<'a, A: BStackOwnedSliceAllocator> PartialEq> for BStackSlice<'a> { - #[inline] - fn eq(&self, other: &BStackByteVec<'a, A>) -> bool { - other == self - } -} - impl<'a, A: BStackOwnedSliceAllocator> BStackByteVec<'a, A> { fn block_size(capacity: u64) -> io::Result { capacity.checked_add(HEADER_LEN).ok_or_else(|| { @@ -1679,34 +1650,4 @@ mod tests { assert_eq!(v.read_bytes().unwrap(), [1, 2, 3]); } - #[test] - fn partial_eq_vec_vec_is_by_location_not_content() { - let (alloc, path) = make_alloc(); - let _g = Guard(path); - let v1 = BStackByteVec::from_slice(&[1, 2, 3], &alloc).unwrap(); - let v2 = BStackByteVec::from_slice(&[1, 2, 3], &alloc).unwrap(); - // Identical content, distinct backing blocks: unequal by location. - assert_ne!(v1, v2); - // A reallocated vec no longer equals a slice snapshot of its old block. - let mut v3 = BStackByteVec::with_capacity(1, &alloc).unwrap(); - let old_block = unsafe { v3.raw_block() }; - v3.push(1).unwrap(); - v3.push(2).unwrap(); // triggers growth: v3 now lives at a new block - assert_ne!(v3, old_block); - } - - #[test] - fn partial_eq_vec_slice_same_location() { - let (alloc, path) = make_alloc(); - let _g = Guard(path); - let v = BStackByteVec::from_slice(&[1, 2, 3], &alloc).unwrap(); - // A vec is equal (by location) to a raw slice view of its own block. - let block = unsafe { v.raw_block() }; - assert_eq!(v, block); - assert_eq!(block, v); - // But not to a slice over a different vec's block. - let other = BStackByteVec::from_slice(&[1, 2, 3], &alloc).unwrap(); - let other_block = unsafe { other.raw_block() }; - assert_ne!(v, other_block); - } } From d12cb68054f26a374463c18d15a1efe77e2e9ca8 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Fri, 7 Aug 2026 16:07:53 -0700 Subject: [PATCH 5/9] Update README --- README.md | 61 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 49ea289..0f5a344 100644 --- a/README.md +++ b/README.md @@ -932,16 +932,33 @@ A borrowed I/O view carrying `&'a BStack` directly. Obtained from `BStackOwnedSl Key methods on `BStackSlice`: -| Method | Description | -|-----------------------------------------------------|---------------------------------------------| -| `read()` | Read the entire region into a new `Vec` | -| `read_into(buf)` | Read into a caller-supplied buffer | -| `read_range(start, end)` | Read a sub-range | -| `subslice(start, end)` | Narrow to a sub-range | -| `reader()` / `reader_at(offset)` | Cursor-based `BStackSliceReader` | -| `write(data)` *(feature `set`)* | Overwrite the beginning of the region | -| `write_range(start, data)` *(feature `set`)* | Overwrite a sub-range | -| `zero()` / `zero_range(start, n)` *(feature `set`)* | Zero the region or a sub-range | +| Method | Description | +|----------------------------------------------------------------------|---------------------------------------------------------| +| `read()` | Read the entire region into a new `Vec` | +| `read_into(buf)` | Read into a caller-supplied buffer | +| `read_range(start, end)` | Read a sub-range | +| `subslice(start, end)` | Narrow to a sub-range | +| `head(n)` / `tail(n)` | Sub-view of the first/last `n` bytes (capped to length) | +| `split_at(mid)` / `split_at_mut(mid)` | Split into two independent sub-views | +| `get(index)` | Read a single byte, or `None` if out of bounds | +| `contains(byte)` | Whether the slice contains a byte | +| `starts_with(prefix)` / `ends_with(suffix)` | Whether the slice starts/ends with a byte pattern | +| `find(byte)` / `rfind(byte)` | Index of the first/last occurrence of a byte | +| `position(pred)` / `rposition(pred)` | Index of the first/last byte matching a predicate | +| `reader()` / `reader_at(offset)` | Cursor-based `BStackSliceReader` | +| `write(data)` *(feature `set`)* | Overwrite the beginning of the region | +| `write_range(start, data)` *(feature `set`)* | Overwrite a sub-range | +| `zero()` / `zero_range(start, n)` *(feature `set`)* | Zero the region or a sub-range | +| `fill(value)` *(feature `set`)* | Overwrite the entire slice with one byte value | +| `fill_with(f)` *(feature `set`)* | Overwrite the entire slice, generating each byte | +| `copy_from_slice(src)` *(feature `set`)* | Overwrite from a matching-length `&[u8]` | +| `copy_from_bstack_slice(src)` *(features `set` + `atomic`)* | Overwrite from a matching-length `BStackSlice` | +| `copy_within(range, dest)` *(features `set` + `atomic`)* | Copy a sub-range to another offset, in place | +| `swap(other)` *(features `set` + `atomic`)* | Exchange contents with another same-length slice | +| `reverse()` *(features `set` + `atomic`)* | Reverse the byte order in place | +| `rotate_left(mid)` / `rotate_right(k)` *(features `set` + `atomic`)* | Rotate the slice in place | + +Every write method above is a single crash-atomic call. `BStackOwnedSlice` mirrors all of these (delegating through `as_slice()`/`as_slice_mut()`). ### `BStackRange` @@ -951,6 +968,14 @@ A raw `(offset, len)` coordinate pair with no backing reference. `Copy`, seriali A cursor-based reader over a `BStackSlice`. Implements `io::Read` and `io::Seek`. +### Slice Location Equality + +`BStackSlice`, `BStackOwnedSlice`, and `BStackRange` implement `PartialEq` against each other — every pairing, both directions (`BStackSlice == BStackSlice`, `BStackOwnedSlice == BStackOwnedSlice`, `BStackSlice == BStackOwnedSlice`, `BStackRange == BStackSlice`, `BStackRange == BStackOwnedSlice`). + +This is **location equality**: it compares coordinates (`offset`, `len`), not the bytes stored there. Two slices over disjoint regions that happen to hold identical bytes compare unequal; two handles over the exact same region compare equal even before anything has been written. The comparison is synchronous and infallible — no I/O is performed. + +To compare *contents* instead, read both sides (`read()`/`read_into()`) and compare the resulting `Vec`/`[u8]` directly. `BStackByteVec` deliberately does **not** implement `PartialEq` against any of these types, since a meaningful comparison for a vec would require reading its header to resolve `len` first — an I/O operation `==` should not perform silently. + ### Lifetime model `BStackOwnedSlice<'a, A>` borrows the allocator for `'a`. Views obtained via `as_slice[_mut]()` have a shorter lifetime tied to the borrow of the owned slice, preventing them from outliving the handle that owns the region. @@ -1048,14 +1073,14 @@ As a general guideline (based on `benches/alloc.rs` mixed-workload results): **Configuration** — all knobs are environment variables read once at startup: -| Variable | Meaning | Default | -|--------------------------|----------------------------------------------------------------|-------------| -| `BSTACK_BENCH_OP` | op mix: preset name or `alloc,realloc,dealloc` weight triple | `mixed` | -| `BSTACK_BENCH_SIZE` | size distribution preset | `uniform` | -| `BSTACK_BENCH_MAX` | maximum allocation length drawn | `1024` | -| `BSTACK_BENCH_THREADS` | comma-separated thread counts | `1,2,4,16` | -| `BSTACK_BENCH_PRE_ALLOC` | live allocations pre-populated per benchmark | `256` | -| `BSTACK_BENCH_SEED` | seed for the decision stream | `48` | +| Variable | Meaning | Default | +|--------------------------|--------------------------------------------------------------|------------| +| `BSTACK_BENCH_OP` | op mix: preset name or `alloc,realloc,dealloc` weight triple | `mixed` | +| `BSTACK_BENCH_SIZE` | size distribution preset | `uniform` | +| `BSTACK_BENCH_MAX` | maximum allocation length drawn | `1024` | +| `BSTACK_BENCH_THREADS` | comma-separated thread counts | `1,2,4,16` | +| `BSTACK_BENCH_PRE_ALLOC` | live allocations pre-populated per benchmark | `256` | +| `BSTACK_BENCH_SEED` | seed for the decision stream | `48` | Op-mix presets: `mixed`, `alloc-only`, `alloc-heavy`, `realloc-heavy`, `churn`. Size presets: `uniform`, `fixed`, `gamma[:k:theta_frac]`, `bimodal[:small:p_large]`. From e58f0d1ccde258f05b2383c7320b966b8b2a7b7a Mon Sep 17 00:00:00 2001 From: williamwutq Date: Fri, 7 Aug 2026 16:09:50 -0700 Subject: [PATCH 6/9] Add to CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c33ab5c..0ed87fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`BStackSlice`, `BStackOwnedSlice`, and `BStackRange` — cross-type `PartialEq` (`alloc`).** Every pairing among the three, both directions, compares `(offset, len)` coordinates only — location equality, not content — and performs no I/O. `BStackByteVec` deliberately does not participate: a meaningful comparison would require reading its header to resolve `len`, and `==` should not perform I/O silently. +- **`BStackSlice` — `std`-slice-style ergonomic methods (`alloc`).** Read-only, no extra feature: `get(index)`, `head(n)`/`tail(n)`, `contains(byte)`, `starts_with`/`ends_with`, `find`/`rfind`, `position`/`rposition`, `split_at`/`split_at_mut`. Write methods (`set`): `fill(value)` (single `BStack::repeat` call), `fill_with(f)`, `copy_from_slice(src)`. Atomic compound writes (`set` + `atomic`, each a single crash-atomic `BStack` call): `copy_from_bstack_slice`, `copy_within`, `swap` (via `cross_exchange`), `reverse`, `rotate_left`/`rotate_right` (via `process`). `BStackOwnedSlice` mirrors the full set, delegating through `as_slice()`/`as_slice_mut()`. + ## [0.4.1] - 2026-08-03 ### Added From e61ccf28d2cf2a37ba9eb4e815cf0e1458f406c7 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Fri, 7 Aug 2026 16:12:46 -0700 Subject: [PATCH 7/9] Remove unnecessary TODOs --- src/alloc/slice.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/alloc/slice.rs b/src/alloc/slice.rs index 31236c7..b8fa5aa 100644 --- a/src/alloc/slice.rs +++ b/src/alloc/slice.rs @@ -719,7 +719,6 @@ impl<'a> BStackSlice<'a> { /// reversed in memory, then committed in one write. /// /// Requires the `set` and `atomic` features. - // TODO: lower memory usage by reversing in chunks for large slices. #[cfg(all(feature = "set", feature = "atomic"))] #[inline] pub fn reverse(&mut self) -> io::Result<()> { @@ -737,7 +736,6 @@ impl<'a> BStackSlice<'a> { /// # Panics /// /// Panics if `mid > self.len()`. - // TODO: lower memory usage for large slices #[cfg(all(feature = "set", feature = "atomic"))] pub fn rotate_left(&mut self, mid: u64) -> io::Result<()> { assert!( @@ -759,7 +757,6 @@ impl<'a> BStackSlice<'a> { /// # Panics /// /// Panics if `k > self.len()`. - // TODO: lower memory usage for large slices #[cfg(all(feature = "set", feature = "atomic"))] pub fn rotate_right(&mut self, k: u64) -> io::Result<()> { assert!(k <= self.len(), "rotate_right: k must be <= slice length"); From 6e688e4baad214729114b4223a80232b19849100 Mon Sep 17 00:00:00 2001 From: williamwutq Date: Fri, 7 Aug 2026 16:17:00 -0700 Subject: [PATCH 8/9] Remove PLANNED.md entry --- PLANNED.md | 54 ------------------------------------------------------ 1 file changed, 54 deletions(-) diff --git a/PLANNED.md b/PLANNED.md index dcccc3f..69d3f2e 100644 --- a/PLANNED.md +++ b/PLANNED.md @@ -123,60 +123,6 @@ The mutex's scope and implementation are not in question here (see the `NOT PLAN - **Validation.** Whether `mixed/uniform` workload is sufficient to measure improvement, or whether a contention-specific microbenchmark (concurrent non-tail alloc/dealloc only) is needed to isolate the critical-section-size effect. -## Additional slice and vector APIs for ergonomics and compatibility - -**Feature flag:** `set` (for write operations) or no flag (for read operations) -**Breaking change:** No — purely additive. - -### Motivation - -`BStackSlice` and `BStackByteVec` currently provide fundamental I/O operations (`read`, `write`, `zero`, `subslice`) and basic vector operations (`push`, `pop`, `extend_from_slice`), but lack many ergonomic methods present in Rust's standard slice and vector types. While the existing API is sufficient for all operations, callers must manually compose primitives for common patterns, reducing productivity and readability. Adding methods that mirror `std` slice/vector APIs improves ergonomics, clarifies intent, and eases migration for code already familiar with Rust's conventions. - -### Proposed additions - -All methods follow crash-safety principles: read operations are side-effect-free, and write operations sync durably before returning. All write operations are **crash-atomic** — either fully persisted or not started, per bstack's core guarantee. - -#### `BStackSlice` — read-only query methods (no feature flag) - -- **`get(&self, index: u64) -> io::Result>`** — Read the byte at `index`, or `None` if out of bounds. (Note: `BStackByteVec` already has this.) -- **`head(&self, n: u64) -> BStackSlice<'a>`** — Return a sub-slice of the first `n` bytes. Returns a slice of length `min(n, self.len())`. Convenient for `slice.head(100)` instead of `slice.subslice(0, n.min(slice.len()))`. -- **`tail(&self, n: u64) -> BStackSlice<'a>`** — Return a sub-slice of the last `n` bytes. Returns a slice of length `min(n, self.len())`, starting at `max(0, self.len() - n)`. -- **`contains(&self, needle: u8) -> io::Result`** — Returns `true` if the slice contains `needle`. -- **`starts_with(&self, prefix: &[u8]) -> io::Result`** — Returns `true` if the slice begins with `prefix`. -- **`ends_with(&self, suffix: &[u8]) -> io::Result`** — Returns `true` if the slice ends with `suffix`. -- **`find(&self, needle: u8) -> io::Result>`** — Returns the index of the first occurrence of `needle`, or `None` if not found. -- **`rfind(&self, needle: u8) -> io::Result>`** — Returns the index of the last occurrence of `needle`, or `None` if not found. -- **`position(&self, predicate: impl Fn(u8) -> bool) -> io::Result>`** — Returns the index of the first byte satisfying `predicate`, or `None`. -- **`rposition(&self, predicate: impl Fn(u8) -> bool) -> io::Result>`** — Reverse variant of `position`. - -#### `BStackSlice` — write methods (requires `set` feature) - -- **`fill(&mut self, value: u8) -> io::Result<()>`** — Fill the entire slice with `value`. (Note: `BStackByteVec` already has this.) -- **`fill_with(&mut self, f: impl FnMut() -> u8) -> io::Result<()>`** — Fill the slice by calling `f` repeatedly. -- **`copy_from_slice(&mut self, src: &[u8]) -> io::Result<()>`** — Copy `src` into this slice. Panics if `src.len() != self.len()`. -- **`copy_from_bstack_slice(&mut self, src: &BStackSlice<'_>) -> io::Result<()>`** — Copy from another `BStackSlice` into this slice. Panics if lengths differ. Requires `atomic` feature. Complements `BStackByteVec::copy_into_bstack_slice` and `extend_from_bstack_slice`. -- **`copy_within(&mut self, src_range: Range, dest: u64) -> io::Result<()>`** — Copy `src_range` within this slice to `dest`. Requires `atomic` feature. -- **`swap(&mut self, other: &mut BStackSlice<'_>) -> io::Result<()>`** — Swap the contents of this slice with `other`. Panics if lengths differ. Requires `atomic` feature. -- **`reverse(&mut self) -> io::Result<()>`** — Reverse the byte order in place. Requires `atomic` feature. -- **`rotate_left(&mut self, mid: u64) -> io::Result<()>`** — Rotate bytes left by `mid` positions. Requires `atomic` feature. -- **`rotate_right(&mut self, k: u64) -> io::Result<()>`** — Rotate bytes right by `k` positions. Requires `atomic` feature. -- **`split_at(&self, mid: u64) -> (BStackSlice<'a>, BStackSlice<'a>)`** — Split into two sub-slices at `mid`. Already achievable via `subslice`, but this follows `std` naming. Panics if `mid > len`. -- **`split_at_mut(&mut self, mid: u64) -> (BStackSlice<'a>, BStackSlice<'a>)`** — Mutable variant. Returns two independent sub-slices. Panics if `mid > len`. - -### Implementation notes - -- **Crash consistency:** All write operations are crash-atomic. Single-write operations (`fill`, `copy_from_slice`) use direct primitives. Multi-step operations (`swap`, `reverse`, `rotate`) require the `atomic` feature to ensure atomicity via `BStack::copy`, `inplace_gen`, or other crash-atomic primitives. -- **`BStackOwnedSlice`:** Methods for `BStackOwnedSlice` should be thin wrappers around the corresponding `BStackSlice` methods via `as_slice()` and `as_slice_mut()`, since owned slices should not perform I/O directly. -- **`BStackByteVec` unchanged:** `BStackByteVec` already has a comprehensive API. No changes proposed. - -### Open questions - -- **Iterator API.** Whether `BStackSlice::iter()` is worth adding, or whether `read()` followed by in-memory iteration is sufficient. -- **Windows.** Whether to add a `windows` iterator to enable sliding-window patterns. -- **Comparison operators.** Whether to implement `PartialEq<[u8]>` for `BStackSlice` and `BStackByteVec`. - -Chunking and sorting are addressed separately below. - ## Chunked slice view, sorting, search, and selection **Feature flag:** `set` (mutating chunk operations: sort/select) or no flag (read-only chunking, search); chunk movement additionally requires `atomic`, per the `BStackSlice::swap`/`rotate_*`/`copy_within` primitives proposed above. From dc48e36a31e7bb8957f5a8c77d36d1dceb068f2f Mon Sep 17 00:00:00 2001 From: williamwutq Date: Fri, 7 Aug 2026 16:18:25 -0700 Subject: [PATCH 9/9] Fmt --- src/alloc/vec.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/alloc/vec.rs b/src/alloc/vec.rs index 50db14c..bc07b7b 100644 --- a/src/alloc/vec.rs +++ b/src/alloc/vec.rs @@ -1649,5 +1649,4 @@ mod tests { assert_eq!(v.move_tail_into(&mut too_big).unwrap(), None); assert_eq!(v.read_bytes().unwrap(), [1, 2, 3]); } - }