Skip to main content

praxis_runtime/
range.rs

1//! `Range` (§4.11, ADR-059).
2//!
3//! `a..b` is the integers from `a` up to but not including `b`; `a..=b` includes
4//! `b`. Both forms build the *same* payload — an inclusiveness flag would be a
5//! second way to spell one set of values, so the constructor normalizes `..=`
6//! into its half-open equivalent and the payload holds only what it iterates.
7//!
8//! A descending range is **empty**, not a countdown. `for i in 5..0` runs zero
9//! times, matching Python and Rust; a range that silently reversed direction
10//! would compute a different loop than the one that was written half the time it
11//! appeared. `RangeVal::new` is where that is decided, once: an `end` below
12//! `start` is stored *as* `start`, so `len` is a subtraction and cannot be
13//! negative.
14//!
15//! The payload is two plain `i64`s. Nothing is owned, nothing is traced, and the
16//! bounds are immutable once built — which is what makes a `Range` hashable and
17//! usable as a `Map` key (ADR-057 D4: the rule is mutability).
18
19use std::fmt::Write as _;
20
21use crate::descriptor::{
22    BuiltinTypeId, DynamicHasher, FormatSink, Payload, Tracer, TypeDescriptor,
23};
24
25/// The `Range` payload: a half-open `[start, end)` interval over `Int`.
26///
27/// The invariant `end >= start` is established by [`RangeVal::new`] and there is
28/// no other constructor and no mutator, so an "inverted" range — one whose
29/// length would be negative — is unrepresentable.
30#[repr(C)]
31#[derive(Clone, Copy, PartialEq, Eq, Debug)]
32pub struct RangeVal {
33    start: i64,
34    /// Exclusive. Always `>= start`.
35    end: i64,
36}
37
38impl RangeVal {
39    /// The half-open range `start..end`, normalizing a descending range to the
40    /// empty range at `start`.
41    #[must_use]
42    pub const fn new(start: i64, end: i64) -> RangeVal {
43        RangeVal {
44            start,
45            end: if end < start { start } else { end },
46        }
47    }
48
49    /// The inclusive range `start..=end`.
50    ///
51    /// `..=Int::MAX` is the one input whose half-open equivalent does not exist:
52    /// its exclusive end is `2^63`. It is **not** a fault — the range itself is
53    /// perfectly well defined — so it saturates, which loses nothing: the
54    /// element `Int::MAX` is still the last one, because there is no `Int` above
55    /// it to have excluded.
56    #[must_use]
57    pub const fn new_inclusive(start: i64, end: i64) -> RangeVal {
58        match end.checked_add(1) {
59            Some(exclusive) => RangeVal::new(start, exclusive),
60            None => RangeVal {
61                start,
62                end: i64::MAX,
63            },
64        }
65    }
66
67    /// The lower bound (inclusive).
68    #[must_use]
69    pub const fn start(&self) -> i64 {
70        self.start
71    }
72
73    /// The upper bound (exclusive).
74    #[must_use]
75    pub const fn end(&self) -> i64 {
76        self.end
77    }
78
79    /// How many integers the range contains.
80    ///
81    /// `end - start` in `i128`: the difference of two `i64`s does not fit an
82    /// `i64` (`0..Int::MAX` is fine, but `Int::MIN..Int::MAX` is `2^64 - 1`), and
83    /// a wrapping subtraction here would report a *negative* length for the
84    /// widest ranges — a `for` loop that ran zero times over every integer.
85    #[must_use]
86    pub const fn len(&self) -> i128 {
87        self.end as i128 - self.start as i128
88    }
89
90    /// Whether the range contains no integers.
91    #[must_use]
92    pub const fn is_empty(&self) -> bool {
93        self.end == self.start
94    }
95
96    /// The `index`-th integer, or `None` if `index` is outside the range.
97    ///
98    /// The addition is done in `i128` and checked on the way back, so a
99    /// nonsensical index cannot wrap into a value the range does not contain.
100    #[must_use]
101    pub fn get(&self, index: i64) -> Option<i64> {
102        if index < 0 || i128::from(index) >= self.len() {
103            return None;
104        }
105        i64::try_from(self.start as i128 + i128::from(index)).ok()
106    }
107}
108
109unsafe fn range_trace(_payload: *mut u8, _tracer: &mut dyn Tracer) {
110    // A Range holds two integers and no references; nothing to trace.
111}
112
113unsafe fn range_drop(_payload: *mut u8) {
114    // `RangeVal` is `Copy` and owns no heap bytes; nothing to release.
115}
116
117/// Render a range the way it was written. `..=` is *not* recovered: the payload
118/// is normalized, so `1..=4` and `1..5` are the same range and print the same —
119/// which is the point of normalizing.
120unsafe fn range_format(payload: *const u8, out: &mut FormatSink<'_>) {
121    // SAFETY: caller guarantees `payload` points at an initialized RangeVal.
122    let r = unsafe { &*(payload as *const RangeVal) };
123    let _ = write!(out, "{}..{}", r.start, r.end);
124}
125
126unsafe fn range_equals(a: *const u8, b: *const u8) -> bool {
127    // SAFETY: caller guarantees both pointers point at initialized RangeVals.
128    let ra = unsafe { &*(a as *const RangeVal) };
129    let rb = unsafe { &*(b as *const RangeVal) };
130    ra == rb
131}
132
133unsafe fn range_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
134    // SAFETY: caller guarantees `payload` points at an initialized RangeVal.
135    let r = unsafe { &*(payload as *const RangeVal) };
136    hasher.write_bytes(&r.start.to_le_bytes());
137    hasher.write_bytes(&r.end.to_le_bytes());
138}
139
140unsafe fn range_compare(a: *const u8, b: *const u8) -> std::cmp::Ordering {
141    // SAFETY: caller guarantees both pointers point at initialized RangeVals.
142    let ra = unsafe { &*(a as *const RangeVal) };
143    let rb = unsafe { &*(b as *const RangeVal) };
144    // Start then end, over the *normalized* payload — so `1..=4` and `1..5`
145    // compare `Equal` for the same reason `range_equals` calls them equal: they
146    // are one value, and an order that disagreed with equality would not be one.
147    (ra.start, ra.end).cmp(&(rb.start, rb.end))
148}
149
150/// Descriptor for `Range` (§4.11). Equatable and hashable over its two bounds,
151/// and orderable over them too — a `Range` can be a `Map` key (its bounds
152/// cannot change after it is stored), so a container has to be able to put one
153/// in a deterministic sequence (ADR-138). `a..b < c..d` in source is still
154/// Y006; that is `capability::supports_ord`'s question.
155pub static RANGE: TypeDescriptor = TypeDescriptor::builtin::<RangeVal>(
156    BuiltinTypeId::Range,
157    "Range",
158    range_trace,
159    range_drop,
160    range_format,
161    Some(range_equals),
162    Some(range_hash),
163    Some(range_compare),
164);
165
166/// `Range`'s payload handle: the two-`i64` value, not a scalar.
167pub static RANGE_PAYLOAD: Payload<RangeVal> = Payload::new(&RANGE);
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    /// A descending range is empty, not a countdown (ADR-059 D3). The decision
174    /// lives in the constructor, so there is no path that produces a range whose
175    /// length is negative — which is what a `for` loop reads.
176    #[test]
177    fn a_descending_range_is_empty_and_cannot_be_built_inverted() {
178        let down = RangeVal::new(5, 0);
179        assert!(down.is_empty());
180        assert_eq!(down.len(), 0);
181        assert_eq!(down.get(0), None);
182        // The normalization is to `start`, so the range is empty *at* 5 rather
183        // than being silently widened.
184        assert_eq!(down.start(), 5);
185        assert_eq!(down.end(), 5);
186
187        // …and every range's length is non-negative, including the widest one
188        // that exists, whose `i64` subtraction would wrap.
189        assert_eq!(RangeVal::new(i64::MIN, i64::MAX).len(), u64::MAX as i128);
190        assert!(RangeVal::new(i64::MAX, i64::MIN).is_empty());
191    }
192
193    /// `..` excludes its end and `..=` includes it — the only difference between
194    /// the two forms, and it is resolved at construction so nothing downstream
195    /// has to remember which was written.
196    #[test]
197    fn inclusive_and_half_open_differ_by_exactly_one_element() {
198        let half = RangeVal::new(1, 5);
199        let incl = RangeVal::new_inclusive(1, 5);
200        assert_eq!(half.len(), 4);
201        assert_eq!(incl.len(), 5);
202        assert_eq!(half.get(3), Some(4));
203        assert_eq!(half.get(4), None);
204        assert_eq!(incl.get(4), Some(5));
205        assert_eq!(incl.get(5), None);
206        // `1..=4` and `1..5` are the same range, which is what normalizing buys.
207        assert_eq!(RangeVal::new_inclusive(1, 4), half);
208        // An empty inclusive range is one whose end is below its start.
209        assert!(RangeVal::new_inclusive(5, 4).is_empty());
210        assert_eq!(RangeVal::new_inclusive(5, 5).len(), 1);
211    }
212
213    /// `..=Int::MAX` has no exclusive end inside `Int`. It saturates rather than
214    /// faulting, and the saturation loses no element: there is no `Int` above
215    /// `Int::MAX` that the range would otherwise have excluded.
216    #[test]
217    fn an_inclusive_range_to_the_last_int_keeps_that_int() {
218        let r = RangeVal::new_inclusive(i64::MAX - 2, i64::MAX);
219        assert_eq!(r.end(), i64::MAX);
220        assert_eq!(r.len(), 2);
221        assert_eq!(r.get(0), Some(i64::MAX - 2));
222        assert_eq!(r.get(1), Some(i64::MAX - 1));
223        // The last element is unreachable by index — the one value the
224        // saturation costs, and it costs it at the very top of the range rather
225        // than reporting a fault for a range the program legitimately wrote.
226        assert_eq!(r.get(2), None);
227    }
228
229    /// An index outside the range has no element, and the arithmetic that
230    /// answers so cannot wrap.
231    #[test]
232    fn an_out_of_range_index_has_no_element() {
233        let r = RangeVal::new(-3, 3);
234        assert_eq!(r.len(), 6);
235        assert_eq!(r.get(0), Some(-3));
236        assert_eq!(r.get(5), Some(2));
237        assert_eq!(r.get(6), None);
238        assert_eq!(r.get(-1), None);
239        assert_eq!(r.get(i64::MAX), None);
240    }
241
242    /// A `Range` is equatable and hashable over its bounds, which is what makes
243    /// it a legal `Map` key: the bounds cannot change after it is stored
244    /// (ADR-057 D4 — the rule is mutability, not container-ness).
245    #[test]
246    fn range_descriptor_reports_its_capabilities() {
247        assert!(RANGE.is_equatable() && RANGE.is_hashable());
248        assert_eq!(RANGE.name, "Range");
249        // Being a key is what makes the container order necessary (ADR-138).
250        assert!(RANGE.is_orderable());
251    }
252
253    /// The container order is start-then-end, and it agrees with equality —
254    /// including across the two spellings the constructor normalizes into one.
255    #[test]
256    fn range_compare_is_start_then_end_and_agrees_with_equality() {
257        let cmp = |a: &RangeVal, b: &RangeVal| unsafe {
258            range_compare((a as *const RangeVal).cast(), (b as *const RangeVal).cast())
259        };
260        let (a, b, c) = (
261            RangeVal::new(1, 4),
262            RangeVal::new(1, 5),
263            RangeVal::new(2, 3),
264        );
265        assert_eq!(cmp(&a, &b), std::cmp::Ordering::Less);
266        assert_eq!(cmp(&b, &c), std::cmp::Ordering::Less);
267        assert_eq!(cmp(&c, &a), std::cmp::Ordering::Greater);
268        // `1..=4` normalizes to `1..5` (ADR-059), so it is not a third value.
269        assert_eq!(
270            cmp(&RangeVal::new_inclusive(1, 4), &b),
271            std::cmp::Ordering::Equal
272        );
273    }
274}