Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 0 additions & 54 deletions PLANNED.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<u8>>`** — 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<bool>`** — Returns `true` if the slice contains `needle`.
- **`starts_with(&self, prefix: &[u8]) -> io::Result<bool>`** — Returns `true` if the slice begins with `prefix`.
- **`ends_with(&self, suffix: &[u8]) -> io::Result<bool>`** — Returns `true` if the slice ends with `suffix`.
- **`find(&self, needle: u8) -> io::Result<Option<u64>>`** — Returns the index of the first occurrence of `needle`, or `None` if not found.
- **`rfind(&self, needle: u8) -> io::Result<Option<u64>>`** — Returns the index of the last occurrence of `needle`, or `None` if not found.
- **`position(&self, predicate: impl Fn(u8) -> bool) -> io::Result<Option<u64>>`** — Returns the index of the first byte satisfying `predicate`, or `None`.
- **`rposition(&self, predicate: impl Fn(u8) -> bool) -> io::Result<Option<u64>>`** — 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<u64>, 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.
Expand Down
61 changes: 43 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>` |
| `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<u8>` |
| `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`

Expand All @@ -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>`/`[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.
Expand Down Expand Up @@ -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]`.
Expand Down
Loading
Loading