Skip to main content

dimvexa_core/
error.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! The single error type of the library and its stable machine-readable codes.
3
4use core::fmt;
5
6use crate::dim::Dim;
7use crate::kind::Kind;
8
9/// Everything that can go wrong in dimvexa. Library code never panics: every fallible
10/// operation returns this type.
11///
12/// Each variant has a stable, documented machine-readable code (see [`Error::code`] and
13/// [`Error::ALL_CODES`]). The enum is `#[non_exhaustive]`: new variants may be added in
14/// minor releases, codes are never renamed within a schema version.
15///
16/// # Examples
17///
18/// ```
19/// use dimvexa_core::{Error, Ratio};
20///
21/// let err = Ratio::new(1, 0).unwrap_err();
22/// assert_eq!(err.code(), "division_by_zero");
23/// assert!(matches!(err, Error::DivisionByZero));
24/// ```
25#[derive(Debug, Clone, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum Error {
28    /// Syntax error in a unit expression or number. Code `parse_error`.
29    Parse {
30        /// The complete input that failed to parse.
31        input: String,
32        /// Zero-based character position of the problem.
33        position: usize,
34        /// Human-readable explanation.
35        message: String,
36    },
37    /// A number could not be read. Code `invalid_number`.
38    InvalidNumber {
39        /// The text that is not a valid number.
40        input: String,
41    },
42    /// The symbol is not in the registry. Code `unknown_unit`.
43    UnknownUnit {
44        /// The unknown symbol.
45        symbol: String,
46        /// Close matches, for example the same text with different capitalisation.
47        suggestions: Vec<String>,
48    },
49    /// The bare symbol has several meanings and no config resolves it.
50    /// Code `ambiguous_symbol`.
51    AmbiguousSymbol {
52        /// The ambiguous symbol as written.
53        symbol: String,
54        /// Explicit unit symbols the caller may use instead.
55        candidates: Vec<String>,
56    },
57    /// The expression is syntactically valid but its meaning depends on operator
58    /// precedence conventions. Code `ambiguous_expression`.
59    AmbiguousExpression {
60        /// The offending expression.
61        input: String,
62        /// Explanation including how to write it unambiguously.
63        message: String,
64    },
65    /// The two units measure different physical dimensions. Code `dimension_mismatch`.
66    DimensionMismatch {
67        /// Dimension of the source.
68        from: Dim,
69        /// Dimension of the target.
70        to: Dim,
71    },
72    /// The two units have the same dimension but incompatible kinds, for example an
73    /// absolute temperature and a temperature difference. Code `kind_mismatch`.
74    KindMismatch {
75        /// Kind of the left or source operand.
76        from: Kind,
77        /// Kind of the right or target operand.
78        to: Kind,
79        /// What was attempted.
80        operation: &'static str,
81        /// Units to use instead, for example `delta_K` for a temperature difference.
82        suggestions: Vec<String>,
83    },
84    /// A checked integer operation overflowed. Code `overflow`.
85    Overflow {
86        /// The operation that overflowed.
87        operation: &'static str,
88    },
89    /// Division by zero in exact arithmetic. Code `division_by_zero`.
90    DivisionByZero,
91    /// Input and output slices have different lengths. Code `length_mismatch`.
92    LengthMismatch {
93        /// Length of the input slice.
94        input: usize,
95        /// Length of the output slice.
96        output: usize,
97    },
98    /// The operation is deliberately not supported because it would require guessing.
99    /// Code `refused`.
100    Refused {
101        /// Why the operation is refused and what to do instead.
102        reason: String,
103        /// Units or expressions to use instead (may be empty).
104        suggestions: Vec<String>,
105    },
106    /// A unit definition is inconsistent. Code `invalid_definition`.
107    InvalidDefinition {
108        /// Symbol of the offending definition.
109        symbol: String,
110        /// What is wrong.
111        message: String,
112    },
113    /// A symbol or alias is registered twice. Code `duplicate_symbol`.
114    DuplicateSymbol {
115        /// The symbol that is already taken.
116        symbol: String,
117    },
118}
119
120impl Error {
121    /// Every stable machine-readable error code, in declaration order.
122    pub const ALL_CODES: &'static [&'static str] = &[
123        "parse_error",
124        "invalid_number",
125        "unknown_unit",
126        "ambiguous_symbol",
127        "ambiguous_expression",
128        "dimension_mismatch",
129        "kind_mismatch",
130        "overflow",
131        "division_by_zero",
132        "length_mismatch",
133        "refused",
134        "invalid_definition",
135        "duplicate_symbol",
136    ];
137
138    /// The stable machine-readable code of this error.
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use dimvexa_core::Error;
144    /// let e = Error::Overflow { operation: "mul" };
145    /// assert_eq!(e.code(), "overflow");
146    /// assert!(Error::ALL_CODES.contains(&e.code()));
147    /// ```
148    #[must_use]
149    pub const fn code(&self) -> &'static str {
150        match self {
151            Self::Parse { .. } => "parse_error",
152            Self::InvalidNumber { .. } => "invalid_number",
153            Self::UnknownUnit { .. } => "unknown_unit",
154            Self::AmbiguousSymbol { .. } => "ambiguous_symbol",
155            Self::AmbiguousExpression { .. } => "ambiguous_expression",
156            Self::DimensionMismatch { .. } => "dimension_mismatch",
157            Self::KindMismatch { .. } => "kind_mismatch",
158            Self::Overflow { .. } => "overflow",
159            Self::DivisionByZero => "division_by_zero",
160            Self::LengthMismatch { .. } => "length_mismatch",
161            Self::Refused { .. } => "refused",
162            Self::InvalidDefinition { .. } => "invalid_definition",
163            Self::DuplicateSymbol { .. } => "duplicate_symbol",
164        }
165    }
166
167    /// Everything the error recommends instead: close matches for an unknown unit, the explicit
168    /// symbols for an ambiguous one, and the difference unit (`delta_K`, ...) for a refused or
169    /// mismatched temperature operation. Empty if the error has no recommendation.
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use dimvexa_core::Error;
175    /// let e = Error::UnknownUnit { symbol: "PSI".into(), suggestions: vec!["psi".into()] };
176    /// assert_eq!(e.suggestions(), ["psi"]);
177    /// assert!(Error::DivisionByZero.suggestions().is_empty());
178    /// ```
179    #[must_use]
180    pub fn suggestions(&self) -> &[String] {
181        match self {
182            Self::AmbiguousSymbol { candidates, .. } => candidates,
183            Self::UnknownUnit { suggestions, .. }
184            | Self::KindMismatch { suggestions, .. }
185            | Self::Refused { suggestions, .. } => suggestions,
186            _ => &[],
187        }
188    }
189
190    /// Candidate symbols attached to the error, if any (ambiguous or unknown symbols).
191    #[must_use]
192    pub fn candidates(&self) -> &[String] {
193        match self {
194            Self::AmbiguousSymbol { candidates, .. } => candidates,
195            Self::UnknownUnit { suggestions, .. } => suggestions,
196            _ => &[],
197        }
198    }
199}
200
201impl fmt::Display for Error {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        match self {
204            Self::Parse {
205                input,
206                position,
207                message,
208            } => {
209                write!(
210                    f,
211                    "cannot parse '{input}' at position {position}: {message}"
212                )
213            }
214            Self::InvalidNumber { input } => write!(f, "'{input}' is not a valid number"),
215            Self::UnknownUnit {
216                symbol,
217                suggestions,
218            } => {
219                write!(f, "unknown unit '{symbol}'")?;
220                if !suggestions.is_empty() {
221                    write!(f, " (did you mean: {}?)", suggestions.join(", "))?;
222                }
223                Ok(())
224            }
225            Self::AmbiguousSymbol { symbol, candidates } => write!(
226                f,
227                "'{symbol}' is ambiguous: use {}, or load a config that resolves it",
228                candidates.join(" or ")
229            ),
230            Self::AmbiguousExpression { input, message } => {
231                write!(f, "ambiguous expression '{input}': {message}")
232            }
233            Self::DimensionMismatch { from, to } => {
234                write!(
235                    f,
236                    "dimension mismatch: [{from}] cannot be converted to [{to}]"
237                )
238            }
239            Self::KindMismatch {
240                from,
241                to,
242                operation,
243                suggestions,
244            } => {
245                write!(
246                    f,
247                    "kind mismatch in {operation}: {from} and {to} are not compatible"
248                )?;
249                if !suggestions.is_empty() {
250                    write!(
251                        f,
252                        " (a temperature point and a temperature difference are different kinds; \
253                         for a difference use {})",
254                        suggestions.join(" or ")
255                    )?;
256                }
257                Ok(())
258            }
259            Self::Overflow { operation } => {
260                write!(f, "integer overflow in exact arithmetic ({operation})")
261            }
262            Self::DivisionByZero => f.write_str("division by zero"),
263            Self::LengthMismatch { input, output } => {
264                write!(
265                    f,
266                    "input has {input} values but output has room for {output}"
267                )
268            }
269            Self::Refused { reason, .. } => write!(f, "refused: {reason}"),
270            Self::InvalidDefinition { symbol, message } => {
271                write!(f, "invalid definition of '{symbol}': {message}")
272            }
273            Self::DuplicateSymbol { symbol } => write!(f, "symbol '{symbol}' is already defined"),
274        }
275    }
276}
277
278impl std::error::Error for Error {}
279
280/// Result alias used throughout the crate.
281pub type Result<T> = core::result::Result<T, Error>;