llvmkit_asmparser/parse_error.rs
1//! Typed parser-error catalogue.
2//!
3//! Mirrors the diagnostic strings emitted by `LLParser::error` /
4//! `LLParser::tokError` in `llvm/lib/AsmParser/LLParser.cpp`. Upstream uses
5//! free-form `Twine` strings; we keep the same wording for the subset we
6//! ship today but route every variant through structured fields so callers
7//! can match on the failure mode without string comparison.
8//!
9//! The catalogue is intentionally narrow in this session — only the
10//! variants that the substrate (lexer pass-through, slot-table integrity,
11//! forward-reference resolution, location registry) and the immediate
12//! follow-on parser sessions will populate. Sessions 2-3 grow the enum as
13//! they add real `parse*` arms; the variants ship now so the parser does
14//! not have to relitigate the public error shape later.
15
16use llvmkit_support::Span;
17
18use crate::file_loc::FileLocRange;
19use crate::ll_lexer::LexError;
20use crate::numbered_values::AddError as SlotAddError;
21
22/// Where in the source a diagnostic points. Carrying both the byte
23/// [`Span`] (set by every parser arm) and the optional [`FileLocRange`]
24/// projection (populated when the parser is configured to track line/col)
25/// keeps low-level tooling and human-facing renderers happy without a
26/// second walk over the source buffer.
27#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
28pub struct DiagLoc {
29 pub span: Span,
30 pub file: Option<FileLocRange>,
31}
32
33impl DiagLoc {
34 /// Construct a diagnostic location pinned to a byte span only.
35 #[inline]
36 pub const fn span(span: Span) -> Self {
37 Self { span, file: None }
38 }
39
40 /// Attach a [`FileLocRange`] projection to an existing diagnostic.
41 #[inline]
42 pub const fn with_file(self, file: FileLocRange) -> Self {
43 Self {
44 span: self.span,
45 file: Some(file),
46 }
47 }
48}
49
50/// Top-level entity kind — distinguishes the namespaces tracked by the
51/// parser when it reports symbol errors. Mirrors the four
52/// `ForwardRefVals` / `ForwardRefBlocks` / `ForwardRefMDNodes` /
53/// `NumberedTypes` tables in `LLParser`.
54#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
55#[non_exhaustive]
56pub enum SymbolKind {
57 /// `@name` — function or global variable.
58 Global,
59 /// `%name` — function-local SSA value or argument.
60 Local,
61 /// `%name` at the type position — named or numbered struct type.
62 Type,
63 /// `label %name` — basic block.
64 Block,
65 /// `!name` — metadata node.
66 Metadata,
67 /// `#name` — attribute group.
68 AttrGroup,
69}
70
71/// Symbol identity: either an explicit name (`@foo`, `%bar`) or a slot
72/// number (`%0`, `@5`).
73#[derive(Clone, PartialEq, Eq, Hash, Debug)]
74pub enum SymbolId {
75 Named(String),
76 Numbered(u32),
77}
78
79impl core::fmt::Display for SymbolId {
80 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
81 match self {
82 SymbolId::Named(n) => f.write_str(n),
83 SymbolId::Numbered(n) => write!(f, "%{n}"),
84 }
85 }
86}
87
88/// Top-level parser error.
89///
90/// Variants are added phase-by-phase as new parser arms come online.
91/// Wording matches `LLParser.cpp` for the cases shipped today; structured
92/// fields let callers match without inspecting the rendered string.
93#[derive(Clone, PartialEq, Eq, Hash, Debug, thiserror::Error)]
94#[non_exhaustive]
95pub enum ParseError {
96 /// The lexer rejected the next token.
97 #[error(transparent)]
98 Lex(#[from] LexError),
99
100 /// `LLParser::error` site for "expected X" / "expected Y" diagnostics.
101 /// `expected` carries the human-readable description LLParser would
102 /// pass to `tokError`.
103 #[error("expected {expected} at {loc:?}")]
104 Expected { expected: String, loc: DiagLoc },
105
106 /// `redefinition of <symbol>` — mirrors `LLParser::checkValueID` and
107 /// the `"redefinition of "` diagnostic site in `LLParser.cpp`.
108 #[error("redefinition of {kind:?} '{id}'")]
109 Redefinition {
110 kind: SymbolKind,
111 id: SymbolId,
112 loc: DiagLoc,
113 },
114
115 /// `use of undefined ...` — mirrors the `"use of undefined "`
116 /// diagnostics that `LLParser` emits when a module-level forward
117 /// reference is never satisfied. Carries the reference's first-seen
118 /// location so renderers can point at the use site.
119 #[error("use of undefined {kind:?} '{id}'")]
120 UndefinedSymbol {
121 kind: SymbolKind,
122 id: SymbolId,
123 loc: DiagLoc,
124 },
125
126 /// `slot mapping rejected slot id` — wraps a [`SlotAddError`] from
127 /// [`crate::numbered_values::NumberedValues::add`]. Mirrors the
128 /// `assert(ID >= NextUnusedID)` site that `LLParser` triggers when a
129 /// `.ll` file uses a non-monotonic slot id.
130 #[error("invalid slot id at {loc:?}: {source}")]
131 InvalidSlotId {
132 #[source]
133 source: SlotAddError,
134 loc: DiagLoc,
135 },
136
137 /// `bitwidth for integer type out of range` — mirrors the upstream
138 /// `LLParser::parseType` arm that rejects `iN` for `N` outside
139 /// `[1, MAX_INT_BITS]`.
140 #[error("integer width {width} out of range (1..={max})")]
141 IntegerWidthOutOfRange { width: u64, max: u32, loc: DiagLoc },
142
143 /// I/O failure pulling source bytes. The lexer itself does not perform
144 /// I/O; this is for callers using
145 /// [`crate::read_to_owned`]-style helpers. The wrapped string is the
146 /// `Display` form of the underlying [`std::io::Error`]; we don't keep
147 /// the [`std::io::Error`] itself because it lacks `Clone`/`Eq`/`Hash`,
148 /// which the rest of [`ParseError`] derives.
149 #[error("I/O error reading source: {0}")]
150 Io(String),
151}
152
153impl From<std::io::Error> for ParseError {
154 #[inline]
155 fn from(e: std::io::Error) -> Self {
156 ParseError::Io(e.to_string())
157 }
158}
159
160impl ParseError {
161 /// The diagnostic location to highlight, when the variant carries one.
162 pub fn loc(&self) -> Option<DiagLoc> {
163 match self {
164 ParseError::Lex(e) => Some(DiagLoc::span(e.span())),
165 ParseError::Expected { loc, .. }
166 | ParseError::Redefinition { loc, .. }
167 | ParseError::UndefinedSymbol { loc, .. }
168 | ParseError::InvalidSlotId { loc, .. }
169 | ParseError::IntegerWidthOutOfRange { loc, .. } => Some(*loc),
170 ParseError::Io(_) => None,
171 }
172 }
173}
174
175/// `Result` alias parameterised on [`ParseError`].
176pub type ParseResult<T> = Result<T, ParseError>;
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 /// Ports the wording used by `LLParser::tokError("expected ...")` in
183 /// `LLParser.cpp`. The Rust analogue routes the message through a
184 /// structured field so tests anchor on the data, not a free-form
185 /// string match.
186 #[test]
187 fn expected_carries_location() {
188 let span = Span::new(5, 9);
189 let err = ParseError::Expected {
190 expected: "type".into(),
191 loc: DiagLoc::span(span),
192 };
193 let loc = err.loc().unwrap();
194 assert_eq!(loc.span, span);
195 assert!(loc.file.is_none());
196 }
197
198 /// Ports the `redefinition of ...` diagnostic family from
199 /// `LLParser.cpp`. We assert structural identity, not string identity,
200 /// to keep wording flexibility for follow-on sessions.
201 #[test]
202 fn redefinition_records_symbol() {
203 let err = ParseError::Redefinition {
204 kind: SymbolKind::Global,
205 id: SymbolId::Named("@foo".into()),
206 loc: DiagLoc::span(Span::new(0, 4)),
207 };
208 if let ParseError::Redefinition { kind, id, .. } = &err {
209 assert_eq!(*kind, SymbolKind::Global);
210 assert_eq!(*id, SymbolId::Named("@foo".into()));
211 } else {
212 panic!("wrong variant");
213 }
214 }
215
216 /// llvmkit-specific: lexer errors flow through [`ParseError::Lex`]
217 /// without re-encoding. Closest upstream anchor: `LLParser` calling
218 /// `Lex.Error(...)` and propagating through `LLParser::error`.
219 #[test]
220 fn lex_error_passes_through() {
221 let lex = LexError::UnknownToken {
222 span: Span::new(0, 1),
223 };
224 let err: ParseError = lex.clone().into();
225 assert_eq!(err.loc().map(|l| l.span), Some(lex.span()));
226 }
227
228 /// Ports the upstream `parseType`-arm rejection of out-of-range integer
229 /// widths (`LLParser.cpp::parseType` checks against `MAX_INT_BITS`).
230 #[test]
231 fn integer_width_out_of_range_is_typed() {
232 let err = ParseError::IntegerWidthOutOfRange {
233 width: 1 << 30,
234 max: (1 << 24) - 1,
235 loc: DiagLoc::span(Span::new(2, 10)),
236 };
237 let rendered = format!("{err}");
238 assert!(rendered.contains("integer width"));
239 assert!(rendered.contains("out of range"));
240 }
241}