driver_lang/error.rs
1//! The error that halts a compilation run.
2
3use alloc::borrow::Cow;
4use core::fmt;
5
6/// The error that stops a compilation run.
7///
8/// A [`Stage`](crate::Stage) returns a `DriverError` from
9/// [`Stage::run`](crate::Stage::run) when it cannot produce its output: a phase
10/// that hit an unrecoverable condition, or a call to
11/// [`Session::abort_if_errors`](crate::Session::abort_if_errors) that found the
12/// session already holds errors. It is the *control-flow* failure that ends the
13/// pipeline, as distinct from a [`Diagnostic`](crate::Diagnostic), which is a
14/// *record* a stage emits and may keep going after.
15///
16/// The error carries the [`message`](Self::message) describing what went wrong and
17/// the [`stage`](Self::stage) it came from. A stage does not name itself: it
18/// returns `DriverError::new(reason)` with an empty stage, and the
19/// [`Pipeline`](crate::Pipeline) stamps in the name of the stage that failed as it
20/// propagates the error. Stamping only fills an *empty* name, so in a chain the
21/// innermost stage — the one that actually failed — keeps the attribution.
22///
23/// The reason is stored as a [`Cow<'static, str>`](alloc::borrow::Cow), so a
24/// static message costs no allocation and a computed one is owned only when built.
25///
26/// # Examples
27///
28/// A stage failing with a static reason:
29///
30/// ```
31/// use driver_lang::DriverError;
32///
33/// let err = DriverError::new("unresolved import");
34/// assert_eq!(err.message(), "unresolved import");
35/// assert_eq!(err.stage(), ""); // not stamped until it flows through a Pipeline
36/// ```
37///
38/// A computed reason:
39///
40/// ```
41/// use driver_lang::DriverError;
42///
43/// let path = "src/main";
44/// let err = DriverError::new(format!("cannot read `{path}.rs`"));
45/// assert_eq!(err.message(), "cannot read `src/main.rs`");
46/// ```
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct DriverError {
49 stage: &'static str,
50 message: Cow<'static, str>,
51}
52
53impl DriverError {
54 /// Create an error describing why a stage could not complete.
55 ///
56 /// Call this from inside [`Stage::run`](crate::Stage::run). The `message`
57 /// accepts a string literal (no allocation) or an owned
58 /// [`String`](alloc::string::String) (a computed reason). The failing stage's
59 /// name is filled in by the [`Pipeline`](crate::Pipeline) — you do not repeat
60 /// it here.
61 ///
62 /// # Examples
63 ///
64 /// ```
65 /// use driver_lang::DriverError;
66 ///
67 /// let from_literal = DriverError::new("boom");
68 /// let from_owned = DriverError::new(String::from("boom"));
69 /// assert_eq!(from_literal, from_owned);
70 /// ```
71 #[must_use]
72 pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
73 Self {
74 stage: "",
75 message: message.into(),
76 }
77 }
78
79 /// The name of the stage that failed, or `""` if the error has not yet
80 /// propagated through a [`Pipeline`](crate::Pipeline).
81 ///
82 /// # Examples
83 ///
84 /// ```
85 /// use driver_lang::DriverError;
86 ///
87 /// assert_eq!(DriverError::new("boom").stage(), "");
88 /// ```
89 #[must_use]
90 #[inline]
91 pub fn stage(&self) -> &str {
92 self.stage
93 }
94
95 /// The reason the stage could not complete.
96 ///
97 /// # Examples
98 ///
99 /// ```
100 /// use driver_lang::DriverError;
101 ///
102 /// assert_eq!(DriverError::new("type error").message(), "type error");
103 /// ```
104 #[must_use]
105 #[inline]
106 pub fn message(&self) -> &str {
107 &self.message
108 }
109
110 /// Stamp the failing stage's name into the error, but only if it is not
111 /// already set. Called by the [`Pipeline`](crate::Pipeline) as it propagates a
112 /// failure; the first (innermost) stage to stamp wins, so the error names the
113 /// stage that actually failed rather than an outer wrapper.
114 #[must_use]
115 pub(crate) fn in_stage(mut self, name: &'static str) -> Self {
116 if self.stage.is_empty() {
117 self.stage = name;
118 }
119 self
120 }
121}
122
123impl fmt::Display for DriverError {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 if self.stage.is_empty() {
126 write!(f, "driver error: {}", self.message)
127 } else {
128 write!(f, "stage `{}` failed: {}", self.stage, self.message)
129 }
130 }
131}
132
133impl core::error::Error for DriverError {}
134
135#[cfg(test)]
136#[allow(clippy::unwrap_used, clippy::expect_used)]
137mod tests {
138 use super::*;
139 use alloc::string::{String, ToString};
140
141 #[test]
142 fn test_new_accepts_literal_and_owned() {
143 assert_eq!(DriverError::new("x"), DriverError::new(String::from("x")));
144 }
145
146 #[test]
147 fn test_message_is_preserved() {
148 assert_eq!(DriverError::new("type error").message(), "type error");
149 }
150
151 #[test]
152 fn test_stage_is_empty_before_stamping() {
153 assert_eq!(DriverError::new("boom").stage(), "");
154 }
155
156 #[test]
157 fn test_in_stage_stamps_empty_name() {
158 let err = DriverError::new("boom").in_stage("parse");
159 assert_eq!(err.stage(), "parse");
160 assert_eq!(err.message(), "boom");
161 }
162
163 #[test]
164 fn test_in_stage_does_not_overwrite_existing_name() {
165 let err = DriverError::new("boom").in_stage("lex").in_stage("parse");
166 assert_eq!(err.stage(), "lex");
167 }
168
169 #[test]
170 fn test_display_without_stage() {
171 assert_eq!(DriverError::new("boom").to_string(), "driver error: boom");
172 }
173
174 #[test]
175 fn test_display_with_stage() {
176 let err = DriverError::new("boom").in_stage("codegen");
177 assert_eq!(err.to_string(), "stage `codegen` failed: boom");
178 }
179
180 #[test]
181 fn test_error_trait_is_implemented() {
182 fn assert_error<E: core::error::Error>(_: &E) {}
183 assert_error(&DriverError::new("boom"));
184 }
185}