Skip to main content

rucc_base/
index.rs

1//! Typed 32-bit indices.
2//!
3//! The compiler stores its trees, its IR and its machine code in flat vectors and refers to
4//! elements by index rather than by pointer. A `u32` index is half the size of a pointer,
5//! it is stable across a reallocation of the backing vector, and it serialises without
6//! fixups, all of which matter at the sizes a translation unit reaches.
7//!
8//! The cost of a bare `u32` is that every index in the program has the same type, so a block
9//! number can be passed where an instruction number was wanted and the compiler will not
10//! object. `Idx<T>` is the fix: it is still a `u32` at runtime and it is a distinct type at
11//! compile time.
12
13use std::fmt;
14use std::marker::PhantomData;
15use std::num::NonZeroU32;
16
17/// A 32-bit index into a flat table of `T`.
18///
19/// `Idx<T>` is `Copy`, is exactly four bytes, and has a niche, so `Option<Idx<T>>` is also
20/// four bytes. Optional indices are everywhere in a compiler (no successor, no parent, no
21/// spill slot) and paying eight bytes for each of them adds up, so the value is stored
22/// biased by one over a `NonZeroU32`. That is where the limit of one below `u32::MAX` on the
23/// largest representable index comes from.
24pub struct Idx<T> {
25    raw: NonZeroU32,
26    _marker: PhantomData<fn() -> T>,
27}
28
29impl<T> Idx<T> {
30    /// The largest index that can be represented.
31    pub const MAX: u32 = u32::MAX - 1;
32
33    /// Wraps a raw index.
34    ///
35    /// # Panics
36    ///
37    /// Panics if `raw` exceeds [`Idx::MAX`]. A translation unit with four billion of
38    /// anything is not a translation unit we intend to compile, and the alternative to
39    /// panicking is silently truncating, which is worse.
40    #[inline]
41    pub const fn new(raw: u32) -> Self {
42        assert!(raw <= Self::MAX, "index out of range");
43        match NonZeroU32::new(raw + 1) {
44            Some(raw) => Self { raw, _marker: PhantomData },
45            None => unreachable!(),
46        }
47    }
48
49    /// Wraps a `usize`, which is what indexing a `Vec` gives back.
50    ///
51    /// # Panics
52    ///
53    /// Panics if the value exceeds [`Idx::MAX`].
54    #[inline]
55    pub fn from_usize(raw: usize) -> Self {
56        Self::new(u32::try_from(raw).expect("index out of range"))
57    }
58
59    /// The underlying `u32`.
60    #[inline]
61    pub const fn raw(self) -> u32 {
62        self.raw.get() - 1
63    }
64
65    /// The index as a `usize`, for slicing.
66    #[inline]
67    pub const fn index(self) -> usize {
68        self.raw() as usize
69    }
70}
71
72// The derives would all demand `T: Trait`, which is wrong here: an index is four bytes of
73// integer no matter what it points at, and requiring `T: Clone` to copy an index is a papercut
74// that shows up in every signature. So they are written out.
75impl<T> Clone for Idx<T> {
76    #[inline]
77    fn clone(&self) -> Self {
78        *self
79    }
80}
81
82impl<T> Copy for Idx<T> {}
83
84impl<T> PartialEq for Idx<T> {
85    #[inline]
86    fn eq(&self, other: &Self) -> bool {
87        self.raw == other.raw
88    }
89}
90
91impl<T> Eq for Idx<T> {}
92
93impl<T> PartialOrd for Idx<T> {
94    #[inline]
95    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
96        Some(self.cmp(other))
97    }
98}
99
100impl<T> Ord for Idx<T> {
101    #[inline]
102    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
103        self.raw.cmp(&other.raw)
104    }
105}
106
107impl<T> std::hash::Hash for Idx<T> {
108    #[inline]
109    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
110        self.raw.hash(state);
111    }
112}
113
114impl<T> fmt::Debug for Idx<T> {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        // The type name is worth the width: a dump full of bare integers is unreadable, and
117        // dumps are the primary debugging tool for a compiler.
118        let name = std::any::type_name::<T>();
119        let short = name.rsplit("::").next().unwrap_or(name);
120        write!(f, "{short}#{}", self.raw())
121    }
122}
123
124/// A contiguous half-open run of indices, `start .. end`.
125///
126/// Children of an AST node, arguments of a call and parameters of a block are all stored as
127/// runs in one flat vector, so the parent holds eight bytes instead of a `Vec`.
128pub struct IdxRange<T> {
129    start: u32,
130    end: u32,
131    _marker: PhantomData<fn() -> T>,
132}
133
134// Written out for the same reason as the ones on `Idx`: a range of indices is eight bytes of
135// integer whatever it points at, and a derive would demand `T: Clone` to copy one.
136impl<T> Clone for IdxRange<T> {
137    #[inline]
138    fn clone(&self) -> Self {
139        *self
140    }
141}
142
143impl<T> Copy for IdxRange<T> {}
144
145impl<T> PartialEq for IdxRange<T> {
146    #[inline]
147    fn eq(&self, other: &Self) -> bool {
148        self.start == other.start && self.end == other.end
149    }
150}
151
152impl<T> Eq for IdxRange<T> {}
153
154impl<T> fmt::Debug for IdxRange<T> {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        let name = std::any::type_name::<T>();
157        let short = name.rsplit("::").next().unwrap_or(name);
158        write!(f, "{short}#{}..{}", self.start, self.end)
159    }
160}
161
162impl<T> IdxRange<T> {
163    /// The empty range at the start of the table.
164    ///
165    /// Every table has a start, so this is valid whatever the table holds and whether or not
166    /// anything has been put in it yet. It is what a node holds when the thing it points at is
167    /// a list that happens to have nothing in it: a declarator with no derivations, a call with
168    /// no arguments, a declaration with no attributes.
169    pub const EMPTY: Self = Self { start: 0, end: 0, _marker: PhantomData };
170
171    /// Builds a range.
172    ///
173    /// # Panics
174    ///
175    /// Panics if `end` is before `start`.
176    #[inline]
177    pub fn new(start: Idx<T>, end: Idx<T>) -> Self {
178        assert!(start.raw() <= end.raw(), "reversed index range");
179        Self { start: start.raw(), end: end.raw(), _marker: PhantomData }
180    }
181
182    /// The empty range at `at`.
183    #[inline]
184    pub fn empty_at(at: Idx<T>) -> Self {
185        Self { start: at.raw(), end: at.raw(), _marker: PhantomData }
186    }
187
188    /// How many indices the range covers.
189    #[inline]
190    pub const fn len(self) -> usize {
191        (self.end - self.start) as usize
192    }
193
194    /// Whether the range covers nothing.
195    #[inline]
196    pub const fn is_empty(self) -> bool {
197        self.start == self.end
198    }
199
200    /// The indices in the range, in order.
201    pub fn iter(self) -> impl Iterator<Item = Idx<T>> {
202        (self.start..self.end).map(Idx::new)
203    }
204
205    /// The range as a `usize` range, for slicing the backing vector.
206    #[inline]
207    pub const fn as_usize_range(self) -> std::ops::Range<usize> {
208        self.start as usize..self.end as usize
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    struct Block;
217    struct Inst;
218
219    #[test]
220    fn an_index_is_four_bytes_and_so_is_an_optional_one() {
221        assert_eq!(size_of::<Idx<Block>>(), 4);
222        assert_eq!(size_of::<Option<Idx<Block>>>(), 4);
223    }
224
225    #[test]
226    fn round_trips_through_usize() {
227        let i = Idx::<Inst>::from_usize(7);
228        assert_eq!(i.index(), 7);
229        assert_eq!(i.raw(), 7);
230    }
231
232    #[test]
233    fn debug_names_the_table() {
234        assert_eq!(format!("{:?}", Idx::<Block>::new(3)), "Block#3");
235    }
236
237    #[test]
238    fn a_range_iterates_half_open() {
239        let r = IdxRange::new(Idx::<Inst>::new(2), Idx::<Inst>::new(5));
240        let got: Vec<u32> = r.iter().map(Idx::raw).collect();
241        assert_eq!(got, vec![2, 3, 4]);
242        assert_eq!(r.len(), 3);
243        assert_eq!(r.as_usize_range(), 2..5);
244    }
245
246    #[test]
247    fn an_empty_range_is_empty() {
248        let r = IdxRange::empty_at(Idx::<Inst>::new(9));
249        assert!(r.is_empty());
250        assert_eq!(r.iter().count(), 0);
251    }
252
253    #[test]
254    fn the_empty_range_slices_an_empty_table() {
255        let r = IdxRange::<Inst>::EMPTY;
256        assert!(r.is_empty());
257        assert_eq!(r.len(), 0);
258        let table: Vec<u8> = Vec::new();
259        assert!(table[r.as_usize_range()].is_empty());
260    }
261
262    #[test]
263    #[should_panic(expected = "reversed index range")]
264    fn a_reversed_range_is_rejected() {
265        let _ = IdxRange::new(Idx::<Inst>::new(5), Idx::<Inst>::new(2));
266    }
267
268    #[test]
269    #[should_panic(expected = "index out of range")]
270    fn the_niche_value_is_rejected() {
271        let _ = Idx::<Inst>::new(u32::MAX);
272    }
273}