Skip to main content

simulator_api/
slot_range.rs

1//! An inclusive slot range.
2//!
3//! Slot bounds travel in pairs through most of the pipeline. As two adjacent
4//! `u64` parameters nothing stops a caller transposing them, and the ordering
5//! invariant gets re-checked ad hoc (or not at all) at each layer. This carries
6//! both together and checks once, at construction.
7
8use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12/// An inclusive `[start, end]` slot range. Construct via [`SlotRange::new`],
13/// which rejects an inverted range, or [`SlotRange::new_unchecked`] where the
14/// ordering is already guaranteed by the caller.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
16pub struct SlotRange {
17    start: u64,
18    end: u64,
19}
20
21/// Why a pair of bounds isn't a valid range.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
23#[error("invalid range: start_slot {start} > end_slot {end}")]
24pub struct InvertedSlotRange {
25    pub start: u64,
26    pub end: u64,
27}
28
29impl SlotRange {
30    /// An inclusive range, rejecting `start > end`.
31    pub fn new(start: u64, end: u64) -> Result<Self, InvertedSlotRange> {
32        if start > end {
33            return Err(InvertedSlotRange { start, end });
34        }
35        Ok(Self { start, end })
36    }
37
38    /// A range whose ordering the caller already guarantees — e.g. one derived
39    /// from a `start` plus a non-zero count. Prefer [`SlotRange::new`].
40    pub const fn new_unchecked(start: u64, end: u64) -> Self {
41        Self { start, end }
42    }
43
44    /// The range covering `count` slots from `start`. `count` of 0 yields a
45    /// single-slot range, matching how the callers' slot counts behave.
46    pub const fn from_count(start: u64, count: u64) -> Self {
47        Self {
48            start,
49            end: start.saturating_add(count.saturating_sub(1)),
50        }
51    }
52
53    pub const fn start(&self) -> u64 {
54        self.start
55    }
56
57    pub const fn end(&self) -> u64 {
58        self.end
59    }
60
61    /// Number of slots covered, inclusive of both bounds.
62    pub const fn len(&self) -> u64 {
63        self.end - self.start + 1
64    }
65
66    /// Never true — an inclusive range always covers at least one slot. Present
67    /// so `len()` doesn't trip `clippy::len_without_is_empty`.
68    pub const fn is_empty(&self) -> bool {
69        false
70    }
71
72    pub const fn contains(&self, slot: u64) -> bool {
73        slot >= self.start && slot <= self.end
74    }
75}
76
77impl fmt::Display for SlotRange {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}..={}", self.start, self.end)
80    }
81}
82
83impl From<SlotRange> for std::ops::RangeInclusive<u64> {
84    fn from(range: SlotRange) -> Self {
85        range.start..=range.end
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn an_inverted_range_is_rejected() {
95        assert_eq!(
96            SlotRange::new(10, 9),
97            Err(InvertedSlotRange { start: 10, end: 9 })
98        );
99        assert!(SlotRange::new(10, 10).is_ok());
100    }
101
102    #[test]
103    fn length_is_inclusive_of_both_bounds() {
104        assert_eq!(SlotRange::new(10, 10).expect("range").len(), 1);
105        assert_eq!(SlotRange::new(10, 19).expect("range").len(), 10);
106    }
107
108    #[test]
109    fn from_count_matches_the_start_plus_count_convention() {
110        assert_eq!(
111            SlotRange::from_count(100, 1),
112            SlotRange::new_unchecked(100, 100)
113        );
114        assert_eq!(
115            SlotRange::from_count(100, 10),
116            SlotRange::new_unchecked(100, 109)
117        );
118        // A zero count still covers the anchor slot rather than inverting.
119        assert_eq!(
120            SlotRange::from_count(100, 0),
121            SlotRange::new_unchecked(100, 100)
122        );
123    }
124
125    #[test]
126    fn contains_is_inclusive() {
127        let range = SlotRange::new(10, 12).expect("range");
128        assert!(range.contains(10) && range.contains(12));
129        assert!(!range.contains(9) && !range.contains(13));
130    }
131}