Skip to main content

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}