Skip to main content

ferray_core/
error.rs

1// ferray-core: Error types (REQ-27)
2
3use core::fmt;
4
5#[cfg(not(feature = "std"))]
6extern crate alloc;
7#[cfg(not(feature = "std"))]
8use alloc::{string::String, string::ToString, vec::Vec};
9
10/// The primary error type for all ferray operations.
11///
12/// This enum is `#[non_exhaustive]`, so new variants may be added
13/// in minor releases without breaking downstream code.
14#[derive(Debug, Clone, thiserror::Error)]
15#[non_exhaustive]
16pub enum FerrayError {
17    /// Operand shapes are incompatible for the requested operation.
18    #[error("shape mismatch: {message}")]
19    ShapeMismatch {
20        /// Human-readable description of the mismatch.
21        message: String,
22    },
23
24    /// Broadcasting failed because shapes cannot be reconciled.
25    #[error("broadcast failure: cannot broadcast shapes {shape_a:?} and {shape_b:?}")]
26    BroadcastFailure {
27        /// First shape.
28        shape_a: Vec<usize>,
29        /// Second shape.
30        shape_b: Vec<usize>,
31    },
32
33    /// An axis index exceeded the array's dimensionality.
34    #[error("axis {axis} is out of bounds for array with {ndim} dimensions")]
35    AxisOutOfBounds {
36        /// The invalid axis.
37        axis: usize,
38        /// Number of dimensions.
39        ndim: usize,
40    },
41
42    /// An element index exceeded the array's extent along some axis.
43    #[error("index {index} is out of bounds for axis {axis} with size {size}")]
44    IndexOutOfBounds {
45        /// The invalid index.
46        index: isize,
47        /// The axis along which the index was applied.
48        axis: usize,
49        /// The size of that axis.
50        size: usize,
51    },
52
53    /// A matrix was singular when an invertible one was required.
54    #[error("singular matrix: {message}")]
55    SingularMatrix {
56        /// Diagnostic context.
57        message: String,
58    },
59
60    /// An iterative algorithm did not converge within its budget.
61    #[error("convergence failure after {iterations} iterations: {message}")]
62    ConvergenceFailure {
63        /// Number of iterations attempted.
64        iterations: usize,
65        /// Diagnostic context.
66        message: String,
67    },
68
69    /// The requested dtype is invalid or unsupported for this operation.
70    #[error("invalid dtype: {message}")]
71    InvalidDtype {
72        /// Diagnostic context.
73        message: String,
74    },
75
76    /// A computation produced NaN / Inf when finite results were required.
77    #[error("numerical instability: {message}")]
78    NumericalInstability {
79        /// Diagnostic context.
80        message: String,
81    },
82
83    /// An I/O operation failed.
84    #[error("I/O error: {message}")]
85    IoError {
86        /// Diagnostic context.
87        message: String,
88    },
89
90    /// A function argument was invalid.
91    #[error("invalid value: {message}")]
92    InvalidValue {
93        /// Diagnostic context.
94        message: String,
95    },
96}
97
98/// Convenience alias used throughout ferray.
99pub type FerrayResult<T> = Result<T, FerrayError>;
100
101impl FerrayError {
102    /// Create a `ShapeMismatch` error with a formatted message.
103    pub fn shape_mismatch(msg: impl fmt::Display) -> Self {
104        Self::ShapeMismatch {
105            message: msg.to_string(),
106        }
107    }
108
109    /// Create a `BroadcastFailure` error.
110    #[must_use]
111    pub fn broadcast_failure(a: &[usize], b: &[usize]) -> Self {
112        Self::BroadcastFailure {
113            shape_a: a.to_vec(),
114            shape_b: b.to_vec(),
115        }
116    }
117
118    /// Create an `AxisOutOfBounds` error.
119    #[must_use]
120    pub const fn axis_out_of_bounds(axis: usize, ndim: usize) -> Self {
121        Self::AxisOutOfBounds { axis, ndim }
122    }
123
124    /// Create an `IndexOutOfBounds` error.
125    #[must_use]
126    pub const fn index_out_of_bounds(index: isize, axis: usize, size: usize) -> Self {
127        Self::IndexOutOfBounds { index, axis, size }
128    }
129
130    /// Create an `InvalidDtype` error with a formatted message.
131    pub fn invalid_dtype(msg: impl fmt::Display) -> Self {
132        Self::InvalidDtype {
133            message: msg.to_string(),
134        }
135    }
136
137    /// Create an `InvalidValue` error with a formatted message.
138    pub fn invalid_value(msg: impl fmt::Display) -> Self {
139        Self::InvalidValue {
140            message: msg.to_string(),
141        }
142    }
143
144    /// Create an `IoError` from a formatted message.
145    pub fn io_error(msg: impl fmt::Display) -> Self {
146        Self::IoError {
147            message: msg.to_string(),
148        }
149    }
150}
151
152#[cfg(feature = "std")]
153impl From<std::io::Error> for FerrayError {
154    fn from(e: std::io::Error) -> Self {
155        Self::IoError {
156            message: e.to_string(),
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn error_display_shape_mismatch() {
167        let e = FerrayError::shape_mismatch("expected (3,4), got (3,5)");
168        assert!(e.to_string().contains("expected (3,4), got (3,5)"));
169    }
170
171    #[test]
172    fn error_display_axis_out_of_bounds() {
173        let e = FerrayError::axis_out_of_bounds(5, 3);
174        assert!(e.to_string().contains("axis 5"));
175        assert!(e.to_string().contains("3 dimensions"));
176    }
177
178    #[test]
179    fn error_display_broadcast_failure() {
180        let e = FerrayError::broadcast_failure(&[4, 3], &[2, 5]);
181        let s = e.to_string();
182        assert!(s.contains("[4, 3]"));
183        assert!(s.contains("[2, 5]"));
184    }
185
186    #[test]
187    fn error_is_non_exhaustive() {
188        // Verify the enum is non_exhaustive by using a wildcard
189        // in a match from an "external" perspective. Inside this crate
190        // the compiler knows all variants, so we just verify construction.
191        let e = FerrayError::invalid_value("test");
192        assert!(matches!(e, FerrayError::InvalidValue { .. }));
193
194        let e2 = FerrayError::shape_mismatch("bad shape");
195        assert!(matches!(e2, FerrayError::ShapeMismatch { .. }));
196    }
197
198    #[test]
199    fn from_io_error() {
200        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
201        let ferray_err: FerrayError = io_err.into();
202        assert!(ferray_err.to_string().contains("file missing"));
203    }
204}