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
4 changes: 4 additions & 0 deletions manual/src/snippets/slices.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ assert_eq([30, 40, 50, 60], my_list[3..=6]);
// Negative indices: Counting from the end of the list
assert_eq([80, 90], my_list[-3..-1]);
```

A range whose start lands after its end (for example `my_list[6..3]`) is out of
bounds and raises an error rather than returning a reversed or empty slice. This
holds for lists, strings, tuples and deques alike.
19 changes: 19 additions & 0 deletions ndc_stdlib/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,14 @@ fn to_forward_index(i: i64, size: usize, for_slice: bool) -> Result<usize, VmErr
}
}

/// Resolves a (possibly negative) slice endpoint to a forward position without
/// clamping it into `0..=size`. Used purely to detect reversed ranges: clamping
/// would collapse out-of-range endpoints to the boundary (e.g. `6` and `5` both
/// becoming `size` on a 5-element string), masking a reversal like `6..=5`.
fn unclamped_slice_index(i: i64, size: usize) -> i64 {
if i < 0 { size as i64 + i } else { i }
}

enum VmOffset {
Element(usize),
Range(usize, usize),
Expand All @@ -223,6 +231,17 @@ fn extract_vm_offset(index_value: &Value, size: usize) -> Result<VmOffset, VmErr
{
let iter_ref = iter.borrow();
if let Some((start, end, inclusive)) = iter_ref.range_bounds() {
// Detect reversal from the unclamped endpoints, so an out-of-range
// reversed range can't collapse to equality once clamped (e.g.
// `6..=5` on a 5-element string would otherwise clamp both ends to
// 5 and silently yield an empty slice instead of an error).
let from_unclamped = unclamped_slice_index(start, size);
let end_unclamped = unclamped_slice_index(end, size);
if end_unclamped < from_unclamped {
return Err(VmError::native(format!(
"{from_unclamped}..{end_unclamped} out of bounds"
)));
}
let from_idx = to_forward_index(start, size, true)?;
let to_idx = to_forward_index(end, size, true)?;
let to_idx = if inclusive {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// expect-error: 3..1 out of bounds
// A reversed range on a string used to panic with "attempt to subtract with
// overflow"; it must now error gracefully like list/tuple slices do.
"hello"[3..1]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// expect-error: 1..0 out of bounds
// A reversed range on a deque used to panic with "attempt to subtract with
// overflow"; it must now error gracefully.
let d = Deque();
d.push_back(1);
d.push_back(2);
d.push_back(3);
d[1..0]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// expect-error: 3..1 out of bounds
// Assigning into a reversed slice used to panic with "attempt to subtract with
// overflow"; it must now error gracefully.
let x = [1, 2, 3, 4];
x[3..1] = [9]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// expect-error: 2..1 out of bounds
// An inclusive range whose start is after its end (off-by-one, so the `+1`
// widening would otherwise mask it) must error, not return an empty slice.
"hello"[2..=1]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// expect-error: 6..5 out of bounds
// A reversed range whose endpoints are both at or beyond the length used to
// clamp to equal indices and return an empty slice. Reversal is now detected
// from the unclamped endpoints, so it errors.
"hello"[6..=5]
Loading