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

RFA-181 · Case file with fixtures · Case 153 of 694 · Runtime evidence

Why VecDeque::as_slices Can Return Two Non-Empty Slices

VecDeque has one logical order over ring-buffer storage that may wrap into two physical regions. Consume both slices in order or call make_contiguous before requiring one slice.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all targets with alloc
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Logical deque order is contiguous but its ring-buffer representation may cross the allocation boundary at an implementation-defined split point.
First discriminating check
Compare deque length with the combined slice lengths and call make_contiguous before requiring one physical slice.

VecDeque looks like one sequence when I iterate it, index it, or compare it. Its physical storage does not always look like one sequence.

The failing program fills a capacity-four deque, removes two items from the front, and adds two at the back. Logical order is [3, 4, 5, 6], but as_slices() returns data across two non-empty slices on Rust 1.98.1.

A ring can cross the allocation boundary

A deque needs cheap work at both ends. Instead of moving all remaining elements after every front removal, a ring buffer advances the logical start position. Later insertions can use space at the physical beginning of the allocation.

The shape can be pictured as:

physical storage: [5, 6, 3, 4]
logical order:          3, 4, 5, 6
first slice:            [3, 4]
second slice:  [5, 6]

The exact split is an implementation detail, but the pair returned by VecDeque::as_slices always contains the complete logical order: first slice followed by second slice.

Do not silently ignore the second slice

Code written for Vec<T> sometimes accepts only one &[T]. Taking the first component from as_slices can process only a prefix of the deque without an error.

If the consumer supports scatter/gather input, I pass both slices. If it accepts an iterator, deque.iter() hides the physical split. If it requires one contiguous region, I make that transformation explicit.

The invariant to test is:

let (left, right) = deque.as_slices();
assert_eq!(left.len() + right.len(), deque.len());

Whether right is empty is not an invariant until contiguity has been requested.

make_contiguous performs the layout change

The repaired program calls VecDeque::make_contiguous. It rearranges the ring so every element is in one slice, preserves logical order, and does not allocate.

The returned mutable slice can be passed directly to a contiguous consumer or sorted in place. Afterwards, as_slices returns all elements in the first slice and an empty second slice.

“Does not allocate” does not mean free. Elements may need to move within the allocation. Calling it before every small operation can discard the performance reason for choosing a deque.

FFI and vectored I/O expose the distinction

A C function expecting one pointer and one length needs contiguous memory for the duration of the call. I can call make_contiguous, copy into a separate buffer, or choose a container whose representation already matches the boundary.

Some I/O interfaces accept multiple buffers. There, keeping both deque slices can avoid rearrangement. The order remains important: the second physical slice follows the first logically.

I decide based on the consumer contract instead of normalising the deque reflexively.

Slice addresses are not stable across mutation

Even after making storage contiguous, later pushes, pops, reservations, or another make_contiguous can change positions. Rust's borrowing rules prevent safe mutation while borrowed slices remain live, but raw pointers passed across FFI need a carefully bounded lifetime.

I do not cache a pointer and assume the deque remains in the same layout after the borrow ends. Logical indexes are more stable as concepts, though insertions and removals can also change what an index identifies.

Testing only a newly created deque misses wrapping

A new deque filled once often has an empty second slice. That happy path can make incomplete consumer code look correct.

My tests force the head forward, refill through the back, and verify the combined slices. I do not assert an exact split point unless the fixture is explicitly measuring one Rust version's implementation.

For application tests, I care that both representations produce identical output: naturally contiguous and deliberately wrapped.

My deque boundary checklist

When a deque seems to lose items at an integration boundary, I check:

  1. Did the consumer receive both slices?
  2. Does it require physical contiguity or only logical order?
  3. Would iteration or vectored input avoid rearranging storage?
  4. How often is make_contiguous called, and what movement does it cause?
  5. Are raw pointers kept past a mutation?
  6. Do tests construct a genuinely wrapped deque?

The broad principle is that logical shape and physical representation are different contracts. VecDeque promises sequence behaviour. as_slices honestly exposes that the sequence may occupy two regions, and make_contiguous is the explicit bridge when one region is required.