RFA-664 · Case file with fixtures · Case 636 of 694 · Runtime evidence
rotate_left Takes a Distance No Larger Than the Slice
slice rotation validates mid against length. Apply modulo deliberately for cyclic distances and handle the empty-slice case before dividing.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- rotate_left accepts a structural split boundary no greater than length, not an arbitrary cyclic distance normalised by the API.
- First discriminating check
- Define cyclic semantics, handle an empty slice, and reduce an arbitrary distance modulo the current length before rotating.
slice.rotate_left(mid) expects mid <= slice.len(). It does not reduce an arbitrary distance modulo the length. The failing fixture rotates three bytes by four and panics at runtime.
mid is a split position
The rotate_left documentation describes the operation as moving the first mid elements to the end. Seen this way, mid is a boundary inside the slice, not an unrestricted number of turns.
For [1, 2, 3], rotating left at boundary one produces [2, 3, 1]. Boundary zero and boundary length leave the sequence unchanged. Boundary four does not exist, so the method rejects it.
I use the word “boundary” in review because it makes the precondition easier to remember. The API accepts every valid split point from zero through length inclusive.
Cyclic input needs normalisation
Some domains naturally produce arbitrary rotation distances: ring buffers, calendar offsets, sharding epochs, card simulations, and wrapped cursors. In those domains I normalise before calling the slice method.
The repaired fixture computes distance % bytes.len() and rotates by the result. Four positions on a length-three slice becomes one. That modulo is domain logic; putting it at the call site shows that wrapping is intended.
An empty slice needs care because remainder by zero panics. A robust helper returns early for empty input or uses checked_rem. I do not hide this case behind an apparently safe normalisation formula.
Left and right express direction, not validation policy
rotate_right has the corresponding length precondition. Converting a large right rotation to len - distance is unsafe until the distance is normalised, and subtraction can underflow.
For a non-empty slice, I first calculate k = distance % len. A right rotation uses rotate_right(k). If an algorithm must translate it into a left rotation, (len - k) % len handles k == 0; a direct len - k is also valid once k <= len, but the chosen formula should remain clear.
Signed offsets need another explicit choice. Rust's rem_euclid is often more suitable than % when negative distances should wrap mathematically. I convert to an unsigned boundary only after defining what a negative value means.
Rotation preserves elements and length
Rotation rearranges values in place. It does not allocate a new vector, change length, clone elements, or create a logical gap. For non-Copy resources, ownership remains inside the slice while positions change.
Position changes can still affect application identity. If another table says “worker 8 is at slot 1,” rotation makes that index stale. As with swap removal, safe Rust cannot update external numeric indices. I update index metadata or avoid exposing positions as stable handles.
Rotation also differs from shifting. A left shift may discard leading values and fill the tail; rotation preserves and wraps them. Naming helpers after the domain action prevents these operations from being confused.
Test algebra, not only examples
Example tests cover zero, one, length, greater-than-length normalisation, and empty input. Property tests can state stronger facts: length and multiset are preserved; rotating left by k and right by k restores the original; rotating by a multiple of length is identity for non-empty slices.
Those properties help expose off-by-one and direction mistakes. They also document whether the public helper accepts arbitrary distances or only valid boundaries. If the public contract accepts arbitrary values, the raw slice panic should never escape.
My rotation checklist
- Is the input a valid split boundary or an arbitrary cyclic distance?
- Who normalises values larger than the slice length?
- What does empty input mean before modulo is calculated?
- Are negative offsets allowed, and how do they wrap?
- Is direction defined from the data or observer's perspective?
- Do external indices need updating after positions move?
- Does the algorithm require rotation, or actually shifting and filling?
- Do tests cover inverse and identity properties?
The core principle is that low-level collection APIs often accept structural coordinates, while domain APIs accept broader values. rotate_left takes a valid slice boundary. I normalise cyclic input at the domain edge and handle emptiness explicitly before using that structural operation.