Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-699 · Case file with fixtures · Case 671 of 694 · Runtime evidence

slice::subslice_range Tracks Origin, Not Equal Values

Rust 1.98 subslice_range recovers the indices of a view borrowed from one parent slice. It uses location and alignment, not element equality, so an equal copy is not a subslice of that allocation.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all targets supported by the API
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The Rust 1.98 API recovers where a borrowed view came from by address and alignment; it does not search the parent slice for equal elements.
First discriminating check
Check whether the argument was sliced from the same allocation, and use windows plus position when the requirement is value search instead of origin recovery.

Rust 1.98 added a small slice method which answers a very specific question: where did this borrowed subslice come from? It does not answer the more familiar question, “where do these values occur?” Confusing the two makes a correct None look like a library bug.

The failing fixture uses these values:

use core::range::Range;

let owner = [10_u8, 20, 30, 40];
let equal_copy = [20_u8, 30];

assert_eq!(
    owner.subslice_range(&equal_copy),
    Some(Range { start: 1, end: 3 }),
);

The two middle values are equal, but equal_copy is a different array in a different piece of storage. subslice_range returns None, and the assertion panics. No element comparison failed because no element comparison happened.

The method reverses a borrow

A slice is a pointer and a length. When I create &owner[1..3], the returned slice points inside owner. subslice_range can compare locations, check alignment, and translate that derived view back into element indices.

The repaired fixture keeps that relationship:

use core::range::Range;

let owner = [10_u8, 20, 30, 40];
let derived = &owner[1..3];

assert_eq!(
    owner.subslice_range(derived),
    Some(Range { start: 1, end: 3 }),
);

This is useful after APIs such as split, because every yielded part is still a view into the original slice. I can attach offsets to those parts without rescanning values and without doing unsafe pointer subtraction myself.

Equal content is a search problem

If the input is a separate sequence and I want the first equal occurrence, I use a value-search operation:

let wanted = [20_u8, 30];
let position = owner
    .windows(wanted.len())
    .position(|window| window == wanted);

assert_eq!(position, Some(1));

That loop has different semantics and different cost. It compares elements. It may also find several occurrences, so “first,” “last,” or “all” must be part of the calling contract.

I do not replace subslice_range with windows as a generic fix. I first decide which question the program needs:

  • Origin recovery asks whether this exact view belongs to this exact parent.
  • Value search asks whether an equal sequence occurs somewhere in the parent.

The same-looking values do not make these contracts interchangeable.

The returned range is the new range type

On Rust 1.98, the method returns Option<core::range::Range<usize>>. This is not the legacy std::ops::Range type people usually get from the 1..3 syntax today. If I annotate the expected value with the old type, I get a type mismatch even though both debug as 1..3.

That secondary surprise is useful during migration. I let inference work when I only inspect start and end, or I import core::range::Range explicitly when the type belongs in an interface. I do not convert types blindly just because their printed form matches.

Empty slices need care

The official documentation calls out an edge case for zero-length slices. Empty slices have no elements whose addresses can establish a unique origin, and an empty view at a boundary may produce a false positive involving another allocation.

If empty pieces matter to my parser, I preserve context from the operation that created them. For example, I enumerate split results or carry the cursor position rather than asking an empty pointer to recover history it may not uniquely encode.

For non-empty views of non-zero-sized elements, the origin model is much easier to reason about. The API also rejects a view which points between elements rather than at the parent's element alignment.

Where I would use it

The strongest use is extending an iterator which already returns borrowed pieces:

let packet = [0, 7, 8, 0, 9];

let fields: Vec<_> = packet
    .split(|byte| *byte == 0)
    .map(|field| packet.subslice_range(field).unwrap())
    .collect();

Now the offsets describe the actual views produced by split, including repeated values. A content search could select the wrong repeated occurrence; origin recovery cannot confuse two non-empty views merely because they compare equal.

In production code I test repeated sequences, a view at each boundary, an unrelated equal allocation, and the empty-view policy. This makes the intention visible. subslice_range is not a faster find. It is the safe inverse of deriving a slice, and that narrower promise is exactly why it can avoid scanning.

I also keep zero-sized element types out of this use. Their elements have no distinct storage offsets, and the documented method panics for them. A cursor carried by the parser is the clearer coordinate in that case.