Skip to main content

llvmkit_asmparser/
numbered_values.rs

1//! Slot-numbered value tables used by the textual IR parser.
2//!
3//! Direct port of `llvm/include/llvm/AsmParser/NumberedValues.h`. The
4//! container is a `u32 -> T` map plus the smallest unused id; `add` is
5//! contractually monotonic — the upstream form `assert`s `ID >= NextUnusedID`,
6//! we surface that contract as a typed [`AddError`].
7//!
8//! The sealed [`forward_ref`] submodule ships the parser-side typestate
9//! ([`forward_ref::Unresolved`] / [`forward_ref::Resolved`]) that Sessions
10//! 1+3 of the parser-first roadmap consume to make "left a forward
11//! reference unresolved" a *compile* error rather than a late runtime
12//! failure.
13
14use std::collections::HashMap;
15
16/// Mapping from slot-id to value, remembering the next unused id.
17///
18/// Upstream form (`NumberedValues.h`):
19///
20/// ```cpp
21/// template <class T> class NumberedValues {
22///   DenseMap<unsigned, T> Vals;
23///   unsigned NextUnusedID = 0;
24/// public:
25///   unsigned getNext() const;
26///   T get(unsigned ID) const;
27///   void add(unsigned ID, T V);
28/// };
29/// ```
30#[derive(Debug, Clone)]
31pub struct NumberedValues<T> {
32    vals: HashMap<u32, T>,
33    next_unused_id: u32,
34}
35
36impl<T> Default for NumberedValues<T> {
37    #[inline]
38    fn default() -> Self {
39        Self {
40            vals: HashMap::new(),
41            next_unused_id: 0,
42        }
43    }
44}
45
46/// Failure modes for [`NumberedValues::add`]. Upstream uses `assert(ID >=
47/// NextUnusedID)`; we prefer a typed error so the parser's diagnostic path
48/// can attribute the failure to a span.
49#[derive(Clone, PartialEq, Eq, Hash, Debug, thiserror::Error)]
50pub enum AddError {
51    /// `id` is strictly less than the next-unused frontier — the slot is
52    /// either already populated or skipped past. Mirrors the
53    /// `"Invalid value ID"` assertion in `NumberedValues.h`.
54    #[error("invalid slot id {id}: next unused is {next}")]
55    StaleId { id: u32, next: u32 },
56}
57
58impl<T> NumberedValues<T> {
59    /// Empty registry. Equivalent to upstream's default-constructed state.
60    #[inline]
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Smallest id that has not been used yet. Mirrors `getNext()`.
66    #[inline]
67    pub fn get_next(&self) -> u32 {
68        self.next_unused_id
69    }
70
71    /// Look up the value for `id`, returning `None` when the slot is empty.
72    /// Upstream returns a default-constructed `T` (typically `nullptr` when
73    /// `T` is a pointer); the Rust analogue uses `Option<&T>` to make the
74    /// "missing" case impossible to confuse with a real value.
75    #[inline]
76    pub fn get(&self, id: u32) -> Option<&T> {
77        self.vals.get(&id)
78    }
79
80    /// Number of populated slots.
81    #[inline]
82    pub fn len(&self) -> usize {
83        self.vals.len()
84    }
85
86    /// `true` iff no slot has been populated.
87    #[inline]
88    pub fn is_empty(&self) -> bool {
89        self.vals.is_empty()
90    }
91
92    /// Insert `value` at `id`. Mirrors upstream `add(ID, V)`; the
93    /// `assert(ID >= NextUnusedID)` becomes [`AddError::StaleId`] so callers
94    /// keep their error path typed.
95    pub fn add(&mut self, id: u32, value: T) -> Result<(), AddError> {
96        if id < self.next_unused_id {
97            return Err(AddError::StaleId {
98                id,
99                next: self.next_unused_id,
100            });
101        }
102        self.vals.insert(id, value);
103        // Saturate at `u32::MAX` rather than panicking: the upstream form
104        // also runs into integer overflow at the same boundary, and a slot
105        // table that big is well past every realistic IR file.
106        self.next_unused_id = id.saturating_add(1);
107        Ok(())
108    }
109
110    /// Iterate `(id, &value)` pairs. Order is unspecified — upstream backs
111    /// the table with `DenseMap`, which is also unordered.
112    pub fn iter(&self) -> impl Iterator<Item = (u32, &T)> + '_ {
113        self.vals.iter().map(|(&k, v)| (k, v))
114    }
115}
116
117/// Forward-reference resolution typestate (Roadmap section 10.5).
118///
119/// The parser keeps slots that hold either a not-yet-defined forward
120/// reference ([`forward_ref::Unresolved`]) or a definition
121/// ([`forward_ref::Resolved`]). The two states
122/// are distinct types so leaking an `Unresolved<T>` past
123/// `PerFunctionState::finish` (Session 3) is a *compile* error: the consume
124/// site only accepts a state that has discharged every slot.
125///
126/// The supporting types ship now so Sessions 2-3 can wire their resolution
127/// pipelines against a stable interface.
128pub mod forward_ref {
129    /// A forward reference to slot `id`, recorded at `first_seen`. Holding an
130    /// [`Unresolved`] is the parser's promise that a definition is still
131    /// owed; `resolve(value)` consumes it and yields a [`Resolved`].
132    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
133    pub struct Unresolved<T, Loc> {
134        id: u32,
135        first_seen: Loc,
136        // `Unresolved` does not own a `T` — the type parameter is purely
137        // phantom so resolution gates can talk about the *kind* of reference
138        // being tracked (global / function / instruction / ...). Marker via
139        // `fn() -> T` makes `Unresolved` covariant in `T` without inheriting
140        // `T`'s drop / send / sync constraints.
141        _kind: core::marker::PhantomData<fn() -> T>,
142    }
143
144    impl<T, Loc> Unresolved<T, Loc> {
145        /// Record a forward reference to `id`, first observed at `first_seen`.
146        #[inline]
147        pub fn pending(id: u32, first_seen: Loc) -> Self {
148            Self {
149                id,
150                first_seen,
151                _kind: core::marker::PhantomData,
152            }
153        }
154
155        /// The slot id this reference points at.
156        #[inline]
157        pub fn id(&self) -> u32 {
158            self.id
159        }
160
161        /// Where the parser first observed the reference.
162        #[inline]
163        pub fn first_seen(&self) -> &Loc {
164            &self.first_seen
165        }
166
167        /// Bind this reference to its definition. Consumes `self` and yields
168        /// a [`Resolved`] view that exposes only read accessors.
169        #[inline]
170        pub fn resolve(self, value: T) -> Resolved<T, Loc> {
171            Resolved {
172                id: self.id,
173                first_seen: self.first_seen,
174                value,
175            }
176        }
177    }
178
179    /// A forward reference that has been bound to its definition. Read-only
180    /// view; cannot transition back to [`Unresolved`].
181    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
182    pub struct Resolved<T, Loc> {
183        id: u32,
184        first_seen: Loc,
185        value: T,
186    }
187
188    impl<T, Loc> Resolved<T, Loc> {
189        /// The slot id that was bound.
190        #[inline]
191        pub fn id(&self) -> u32 {
192            self.id
193        }
194
195        /// Where the parser first observed the reference (before resolution).
196        #[inline]
197        pub fn first_seen(&self) -> &Loc {
198            &self.first_seen
199        }
200
201        /// The bound value.
202        #[inline]
203        pub fn get(&self) -> &T {
204            &self.value
205        }
206
207        /// Take ownership of the bound value, discarding the slot metadata.
208        #[inline]
209        pub fn into_value(self) -> T {
210            self.value
211        }
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    /// Ports the contract spelled by `NumberedValues::getNext` in
220    /// `llvm/include/llvm/AsmParser/NumberedValues.h`. The header is the
221    /// canonical source — there is no dedicated upstream unit test.
222    #[test]
223    fn empty_registry_starts_at_zero() {
224        let n: NumberedValues<u32> = NumberedValues::new();
225        assert_eq!(n.get_next(), 0);
226        assert_eq!(n.get(0), None);
227        assert!(n.is_empty());
228    }
229
230    /// Ports the `add(ID, V)` body in `NumberedValues.h`: the next-unused
231    /// frontier becomes `ID + 1` after a successful insert.
232    #[test]
233    fn add_advances_next_unused() {
234        let mut n: NumberedValues<u32> = NumberedValues::new();
235        n.add(0, 100).expect("fresh id");
236        assert_eq!(n.get_next(), 1);
237        assert_eq!(n.get(0), Some(&100));
238
239        n.add(5, 500).expect("monotonic id");
240        assert_eq!(n.get_next(), 6);
241        assert_eq!(n.get(5), Some(&500));
242        // Skipped slots remain empty.
243        assert_eq!(n.get(2), None);
244    }
245
246    /// Ports `assert(ID >= NextUnusedID && "Invalid value ID")` from
247    /// `NumberedValues.h`. We surface it as [`AddError::StaleId`] instead of
248    /// aborting, which keeps the contract in plain sight at the call site.
249    #[test]
250    fn add_stale_id_is_typed_error() {
251        let mut n: NumberedValues<u32> = NumberedValues::new();
252        n.add(3, 30).unwrap();
253        let err = n.add(2, 20).unwrap_err();
254        assert_eq!(err, AddError::StaleId { id: 2, next: 4 });
255    }
256
257    /// Ports the slot-mapping shape sketched by
258    /// `unittests/AsmParser/AsmParserTest.cpp::TEST(AsmParserTest,
259    /// SlotMappingTest)`. The full integration test waits on the parser; this
260    /// test exercises the same `getNext` / `get` shape directly.
261    #[test]
262    fn slot_mapping_shape_matches_upstream_test() {
263        let mut globals: NumberedValues<&'static str> = NumberedValues::new();
264        globals.add(0, "@0").unwrap();
265        // After parsing `@0 = global ...`, `getNext()` is 1 — same shape as
266        // `Mapping.GlobalValues.getNext()` in the upstream test.
267        assert_eq!(globals.get_next(), 1);
268        assert_eq!(globals.get(0), Some(&"@0"));
269    }
270
271    /// llvmkit-specific (Roadmap 10.5): the forward-reference typestate
272    /// transitions from `Unresolved` to `Resolved` only through `resolve()`.
273    /// Closest upstream anchor: the `ForwardRefVals` map populated by
274    /// `LLParser::GetVal` / `getVal`-flavored helpers in
275    /// `llvm/lib/AsmParser/LLParser.cpp`.
276    #[test]
277    fn forward_ref_state_transition() {
278        use forward_ref::Unresolved;
279        let pending: Unresolved<u32, ()> = Unresolved::pending(7, ());
280        assert_eq!(pending.id(), 7);
281        let resolved = pending.resolve(42);
282        assert_eq!(resolved.id(), 7);
283        assert_eq!(*resolved.get(), 42);
284        assert_eq!(resolved.into_value(), 42);
285    }
286}