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}