pass_lang/error.rs
1//! The error a pass returns when it cannot complete.
2
3use alloc::borrow::Cow;
4use core::fmt;
5
6/// The error a [`Pass`](crate::Pass) returns when it cannot complete its work.
7///
8/// A pass produces a `PassError` from inside [`Pass::run`](crate::Pass::run)
9/// when it hits a condition it cannot transform past — a construct it does not
10/// support, an invariant the input violates, an arithmetic operation that would
11/// overflow. The error carries a human-readable [`message`](Self::message)
12/// explaining what went wrong; the [`PassManager`](crate::PassManager) then
13/// stamps in the [`name`](Self::pass) of the pass that failed before returning
14/// it, so the caller always knows *which* pass halted the pipeline and *why*.
15///
16/// The reason is stored as a [`Cow<'static, str>`](alloc::borrow::Cow): a static
17/// message (`"unsupported construct"`) costs no allocation, while a computed one
18/// (`format!("overflow folding {a} * {b}")`) is owned only when it is built.
19///
20/// # Examples
21///
22/// Failing a pass with a static reason:
23///
24/// ```
25/// use pass_lang::{Outcome, Pass, PassError};
26///
27/// struct RejectEmpty;
28/// impl Pass<Vec<i64>> for RejectEmpty {
29/// fn name(&self) -> &'static str { "reject-empty" }
30/// fn run(&mut self, unit: &mut Vec<i64>) -> Result<Outcome, PassError> {
31/// if unit.is_empty() {
32/// return Err(PassError::new("the unit is empty"));
33/// }
34/// Ok(Outcome::Unchanged)
35/// }
36/// }
37/// ```
38///
39/// Failing with a computed reason:
40///
41/// ```
42/// use pass_lang::PassError;
43///
44/// let (a, b) = (i64::MAX, 2);
45/// let err = PassError::new(format!("overflow folding {a} * {b}"));
46/// assert_eq!(err.message(), "overflow folding 9223372036854775807 * 2");
47/// ```
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct PassError {
50 pass: &'static str,
51 message: Cow<'static, str>,
52}
53
54impl PassError {
55 /// Create an error describing why a pass could not complete.
56 ///
57 /// Call this from inside [`Pass::run`](crate::Pass::run). The `message`
58 /// accepts a string literal (no allocation) or an owned [`String`] (a
59 /// computed reason). The failing pass's name is filled in by the
60 /// [`PassManager`](crate::PassManager) — you do not need to repeat it.
61 ///
62 /// # Examples
63 ///
64 /// ```
65 /// use pass_lang::PassError;
66 ///
67 /// let from_literal = PassError::new("division by zero");
68 /// let from_owned = PassError::new(String::from("division by zero"));
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 pass: "",
75 message: message.into(),
76 }
77 }
78
79 /// The name of the pass that failed, or `""` if the error has not yet been
80 /// returned through a [`PassManager`](crate::PassManager).
81 ///
82 /// # Examples
83 ///
84 /// ```
85 /// use pass_lang::PassError;
86 ///
87 /// // Before the manager stamps it in, the pass name is empty.
88 /// assert_eq!(PassError::new("boom").pass(), "");
89 /// ```
90 #[must_use]
91 pub fn pass(&self) -> &str {
92 self.pass
93 }
94
95 /// The reason the pass could not complete.
96 ///
97 /// # Examples
98 ///
99 /// ```
100 /// use pass_lang::PassError;
101 ///
102 /// assert_eq!(PassError::new("unsupported node").message(), "unsupported node");
103 /// ```
104 #[must_use]
105 pub fn message(&self) -> &str {
106 &self.message
107 }
108
109 /// Stamp the failing pass's name into the error. Called by the
110 /// [`PassManager`](crate::PassManager) as it propagates the failure.
111 pub(crate) fn in_pass(mut self, name: &'static str) -> Self {
112 self.pass = name;
113 self
114 }
115}
116
117impl fmt::Display for PassError {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 if self.pass.is_empty() {
120 write!(f, "pass failed: {}", self.message)
121 } else {
122 write!(f, "pass `{}` failed: {}", self.pass, self.message)
123 }
124 }
125}
126
127impl core::error::Error for PassError {}
128
129#[cfg(test)]
130#[allow(clippy::expect_used, clippy::unwrap_used)]
131mod tests {
132 use super::*;
133 use alloc::string::{String, ToString};
134
135 #[test]
136 fn test_new_accepts_literal_and_owned() {
137 assert_eq!(PassError::new("x"), PassError::new(String::from("x")));
138 }
139
140 #[test]
141 fn test_message_is_preserved() {
142 assert_eq!(
143 PassError::new("division by zero").message(),
144 "division by zero"
145 );
146 }
147
148 #[test]
149 fn test_pass_is_empty_before_stamping() {
150 assert_eq!(PassError::new("boom").pass(), "");
151 }
152
153 #[test]
154 fn test_in_pass_stamps_name() {
155 let err = PassError::new("boom").in_pass("const-fold");
156 assert_eq!(err.pass(), "const-fold");
157 assert_eq!(err.message(), "boom");
158 }
159
160 #[test]
161 fn test_display_without_pass_name() {
162 assert_eq!(PassError::new("boom").to_string(), "pass failed: boom");
163 }
164
165 #[test]
166 fn test_display_with_pass_name() {
167 let err = PassError::new("boom").in_pass("simplify");
168 assert_eq!(err.to_string(), "pass `simplify` failed: boom");
169 }
170
171 #[test]
172 fn test_error_trait_is_implemented() {
173 fn assert_error<E: core::error::Error>(_: &E) {}
174 assert_error(&PassError::new("boom"));
175 }
176}