Skip to main content

tenferro_linalg/
error.rs

1//! Domain errors owned by the linear-algebra extension.
2//!
3//! Numerical failures and unsupported dtypes remain linalg-owned values until
4//! they cross into the tensor/runtime boundary. That boundary wraps the value
5//! as a typed extension source, so callers can classify the failure without
6//! losing the operation-specific payload.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use tenferro_linalg::Error;
12//! use tenferro_tensor::{ErrorKind, DType};
13//!
14//! let error = Error::UnsupportedDType {
15//!     op: "svd",
16//!     dtype: DType::I32,
17//! };
18//! assert_eq!(error.kind(), ErrorKind::Unsupported);
19//! ```
20
21use tenferro_tensor::{DType, ErrorKind};
22
23/// Typed diagnostics for provider status and workspace contracts.
24#[cfg(any(feature = "cpu-blas", feature = "cuda"))]
25#[derive(Debug, thiserror::Error)]
26pub(crate) enum BackendError {
27    #[cfg(feature = "cuda")]
28    #[error("{library} call {call} returned status {status}")]
29    ProviderStatus {
30        library: &'static str,
31        call: &'static str,
32        status: i32,
33    },
34    #[cfg(feature = "cpu-blas")]
35    #[error("{library} routine {routine} returned an invalid workspace: {detail}")]
36    InvalidWorkspace {
37        library: &'static str,
38        routine: &'static str,
39        detail: String,
40    },
41}
42
43/// Failures owned by a linalg algorithm or provider boundary.
44///
45/// # Examples
46///
47/// ```rust
48/// use tenferro_linalg::Error;
49/// use tenferro_tensor::ErrorKind;
50///
51/// let error = Error::Singular { op: "solve" };
52/// assert_eq!(error.kind(), ErrorKind::NumericalFailure);
53/// ```
54#[derive(Debug, thiserror::Error)]
55#[non_exhaustive]
56pub enum Error {
57    /// The algorithm could not converge for the supplied numeric input.
58    #[error("{op} did not converge")]
59    NonConvergence {
60        /// Linalg operation that failed to converge.
61        op: &'static str,
62    },
63    /// The input matrix or factorization became singular.
64    #[error("{op} is singular")]
65    Singular {
66        /// Linalg operation that encountered a singular matrix.
67        op: &'static str,
68    },
69    /// The selected linalg operation has no implementation for this dtype.
70    #[error("{op} does not support dtype {dtype:?}")]
71    UnsupportedDType {
72        /// Linalg operation that rejected the dtype.
73        op: &'static str,
74        /// Rejected input dtype.
75        dtype: DType,
76    },
77}
78
79impl Error {
80    /// Return the stable coarse classification for this linalg failure.
81    ///
82    /// # Examples
83    ///
84    /// ```rust
85    /// use tenferro_linalg::Error;
86    /// use tenferro_tensor::ErrorKind;
87    ///
88    /// assert_eq!(
89    ///     Error::NonConvergence { op: "svd" }.kind(),
90    ///     ErrorKind::NumericalFailure
91    /// );
92    /// ```
93    #[must_use]
94    pub fn kind(&self) -> ErrorKind {
95        match self {
96            Self::NonConvergence { .. } | Self::Singular { .. } => ErrorKind::NumericalFailure,
97            Self::UnsupportedDType { .. } => ErrorKind::Unsupported,
98        }
99    }
100}
101
102/// Result type for linalg-owned domain operations.
103///
104/// # Examples
105///
106/// ```rust
107/// use tenferro_linalg::{Error, Result};
108///
109/// let result: Result<()> = Err(Error::Singular { op: "solve" });
110/// assert!(result.is_err());
111/// ```
112pub type Result<T> = std::result::Result<T, Error>;
113
114/// Wrap a linalg-owned source at the tensor extension boundary.
115pub(crate) fn into_tensor_error(op: &'static str, source: Error) -> tenferro_tensor::Error {
116    tenferro_tensor::Error::extension(
117        op,
118        crate::extension::LINALG_EXTENSION_FAMILY_ID,
119        source.kind(),
120        source,
121    )
122}
123
124/// Construct a typed unsupported-dtype tensor error for linalg backends.
125pub(crate) fn unsupported_dtype(op: &'static str, dtype: DType) -> tenferro_tensor::Error {
126    into_tensor_error(op, Error::UnsupportedDType { op, dtype })
127}
128
129/// Preserve a provider status as a typed backend source.
130#[cfg(feature = "cuda")]
131pub(crate) fn backend_status(
132    op: &'static str,
133    library: &'static str,
134    call: &'static str,
135    status: i32,
136) -> tenferro_tensor::Error {
137    tenferro_tensor::Error::backend_source(
138        op,
139        BackendError::ProviderStatus {
140            library,
141            call,
142            status,
143        },
144    )
145}
146
147/// Preserve an invalid provider workspace response as a typed backend source.
148#[cfg(feature = "cpu-blas")]
149pub(crate) fn invalid_workspace(
150    op: &'static str,
151    library: &'static str,
152    routine: &'static str,
153    detail: impl Into<String>,
154) -> tenferro_tensor::Error {
155    tenferro_tensor::Error::backend_source(
156        op,
157        BackendError::InvalidWorkspace {
158            library,
159            routine,
160            detail: detail.into(),
161        },
162    )
163}
164
165#[cfg(all(test, any(feature = "cpu-blas", feature = "cuda")))]
166mod tests {
167    use std::error::Error as _;
168
169    use super::*;
170
171    #[cfg(feature = "cuda")]
172    #[test]
173    fn provider_status_keeps_typed_backend_source() {
174        let error = backend_status("svd", "cuSOLVER", "cusolverDnSgesvd", 7);
175
176        assert_eq!(error.kind(), ErrorKind::BackendFailure);
177        assert!(matches!(
178            error.source().and_then(|source| source.downcast_ref()),
179            Some(BackendError::ProviderStatus {
180                library: "cuSOLVER",
181                call: "cusolverDnSgesvd",
182                status: 7,
183            })
184        ));
185    }
186
187    #[cfg(feature = "cpu-blas")]
188    #[test]
189    fn invalid_workspace_keeps_typed_backend_source() {
190        let error = invalid_workspace("eigh", "LAPACK", "dsyevd", "query was zero");
191
192        assert_eq!(error.kind(), ErrorKind::BackendFailure);
193        assert!(matches!(
194            error.source().and_then(|source| source.downcast_ref()),
195            Some(BackendError::InvalidWorkspace {
196                library: "LAPACK",
197                routine: "dsyevd",
198                detail,
199            }) if detail == "query was zero"
200        ));
201    }
202}