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}