Skip to main content

kohebi_core/
slice.rs

1//! `slice`, which is what a subscript with a colon in it builds.
2//!
3//! A slice holds three objects rather than three integers. `x['a':'b']` builds
4//! one without complaint and only raises when a sequence tries to use it, which
5//! is why the check lives in [`Slice::indices`] rather than in the constructor.
6//!
7//! [`Slice::indices`] is CPython's `PySlice_Unpack` followed by
8//! `PySlice_AdjustIndices`, and the reason to follow it that closely is the
9//! clamping. Every out of range bound in Python is quietly pulled back to the
10//! end of the sequence rather than raising, so `x[5:100]` on a list of three is
11//! `[]` and `x[-100:]` is the whole list, and an index far larger than the
12//! machine can hold is pulled back too: `x[2**100:]` is `[]` rather than an
13//! `OverflowError`. Getting that wrong produces an exception where a program
14//! expected an empty list, which is the sort of difference that only shows up
15//! on somebody else's input.
16
17// `stop` and `step` are the names Python gives these two, and a reader coming
18// from `slice(start, stop, step)` will look for exactly those. Renaming one to
19// please the lint would cost more than the lint is worth here.
20#![expect(clippy::similar_names, reason = "stop and step are Python's names")]
21
22use crate::error::{Error, Result};
23use crate::int::Int;
24use crate::object::Object;
25
26/// `slice(start, stop, step)`, holding whatever it was given.
27#[derive(Debug, Clone)]
28pub struct Slice {
29    /// Where to start, or `None` for the near end.
30    pub start: Object,
31    /// Where to stop, or `None` for the far end.
32    pub stop: Object,
33    /// How far to move each time, or `None` for one.
34    pub step: Object,
35}
36
37/// A slice resolved against the length of a particular sequence.
38///
39/// Signed, because a slice walking backwards stops just before the front of the
40/// sequence and there is no unsigned way to say that.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct Indices {
43    /// The first offset, which is not visited when [`Indices::len`] is zero.
44    pub start: isize,
45    /// One past the last offset, in the direction of travel.
46    pub stop: isize,
47    /// How far to move each time. Never zero, negative when walking backwards.
48    pub step: isize,
49    /// How many elements this selects, which is the length of the result.
50    pub len: usize,
51}
52
53impl Indices {
54    /// The offsets this selects, in order.
55    pub fn offsets(self) -> impl Iterator<Item = usize> {
56        // Every offset is inside the sequence by construction, so the cast back
57        // is always in range.
58        (0..self.len).map(move |i| {
59            let step = self.step.saturating_mul(i.cast_signed());
60            self.start.saturating_add(step).cast_unsigned()
61        })
62    }
63
64    /// Whether this is a plain `a:b` with no step, which is the case a sequence
65    /// can serve as one contiguous run.
66    #[must_use]
67    pub const fn is_contiguous(self) -> bool {
68        self.step == 1
69    }
70}
71
72impl Slice {
73    /// A slice from three values, any of which may be `None`.
74    #[must_use]
75    pub const fn new(start: Object, stop: Object, step: Object) -> Self {
76        Slice { start, stop, step }
77    }
78
79    /// What `repr` prints, which names all three parts even when none were
80    /// written down.
81    #[must_use]
82    pub fn repr(&self) -> String {
83        format!(
84            "slice({}, {}, {})",
85            self.start.repr(),
86            self.stop.repr(),
87            self.step.repr()
88        )
89    }
90
91    /// The three parts, which is what equality and hashing are defined on.
92    #[must_use]
93    pub const fn parts(&self) -> [&Object; 3] {
94        [&self.start, &self.stop, &self.step]
95    }
96
97    /// This slice against a sequence of `len` elements.
98    ///
99    /// # Errors
100    ///
101    /// A step of zero, or a bound that is neither an integer nor `None`.
102    pub fn indices(&self, len: usize) -> Result<Indices> {
103        let step = bound(&self.step)?.unwrap_or(1);
104        if step == 0 {
105            return Err(Error::value_error("slice step cannot be zero"));
106        }
107        // A sequence never has more than `isize::MAX` elements, so this is the
108        // whole range and the cast cannot wrap.
109        let len = len.cast_signed();
110        let backwards = step < 0;
111
112        let start = match bound(&self.start)? {
113            Some(start) => clamp(start, len, backwards),
114            None if backwards => len - 1,
115            None => 0,
116        };
117        let stop = match bound(&self.stop)? {
118            Some(stop) => clamp(stop, len, backwards),
119            None if backwards => -1,
120            None => len,
121        };
122
123        // How many steps fit between the two, which is zero when the slice runs
124        // the wrong way rather than a negative count.
125        let span = if backwards {
126            start - stop
127        } else {
128            stop - start
129        };
130        let count = if span > 0 {
131            ((span - 1) / step.abs()) + 1
132        } else {
133            0
134        };
135
136        Ok(Indices {
137            start,
138            stop,
139            step,
140            len: count.cast_unsigned(),
141        })
142    }
143}
144
145/// One bound pulled inside the sequence, the way CPython pulls it.
146///
147/// The same rule serves `start` and `stop`, which is worth saying because it
148/// looks as though it should not: a `start` past the end and a `stop` past the
149/// end both land on the end, and the direction of travel decides which end that
150/// is. The two agreeing is what makes an empty slice come out empty from either
151/// side.
152const fn clamp(value: isize, len: isize, backwards: bool) -> isize {
153    if value < 0 {
154        let shifted = value.saturating_add(len);
155        // Off the front. Walking backwards, that is one before the first
156        // element, which is exactly where a backwards walk stops.
157        return if shifted < 0 {
158            if backwards { -1 } else { 0 }
159        } else {
160            shifted
161        };
162    }
163    if value >= len {
164        // Off the back, so begin at the last element or stop past it.
165        return if backwards { len - 1 } else { len };
166    }
167    value
168}
169
170/// One part of a slice as a machine offset, or `None` when it was not given.
171///
172/// A number too large for the machine is clamped rather than refused, which is
173/// what makes `x[2**100:]` an empty list. That is the one place a slice bound
174/// and an ordinary index disagree: `x[2**100]` is an `IndexError`.
175fn bound(value: &Object) -> Result<Option<isize>> {
176    match value {
177        Object::None => Ok(None),
178        Object::Bool(value) => Ok(Some(isize::from(*value))),
179        Object::Int(Int::Small(value)) => {
180            Ok(Some(isize::try_from(*value).unwrap_or(if *value < 0 {
181                isize::MIN
182            } else {
183                isize::MAX
184            })))
185        }
186        Object::Int(big @ Int::Big(_)) => Ok(Some(if big.is_negative() {
187            isize::MIN
188        } else {
189            isize::MAX
190        })),
191        _ => Err(Error::type_error(
192            "slice indices must be integers or None or have an __index__ method",
193        )),
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    /// A slice of plain integers, which is all these tests need.
202    fn slice(start: Option<i64>, stop: Option<i64>, step: Option<i64>) -> Slice {
203        let part = |value: Option<i64>| value.map_or(Object::None, Object::int);
204        Slice::new(part(start), part(stop), part(step))
205    }
206
207    /// The offsets a slice selects against a sequence of `len`, which is the
208    /// only thing a caller ever wants out of [`Slice::indices`].
209    fn walk(start: Option<i64>, stop: Option<i64>, step: Option<i64>, len: usize) -> Vec<usize> {
210        slice(start, stop, step)
211            .indices(len)
212            .expect("these are all integers")
213            .offsets()
214            .collect()
215    }
216
217    #[test]
218    fn a_slice_with_nothing_written_down_is_the_whole_sequence() {
219        assert_eq!(walk(None, None, None, 3), [0, 1, 2]);
220        assert_eq!(walk(None, None, None, 0), []);
221    }
222
223    #[test]
224    fn a_bound_past_the_end_is_pulled_back_to_the_end() {
225        assert_eq!(walk(Some(5), Some(100), None, 3), []);
226        assert_eq!(walk(Some(1), Some(100), None, 3), [1, 2]);
227        assert_eq!(walk(Some(-100), None, None, 3), [0, 1, 2]);
228    }
229
230    #[test]
231    fn a_bound_too_big_for_the_machine_is_pulled_back_as_well() {
232        // The case that would be an `OverflowError` if the clamp went missing.
233        let huge = Object::Int(Int::Small(1).shl(&Int::Small(100)).expect("2 ** 100"));
234        let far = Slice::new(huge.clone(), Object::None, Object::None);
235        assert_eq!(far.indices(3).expect("clamped").len, 0);
236        let near = Slice::new(Object::None, huge, Object::None);
237        assert_eq!(near.indices(3).expect("clamped").len, 3);
238    }
239
240    #[test]
241    fn a_negative_step_walks_backwards_and_stops_before_the_front() {
242        assert_eq!(walk(None, None, Some(-1), 3), [2, 1, 0]);
243        assert_eq!(walk(Some(100), None, Some(-1), 3), [2, 1, 0]);
244        assert_eq!(walk(None, Some(0), Some(-1), 3), [2, 1]);
245    }
246
247    #[test]
248    fn a_step_skips_and_the_count_rounds_up() {
249        assert_eq!(walk(None, None, Some(2), 5), [0, 2, 4]);
250        assert_eq!(walk(None, None, Some(3), 5), [0, 3]);
251        assert_eq!(walk(None, None, Some(-2), 5), [4, 2, 0]);
252    }
253
254    #[test]
255    fn a_slice_that_runs_the_wrong_way_is_empty_rather_than_negative() {
256        assert_eq!(walk(Some(3), Some(1), None, 5), []);
257        assert_eq!(walk(Some(1), Some(3), Some(-1), 5), []);
258    }
259
260    #[test]
261    fn only_a_plain_run_is_contiguous() {
262        let contiguous = |step| {
263            slice(None, None, step)
264                .indices(5)
265                .expect("integers")
266                .is_contiguous()
267        };
268        assert!(contiguous(None));
269        assert!(contiguous(Some(1)));
270        assert!(!contiguous(Some(2)));
271        assert!(!contiguous(Some(-1)));
272    }
273
274    #[test]
275    fn a_step_of_zero_and_a_bound_that_is_not_a_number_are_refused() {
276        let zero = slice(None, None, Some(0))
277            .indices(3)
278            .expect_err("zero step");
279        assert_eq!(zero.to_string(), "ValueError: slice step cannot be zero");
280        let text = Slice::new(Object::str("a"), Object::None, Object::None);
281        assert_eq!(
282            text.indices(3).expect_err("not a number").to_string(),
283            "TypeError: slice indices must be integers or None or have an __index__ method"
284        );
285    }
286
287    #[test]
288    fn a_bool_is_a_bound_because_it_is_an_int() {
289        let from_true = Slice::new(Object::Bool(true), Object::None, Object::None);
290        assert_eq!(from_true.indices(3).expect("a bool is an int").start, 1);
291    }
292
293    #[test]
294    fn repr_names_all_three_parts_even_when_none_were_written() {
295        assert_eq!(slice(None, None, None).repr(), "slice(None, None, None)");
296        assert_eq!(slice(Some(1), Some(2), Some(3)).repr(), "slice(1, 2, 3)");
297    }
298}