Skip to main content

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}