Skip to main content

ferray_core/
error.rs

1// ferray-core: Error types (REQ-27)
2
3use core::fmt;
4
5#[cfg(feature = "no_std")]
6extern crate alloc;
7#[cfg(feature = "no_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    pub fn broadcast_failure(a: &[usize], b: &[usize]) -> Self {
111        Self::BroadcastFailure {
112            shape_a: a.to_vec(),
113            shape_b: b.to_vec(),
114        }
115    }
116
117    /// Create an `AxisOutOfBounds` error.
118    pub fn axis_out_of_bounds(axis: usize, ndim: usize) -> Self {
119        Self::AxisOutOfBounds { axis, ndim }
120    }
121
122    /// Create an `IndexOutOfBounds` error.
123    pub fn index_out_of_bounds(index: isize, axis: usize, size: usize) -> Self {
124        Self::IndexOutOfBounds { index, axis, size }
125    }
126
127    /// Create an `InvalidDtype` error with a formatted message.
128    pub fn invalid_dtype(msg: impl fmt::Display) -> Self {
129        Self::InvalidDtype {
130            message: msg.to_string(),
131        }
132    }
133
134    /// Create an `InvalidValue` error with a formatted message.
135    pub fn invalid_value(msg: impl fmt::Display) -> Self {
136        Self::InvalidValue {
137            message: msg.to_string(),
138        }
139    }
140
141    /// Create an `IoError` from a formatted message.
142    pub fn io_error(msg: impl fmt::Display) -> Self {
143        Self::IoError {
144            message: msg.to_string(),
145        }
146    }
147}
148
149#[cfg(not(feature = "no_std"))]
150impl From<std::io::Error> for FerrayError {
151    fn from(e: std::io::Error) -> Self {
152        Self::IoError {
153            message: e.to_string(),
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn error_display_shape_mismatch() {
164        let e = FerrayError::shape_mismatch("expected (3,4), got (3,5)");
165        assert!(e.to_string().contains("expected (3,4), got (3,5)"));
166    }
167
168    #[test]
169    fn error_display_axis_out_of_bounds() {
170        let e = FerrayError::axis_out_of_bounds(5, 3);
171        assert!(e.to_string().contains("axis 5"));
172        assert!(e.to_string().contains("3 dimensions"));
173    }
174
175    #[test]
176    fn error_display_broadcast_failure() {
177        let e = FerrayError::broadcast_failure(&[4, 3], &[2, 5]);
178        let s = e.to_string();
179        assert!(s.contains("[4, 3]"));
180        assert!(s.contains("[2, 5]"));
181    }
182
183    #[test]
184    fn error_is_non_exhaustive() {
185        // Verify the enum is non_exhaustive by using a wildcard
186        // in a match from an "external" perspective. Inside this crate
187        // the compiler knows all variants, so we just verify construction.
188        let e = FerrayError::invalid_value("test");
189        assert!(matches!(e, FerrayError::InvalidValue { .. }));
190
191        let e2 = FerrayError::shape_mismatch("bad shape");
192        assert!(matches!(e2, FerrayError::ShapeMismatch { .. }));
193    }
194
195    #[test]
196    fn from_io_error() {
197        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
198        let ferray_err: FerrayError = io_err.into();
199        assert!(ferray_err.to_string().contains("file missing"));
200    }
201}