Skip to main content

workshop_rs/
ids.rs

1//! Strongly typed IDs.
2//!
3//! Every stable identity in the IR is an [`Id<T>`]: an opaque newtype over a
4//! `u32` arena index, tagged with the type it references. The type parameter
5//! makes it impossible to pass, say, an expression ID where a rule ID is
6//! expected, while `Id<T>` remains cheap, comparable, and hashable.
7//!
8//! IDs are produced by [`Arena::push`](crate::arena::Arena::push) and are
9//! stable for the lifetime of the arena. They are never dereferenced
10//! directly: lookup goes through the owning arena, which bounds-checks and
11//! returns `Option`, so an invalid or dangling ID is a recoverable invariant
12//! error rather than a panic.
13
14use std::marker::PhantomData;
15
16/// A typed, stable index into an [`Arena`](crate::arena::Arena).
17///
18/// `T` is the referenced type and is used only as a marker; `Id<T>` has the
19/// size of `u32` and is `Copy`, `Send`, and `Sync` regardless of `T`. The
20/// comparison, hashing, and formatting impls are implemented manually so they
21/// never require `T` itself to implement them.
22pub struct Id<T> {
23    index: u32,
24    _marker: PhantomData<fn() -> T>,
25}
26
27impl<T> std::fmt::Debug for Id<T> {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        f.debug_tuple("Id").field(&self.index).finish()
30    }
31}
32
33impl<T> Clone for Id<T> {
34    fn clone(&self) -> Self {
35        *self
36    }
37}
38
39impl<T> Copy for Id<T> {}
40
41impl<T> PartialEq for Id<T> {
42    fn eq(&self, other: &Self) -> bool {
43        self.index == other.index
44    }
45}
46
47impl<T> Eq for Id<T> {}
48
49impl<T> PartialOrd for Id<T> {
50    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
51        Some(self.cmp(other))
52    }
53}
54
55impl<T> Ord for Id<T> {
56    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
57        self.index.cmp(&other.index)
58    }
59}
60
61impl<T> std::hash::Hash for Id<T> {
62    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
63        self.index.hash(state);
64    }
65}
66
67impl<T> Id<T> {
68    /// Build an ID from an arena index.
69    ///
70    /// This is the only way to construct an ID outside an arena. Prefer
71    /// [`Arena::push`](crate::arena::Arena::push) so the ID is valid by
72    /// construction; `from_index` exists for tests and deserialization paths
73    /// that then rely on bounds-checked lookup.
74    pub const fn from_index(index: usize) -> Self {
75        Id {
76            index: index as u32,
77            _marker: PhantomData,
78        }
79    }
80
81    /// The arena index this ID refers to.
82    pub const fn index(self) -> usize {
83        self.index as usize
84    }
85}
86
87impl<T> std::fmt::Display for Id<T> {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        write!(f, "{}", self.index)
90    }
91}
92
93/// Any typed ID: exposes the arena index, usable in generic contexts.
94pub trait IdLike {
95    /// The arena index this ID refers to.
96    fn index(self) -> usize;
97}
98
99impl<T> IdLike for Id<T> {
100    fn index(self) -> usize {
101        Id::index(self)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::Id;
108
109    struct File;
110    struct Rule;
111
112    #[test]
113    fn ids_are_cheap_copyable_and_comparable() {
114        let a = Id::<File>::from_index(3);
115        let b = Id::<File>::from_index(3);
116        let c = Id::<File>::from_index(4);
117        assert_eq!(a, b);
118        assert_ne!(a, c);
119        assert_eq!(a.index(), 3);
120        assert!(a < c);
121        assert_eq!(std::mem::size_of::<Id<File>>(), 4);
122    }
123
124    #[test]
125    fn distinct_id_types_are_distinct() {
126        // Compile-time proof: a `File` id cannot be passed where a `Rule` id
127        // is expected. (No runtime assertion; the code below would not
128        // compile if `Id<T>` were not parameterized.)
129        let _file: Id<File> = Id::from_index(0);
130        let _rule: Id<Rule> = Id::from_index(0);
131        let _ = _file;
132        let _ = _rule;
133    }
134
135    #[test]
136    fn ids_survive_hashing() {
137        use std::collections::HashSet;
138        let mut set = HashSet::new();
139        set.insert(Id::<File>::from_index(7));
140        assert!(set.contains(&Id::<File>::from_index(7)));
141    }
142}