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}