Skip to main content

driver_lang/
diagnostic.rs

1//! A single message a stage reports about the program it is compiling.
2
3use alloc::borrow::Cow;
4use core::fmt;
5
6use crate::severity::Severity;
7
8/// One message a stage reports about the program under compilation.
9///
10/// A diagnostic pairs a [`Severity`] with a human-readable message. Stages create
11/// diagnostics and hand them to the [`Session`](crate::Session), which stores them
12/// in emission order and keeps a running count of how many were errors. The driver
13/// owns *policy* — whether to keep going after an error, when to stop — while the
14/// diagnostic is just the *record* of what happened.
15///
16/// The message is a [`Cow<'static, str>`](alloc::borrow::Cow): a fixed message
17/// (`"unexpected end of input"`) is stored as a borrowed `&'static str` with no
18/// allocation, while a computed one (`format!("unknown name `{name}`")`) is owned
19/// only when it is actually built. This keeps the common "constant message" path
20/// allocation-free.
21///
22/// This type is deliberately small — a severity and a message, nothing else.
23/// driver-lang does not model spans, error codes, or suggestions: those belong to
24/// a dedicated diagnostics crate that a language can plug in as its own concern.
25/// The driver only needs enough to route messages and decide when to abort.
26///
27/// # Examples
28///
29/// ```
30/// use driver_lang::{Diagnostic, Severity};
31///
32/// let d = Diagnostic::error("unexpected `}`");
33/// assert_eq!(d.severity(), Severity::Error);
34/// assert_eq!(d.message(), "unexpected `}`");
35/// assert!(d.is_error());
36/// ```
37#[derive(Debug, Clone, PartialEq, Eq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize))]
39pub struct Diagnostic {
40    severity: Severity,
41    message: Cow<'static, str>,
42}
43
44impl Diagnostic {
45    /// Create a diagnostic with an explicit [`Severity`].
46    ///
47    /// The `message` accepts a string literal (stored without allocation) or an
48    /// owned [`String`](alloc::string::String) (a computed message). Prefer the
49    /// [`error`](Self::error), [`warning`](Self::warning), and [`note`](Self::note)
50    /// shorthands when the severity is known at the call site.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use driver_lang::{Diagnostic, Severity};
56    ///
57    /// let from_literal = Diagnostic::new(Severity::Note, "defined here");
58    /// let from_owned = Diagnostic::new(Severity::Note, String::from("defined here"));
59    /// assert_eq!(from_literal, from_owned);
60    /// ```
61    #[must_use]
62    pub fn new(severity: Severity, message: impl Into<Cow<'static, str>>) -> Self {
63        Self {
64            severity,
65            message: message.into(),
66        }
67    }
68
69    /// Create an [`Error`](Severity::Error) diagnostic — a hard failure that
70    /// counts toward the session's error total.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use driver_lang::Diagnostic;
76    ///
77    /// let d = Diagnostic::error("type mismatch");
78    /// assert!(d.is_error());
79    /// ```
80    #[must_use]
81    pub fn error(message: impl Into<Cow<'static, str>>) -> Self {
82        Self::new(Severity::Error, message)
83    }
84
85    /// Create a [`Warning`](Severity::Warning) diagnostic — recorded and shown,
86    /// but it does not by itself stop the build.
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use driver_lang::Diagnostic;
92    ///
93    /// let d = Diagnostic::warning("unused variable `x`");
94    /// assert!(!d.is_error());
95    /// ```
96    #[must_use]
97    pub fn warning(message: impl Into<Cow<'static, str>>) -> Self {
98        Self::new(Severity::Warning, message)
99    }
100
101    /// Create a [`Note`](Severity::Note) diagnostic — neutral context attached to
102    /// the output.
103    ///
104    /// # Examples
105    ///
106    /// ```
107    /// use driver_lang::Diagnostic;
108    ///
109    /// let d = Diagnostic::note("`main` is defined here");
110    /// assert!(!d.is_error());
111    /// ```
112    #[must_use]
113    pub fn note(message: impl Into<Cow<'static, str>>) -> Self {
114        Self::new(Severity::Note, message)
115    }
116
117    /// The [`Severity`] of this diagnostic.
118    #[must_use]
119    #[inline]
120    pub fn severity(&self) -> Severity {
121        self.severity
122    }
123
124    /// The message text.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use driver_lang::Diagnostic;
130    ///
131    /// assert_eq!(Diagnostic::error("boom").message(), "boom");
132    /// ```
133    #[must_use]
134    #[inline]
135    pub fn message(&self) -> &str {
136        &self.message
137    }
138
139    /// Whether this diagnostic's severity is [`Error`](Severity::Error).
140    ///
141    /// A shorthand for `self.severity().is_error()`, the same test the
142    /// [`Session`](crate::Session) uses to decide whether a diagnostic counts
143    /// toward its error total.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use driver_lang::Diagnostic;
149    ///
150    /// assert!(Diagnostic::error("boom").is_error());
151    /// assert!(!Diagnostic::warning("hmm").is_error());
152    /// ```
153    #[must_use]
154    #[inline]
155    pub fn is_error(&self) -> bool {
156        self.severity.is_error()
157    }
158}
159
160impl fmt::Display for Diagnostic {
161    /// Renders as `severity: message`, e.g. `error: unexpected `}``.
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        write!(f, "{}: {}", self.severity, self.message)
164    }
165}
166
167#[cfg(test)]
168#[allow(clippy::unwrap_used, clippy::expect_used)]
169mod tests {
170    use super::*;
171    use alloc::string::{String, ToString};
172
173    #[test]
174    fn test_new_accepts_literal_and_owned() {
175        let a = Diagnostic::new(Severity::Error, "x");
176        let b = Diagnostic::new(Severity::Error, String::from("x"));
177        assert_eq!(a, b);
178    }
179
180    #[test]
181    fn test_error_constructor_sets_error_severity() {
182        let d = Diagnostic::error("boom");
183        assert_eq!(d.severity(), Severity::Error);
184        assert!(d.is_error());
185    }
186
187    #[test]
188    fn test_warning_constructor_is_not_error() {
189        let d = Diagnostic::warning("hmm");
190        assert_eq!(d.severity(), Severity::Warning);
191        assert!(!d.is_error());
192    }
193
194    #[test]
195    fn test_note_constructor_is_not_error() {
196        let d = Diagnostic::note("fyi");
197        assert_eq!(d.severity(), Severity::Note);
198        assert!(!d.is_error());
199    }
200
201    #[test]
202    fn test_message_is_preserved() {
203        assert_eq!(
204            Diagnostic::error("unexpected token").message(),
205            "unexpected token"
206        );
207    }
208
209    #[test]
210    fn test_display_renders_severity_and_message() {
211        assert_eq!(Diagnostic::error("boom").to_string(), "error: boom");
212        assert_eq!(Diagnostic::note("here").to_string(), "note: here");
213    }
214}