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>;