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 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. 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]`. diff --git a/src/alloc/slice.rs b/src/alloc/slice.rs index f28fe79..b8fa5aa 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,203 @@ 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. + #[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()`. + #[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()`. + #[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. @@ -516,6 +841,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 { @@ -767,6 +1106,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 +1265,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) @@ -888,6 +1461,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 ccb3850..83a8921 100644 --- a/src/test.rs +++ b/src/test.rs @@ -2831,6 +2831,416 @@ 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); + } + + // ---- 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.