Skip to content

← Unsafe and Soundness step 22 of 24

Medium Primitives

Build a safe mutable-slice split

Implement a safe function that divides one mutable slice into two disjoint mutable slices, then use it to process segments of a vector.

pub fn split_at_mut<T>(v: &mut [T], mid: usize) -> (&mut [T], &mut [T])
pub fn split_sums(v: Vec<i64>, cuts: Vec<usize>) -> Vec<i64>

Split contract

split_at_mut returns the elements before mid and the elements from mid onward, in their original order. Both results borrow the original storage: changing an element through either result changes that element in v.

Accept every mid <= v.len(), including zero, the end and an empty input. Panic for a larger mid before constructing any invalid reference. Support arbitrary T, including non-Copy and zero-sized types, without cloning or allocating replacement storage.

Implement the split using the slice's raw pointer and std::slice::from_raw_parts_mut. Do not call the standard library's splitting methods to perform the split. Keep the public function safe and explain each unsafe block with a safety comment.

Segment contract

In split_sums:

  1. Discard cuts greater than the vector length, sort the remaining cuts and remove duplicates. Cuts are absolute positions in the original vector.
  2. Partition at those positions. There is one more segment than retained cuts, including empty segments at zero or the end.
  3. Number segments from zero. Negate the elements of odd-indexed segments in place, using your split_at_mut to obtain the segments.
  4. Return each segment's sum in order. An empty segment has sum zero.

Inputs are chosen so each negation and intermediate sum fits in i64. In particular, an odd segment will not require negating i64::MIN.

For v = [1, 2, 3, 4, 5]:

Cuts Result
[] [15]
[0] [0, -15]
[5] [15, 0]
[1, 2, 3] [1, -2, 3, -9]
[2, 2, 9, 5, 0] [0, -3, 12, 0]

With an empty vector, no cuts gives [0]; retaining a cut at zero gives [0, 0]. Other cuts are discarded.

Establish the unsafe preconditions

Let len = v.len() and ptr = v.as_mut_ptr(). Check mid <= len first. Construct the left slice from ptr with length mid, and the right slice from ptr.add(mid) with length len - mid.

The bounds check is one part of the safety argument. The valid input slice supplies aligned, non-null storage for initialized elements. The two outputs cover disjoint portions of that storage, and their lifetimes are tied to the input borrow by the function signature. Do not access the original slice in conflict with the returned mutable borrows.

Empty slices still need valid aligned, non-null pointers. At the end of a nonempty slice, the right pointer can be one past the final element. Zero-sized elements can share an address, so different pointer values are not the test for whether this split is valid. The lengths and logical element ranges matter.

Read the requirements for from_raw_parts_mut and pointer addition. A safety comment should justify those requirements, not merely repeat the function call.

Repair the starter

Its strict mid < len check rejects a valid empty right half, including the empty-slice case. Its right slice also starts at the wrong pointer. When both nonempty halves cover the same non-zero-sized elements, that creates invalid overlapping mutable borrows. Fix both issues before exercising the split.

Panicking on an invalid index is defined behavior, unlike constructing invalid references. A panic can unwind or abort depending on the build configuration; it is not always recoverable.

Walk the remainder

Keep the unprocessed suffix in rest and the previous absolute cut in prev. For each sorted cut bound, split rest at bound - prev. Process the left part, then continue with the right part. Finally process the remaining suffix.

A mutable reference is not Copy, but passing it to a function can create a reborrow. The loop works because each split provides disjoint parts and the next iteration continues with the unprocessed part. It does not rely on a rule that two variable names can never refer to related mutable borrows.

The Rustonomicon's borrow-splitting example explains why ordinary indexing does not establish the same relationship for the borrow checker.

What passing tests means

Compilation, test cases and Clippy with warnings denied are required. These checks do not prove an unsafe implementation sound. Review the generic split against its preconditions, including empty and zero-sized inputs. Miri can help detect invalid memory access in tests; it is not part of this exercise's runner and does not replace the safety argument.