Skip to main content

rucc_diag/
lib.rs

1//! Diagnostics: spans, severities, and the structured form every message is built as.
2//!
3//! Design: `spec/03-architecture.md`. Layer rank 1, see `spec/18-package-layout.md`.
4//!
5//! A diagnostic is a value, not a string. It is rendered to a terminal, to JSON for editors,
6//! or not at all when a caller is only counting errors, and building it as a value is what
7//! makes those three the same code path. `spec/03-architecture.md` makes the JSON form a
8//! tier 2 stability promise because editors consume it.
9//!
10//! # Status
11//!
12//! The severity, span and diagnostic types are real, and so is the source map that turns a
13//! span back into a file, a line and a column. Rendering and the `-fdiagnostics-format=`
14//! plumbing are the remaining piece.
15//!
16//! This crate is tier 3 in `spec/18-package-layout.md` section 18.5: its Rust API is
17//! explicitly unstable and will change without a major version bump.
18
19#![doc(html_root_url = "https://docs.rs/rucc-diag/0.3.3")]
20
21mod errors;
22mod source;
23
24pub use crate::errors::{DEFAULT_ERROR_LIMIT, Errors};
25pub use crate::source::{
26    FileId, Loc, PresumedLoc, SourceBytes, SourceFile, SourceMap, SourceMapFull,
27};
28
29use std::fmt;
30
31/// A byte offset into the concatenated source map.
32///
33/// One flat coordinate space across every file in the translation unit, so a span is eight
34/// bytes and comparing two spans does not need to know which file they came from. The map
35/// from offset to file, line and column is built once and queried only when a diagnostic is
36/// actually rendered, which keeps the cost off the hot path.
37pub type BytePos = u32;
38
39/// A half-open range of source, `lo .. hi`.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub struct Span {
42    /// First byte of the range.
43    pub lo: BytePos,
44    /// One past the last byte of the range.
45    pub hi: BytePos,
46}
47
48impl Span {
49    /// The span covering `lo .. hi`.
50    ///
51    /// # Panics
52    ///
53    /// Panics if `hi` is before `lo`.
54    #[inline]
55    pub const fn new(lo: BytePos, hi: BytePos) -> Self {
56        assert!(lo <= hi, "reversed span");
57        Self { lo, hi }
58    }
59
60    /// The empty span at `at`, used for things the source does not contain: an implicit
61    /// conversion, a compiler-generated temporary, a builtin declaration.
62    #[inline]
63    pub const fn empty_at(at: BytePos) -> Self {
64        Self { lo: at, hi: at }
65    }
66
67    /// A span with no position at all.
68    ///
69    /// Distinct from an empty span, because "generated by the compiler" and "zero width at
70    /// offset zero" are different things and only one of them should be rendered with a
71    /// caret.
72    pub const DUMMY: Self = Self { lo: BytePos::MAX, hi: BytePos::MAX };
73
74    /// Whether this is [`Span::DUMMY`].
75    #[inline]
76    pub const fn is_dummy(self) -> bool {
77        self.lo == BytePos::MAX
78    }
79
80    /// Width in bytes.
81    #[inline]
82    pub const fn len(self) -> u32 {
83        self.hi - self.lo
84    }
85
86    /// Whether the span covers no bytes.
87    #[inline]
88    pub const fn is_empty(self) -> bool {
89        self.lo == self.hi
90    }
91
92    /// The smallest span covering both, ignoring dummies.
93    ///
94    /// Macro expansion and error recovery both need this constantly: the span of a binary
95    /// expression is the join of its operands, and the span of a recovered declaration is
96    /// the join of everything the parser skipped.
97    #[inline]
98    pub fn to(self, other: Self) -> Self {
99        if self.is_dummy() {
100            return other;
101        }
102        if other.is_dummy() {
103            return self;
104        }
105        Self { lo: self.lo.min(other.lo), hi: self.hi.max(other.hi) }
106    }
107
108    /// Whether `pos` falls inside the span.
109    #[inline]
110    pub const fn contains(self, pos: BytePos) -> bool {
111        !self.is_dummy() && self.lo <= pos && pos < self.hi
112    }
113}
114
115/// How bad a diagnostic is.
116///
117/// The ordering is by severity, so `max` over a run of diagnostics gives the worst one and
118/// the exit status falls out of that.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
120pub enum Severity {
121    /// Extra context attached to another diagnostic, never emitted alone.
122    Note,
123    /// A suggested fix, attached to another diagnostic.
124    Help,
125    /// Accepted, compiled, and worth telling the user about. Becomes an error under
126    /// `-Werror`.
127    Warning,
128    /// Rejected. Compilation continues so that more than one error is reported, but no
129    /// output is produced.
130    Error,
131    /// A bug in the compiler. Distinguished from `Error` because the two need completely
132    /// different text: an error is the user's problem to fix, an internal compiler error is
133    /// ours, and telling a user to fix an ICE wastes their afternoon.
134    Ice,
135}
136
137impl Severity {
138    /// Whether a diagnostic at this severity means no output is produced.
139    #[inline]
140    pub const fn is_fatal(self) -> bool {
141        matches!(self, Severity::Error | Severity::Ice)
142    }
143
144    /// The lowercase word used when rendering.
145    pub const fn as_str(self) -> &'static str {
146        match self {
147            Severity::Note => "note",
148            Severity::Help => "help",
149            Severity::Warning => "warning",
150            Severity::Error => "error",
151            Severity::Ice => "internal compiler error",
152        }
153    }
154}
155
156impl fmt::Display for Severity {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        f.write_str(self.as_str())
159    }
160}
161
162/// One message, with the place it is about and any attached sub-diagnostics.
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct Diagnostic {
165    /// How bad it is.
166    pub severity: Severity,
167    /// The stable identifier, for example `E0102`. Every diagnostic has one so that it can
168    /// be suppressed, documented and searched for. `None` only during construction.
169    pub code: Option<&'static str>,
170    /// The one-line summary. Lowercase, no trailing period, no formatting: this is the line
171    /// a user greps for.
172    pub message: String,
173    /// Where in the source.
174    pub span: Span,
175    /// Notes and helps hanging off this diagnostic.
176    pub children: Vec<Diagnostic>,
177}
178
179impl Diagnostic {
180    /// A new diagnostic at `severity`.
181    pub fn new(severity: Severity, message: impl Into<String>, span: Span) -> Self {
182        Self { severity, code: None, message: message.into(), span, children: Vec::new() }
183    }
184
185    /// A new error.
186    pub fn error(message: impl Into<String>, span: Span) -> Self {
187        Self::new(Severity::Error, message, span)
188    }
189
190    /// A new warning.
191    pub fn warning(message: impl Into<String>, span: Span) -> Self {
192        Self::new(Severity::Warning, message, span)
193    }
194
195    /// Attaches the stable diagnostic code.
196    #[must_use]
197    pub fn with_code(mut self, code: &'static str) -> Self {
198        self.code = Some(code);
199        self
200    }
201
202    /// Attaches a note.
203    #[must_use]
204    pub fn note(mut self, message: impl Into<String>, span: Span) -> Self {
205        self.children.push(Self::new(Severity::Note, message, span));
206        self
207    }
208
209    /// Attaches a suggestion.
210    #[must_use]
211    pub fn help(mut self, message: impl Into<String>, span: Span) -> Self {
212        self.children.push(Self::new(Severity::Help, message, span));
213        self
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn joining_spans_covers_both() {
223        let a = Span::new(4, 9);
224        let b = Span::new(20, 22);
225        assert_eq!(a.to(b), Span::new(4, 22));
226        assert_eq!(b.to(a), Span::new(4, 22));
227    }
228
229    #[test]
230    fn joining_with_a_dummy_keeps_the_real_one() {
231        let a = Span::new(4, 9);
232        assert_eq!(a.to(Span::DUMMY), a);
233        assert_eq!(Span::DUMMY.to(a), a);
234    }
235
236    #[test]
237    fn a_dummy_span_contains_nothing() {
238        assert!(!Span::DUMMY.contains(0));
239        assert!(!Span::DUMMY.contains(BytePos::MAX));
240    }
241
242    #[test]
243    fn an_empty_span_is_not_a_dummy_span() {
244        let e = Span::empty_at(0);
245        assert!(e.is_empty());
246        assert!(!e.is_dummy());
247    }
248
249    #[test]
250    fn severity_orders_by_how_bad_it_is() {
251        assert!(Severity::Error > Severity::Warning);
252        assert!(Severity::Ice > Severity::Error);
253        assert!(Severity::Warning > Severity::Note);
254    }
255
256    #[test]
257    fn only_errors_and_ices_suppress_output() {
258        assert!(Severity::Error.is_fatal());
259        assert!(Severity::Ice.is_fatal());
260        assert!(!Severity::Warning.is_fatal());
261    }
262
263    #[test]
264    fn a_diagnostic_carries_its_children() {
265        let d = Diagnostic::error("expected an expression", Span::new(1, 2))
266            .with_code("E0001")
267            .note("in this macro expansion", Span::new(0, 8))
268            .help("did you mean a compound literal", Span::DUMMY);
269        assert_eq!(d.code, Some("E0001"));
270        assert_eq!(d.children.len(), 2);
271        assert_eq!(d.children[0].severity, Severity::Note);
272        assert_eq!(d.children[1].severity, Severity::Help);
273    }
274
275    #[test]
276    #[should_panic(expected = "reversed span")]
277    fn a_reversed_span_is_rejected() {
278        let _ = Span::new(9, 4);
279    }
280}