Skip to main content

tenferro_tensor/
error.rs

1//! Runtime error types for tensor execution.
2//!
3//! # Examples
4//!
5//! ```rust
6//! let error = tenferro_tensor::Error::shape_mismatch("add", [2], [3]);
7//! assert!(matches!(
8//!     error,
9//!     tenferro_tensor::Error::Validation { op: "add", .. }
10//! ));
11//! ```
12
13use std::error::Error as StdError;
14
15use tenferro_tensor_core::{ErrorKind, ValidationError};
16
17/// Boxed source used for backend and extension failures whose concrete type is
18/// owned by another crate or a vendor API.
19pub type BoxError = Box<dyn StdError + Send + Sync + 'static>;
20
21/// Runtime failures produced by tensor execution backends and helpers.
22///
23/// Validation failures retain the shared tensor-core payload as a typed source.
24/// Backend and extension failures retain opaque typed sources when one exists;
25/// text-only vendor failures use [`Error::BackendFailure`].
26///
27/// # Examples
28///
29/// ```rust
30/// let error = tenferro_tensor::Error::rank_mismatch("reshape", 2, 1);
31/// assert!(matches!(
32///     error,
33///     tenferro_tensor::Error::Validation { op: "reshape", .. }
34/// ));
35/// ```
36#[derive(Debug, thiserror::Error)]
37#[non_exhaustive]
38pub enum Error {
39    #[error("{op}: {source}")]
40    Validation {
41        op: &'static str,
42        #[source]
43        source: ValidationError,
44    },
45    #[error("{op}: unsupported dtype conversion from {from:?} to {to:?}: {message}")]
46    UnsupportedDTypeConversion {
47        op: &'static str,
48        from: crate::DType,
49        to: crate::DType,
50        message: String,
51    },
52    #[error("{op}: unsupported dtype {dtype:?}: {message}")]
53    UnsupportedDType {
54        op: &'static str,
55        dtype: crate::DType,
56        message: String,
57    },
58    #[error("{op}: unsupported operation: {message}")]
59    Unsupported { op: &'static str, message: String },
60    #[error("{op}: backend failure: {message}")]
61    BackendFailure { op: &'static str, message: String },
62    #[error("{op}: backend failure: {source}")]
63    BackendSource {
64        op: &'static str,
65        #[source]
66        source: BoxError,
67    },
68    #[error("{op}: I/O failure: {source}")]
69    IoSource {
70        op: &'static str,
71        #[source]
72        source: BoxError,
73    },
74    #[error("{op}: runtime state failure: {message}")]
75    RuntimeState { op: &'static str, message: String },
76    #[error("{op}: runtime state failure: {source}")]
77    RuntimeStateSource {
78        op: &'static str,
79        #[source]
80        source: BoxError,
81    },
82    #[error("{op}: host access failed: {source}")]
83    HostAccess {
84        op: &'static str,
85        #[source]
86        source: crate::HostAccessError,
87    },
88    #[error("{op}: extension {family} failed: {source}")]
89    Extension {
90        op: &'static str,
91        family: &'static str,
92        kind: ErrorKind,
93        #[source]
94        source: BoxError,
95    },
96    #[error("missing runtime value for slot {slot}")]
97    MissingValue { slot: usize },
98    #[error("internal tensor error: {0}")]
99    Internal(String),
100}
101
102/// Owns the original tensor when a consuming representation reinterpretation
103/// cannot publish its checked descriptor.
104///
105/// Reinterpretation never falls back to an allocation or a copy.  Call
106/// [`Self::into_owner`] to recover the unchanged input and [`Self::error`] to
107/// inspect the typed failure.
108///
109/// # Examples
110///
111/// ```
112/// use tenferro_tensor::TypedTensor;
113///
114/// let tensor = TypedTensor::<f32>::from_vec_col_major(vec![1], vec![1.0])?;
115/// let Err(failure) = tensor.into_complex() else { return Ok(()); };
116/// assert!(!failure.error().to_string().is_empty());
117/// # Ok::<(), tenferro_tensor::Error>(())
118/// ```
119#[derive(Debug)]
120pub struct ReinterpretError<T> {
121    owner: Box<T>,
122    error: Error,
123}
124
125impl<T> ReinterpretError<T> {
126    pub(crate) fn new(owner: T, error: Error) -> Self {
127        Self {
128            owner: Box::new(owner),
129            error,
130        }
131    }
132
133    /// Recover the unchanged original owner.
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// use tenferro_tensor::TypedTensor;
139    ///
140    /// let tensor = TypedTensor::<f32>::from_vec_col_major(vec![1], vec![1.0])?;
141    /// let Err(failure) = tensor.into_complex() else { return Ok(()); };
142    /// let _owner = failure.into_owner();
143    /// # Ok::<(), tenferro_tensor::Error>(())
144    /// ```
145    pub fn into_owner(self) -> T {
146        *self.owner
147    }
148
149    /// Borrow the typed failure without consuming the owner.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use tenferro_tensor::TypedTensor;
155    ///
156    /// let tensor = TypedTensor::<f32>::from_vec_col_major(vec![1], vec![1.0])?;
157    /// let Err(failure) = tensor.into_complex() else { return Ok(()); };
158    /// assert!(!failure.error().to_string().is_empty());
159    /// # Ok::<(), tenferro_tensor::Error>(())
160    /// ```
161    pub fn error(&self) -> &Error {
162        &self.error
163    }
164
165    pub(crate) fn into_parts(self) -> (T, Error) {
166        (*self.owner, self.error)
167    }
168}
169
170impl<T: std::fmt::Debug> std::fmt::Display for ReinterpretError<T> {
171    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        write!(formatter, "tensor reinterpretation failed: {}", self.error)
173    }
174}
175
176impl<T: std::fmt::Debug + 'static> std::error::Error for ReinterpretError<T> {}
177
178impl Error {
179    /// Construct an incompatible-shapes validation error.
180    ///
181    /// # Examples
182    ///
183    /// ```rust
184    /// use tenferro_tensor::Error;
185    ///
186    /// let error = Error::shape_mismatch("add", [2, 3], [2, 4]);
187    /// assert!(matches!(error, Error::Validation { .. }));
188    /// ```
189    pub fn shape_mismatch(
190        op: &'static str,
191        lhs: impl Into<Vec<usize>>,
192        rhs: impl Into<Vec<usize>>,
193    ) -> Self {
194        Self::validation(
195            op,
196            tenferro_tensor_core::ShapeMismatch::IncompatibleShapes {
197                lhs: tenferro_tensor_core::ShapeVec::from_vec(lhs.into()),
198                rhs: tenferro_tensor_core::ShapeVec::from_vec(rhs.into()),
199            }
200            .into(),
201        )
202    }
203
204    /// Construct a rank-mismatch validation error.
205    ///
206    /// # Examples
207    ///
208    /// ```rust
209    /// use tenferro_tensor::Error;
210    ///
211    /// let error = Error::rank_mismatch("transpose", 2, 3);
212    /// assert!(matches!(error, Error::Validation { .. }));
213    /// ```
214    pub fn rank_mismatch(op: &'static str, expected: usize, actual: usize) -> Self {
215        Self::validation(op, ValidationError::RankMismatch { expected, actual })
216    }
217
218    /// Construct an axis-out-of-bounds validation error.
219    ///
220    /// # Examples
221    ///
222    /// ```rust
223    /// use tenferro_tensor::Error;
224    ///
225    /// let error = Error::axis_out_of_bounds("sum", 2, 2);
226    /// assert!(matches!(error, Error::Validation { .. }));
227    /// ```
228    pub fn axis_out_of_bounds(op: &'static str, axis: usize, rank: usize) -> Self {
229        Self::validation(op, ValidationError::AxisOutOfBounds { axis, rank })
230    }
231
232    /// Construct a duplicate-axis validation error.
233    ///
234    /// # Examples
235    ///
236    /// ```rust
237    /// use tenferro_tensor::Error;
238    ///
239    /// let error = Error::duplicate_axis("transpose", 1, "permutation");
240    /// assert!(matches!(error, Error::Validation { .. }));
241    /// ```
242    pub fn duplicate_axis(op: &'static str, axis: usize, role: &'static str) -> Self {
243        Self::validation(op, ValidationError::DuplicateAxis { axis, role })
244    }
245
246    /// Construct a dtype-mismatch validation error.
247    ///
248    /// # Examples
249    ///
250    /// ```rust
251    /// use tenferro_tensor::{DType, Error};
252    ///
253    /// let error = Error::dtype_mismatch("add", DType::F32, DType::F64);
254    /// assert!(matches!(error, Error::Validation { .. }));
255    /// ```
256    pub fn dtype_mismatch(op: &'static str, expected: crate::DType, actual: crate::DType) -> Self {
257        Self::validation(
258            op,
259            ValidationError::DTypeMismatch {
260                expected: crate::core_dtype(expected),
261                actual: crate::core_dtype(actual),
262            },
263        )
264    }
265
266    /// Wrap shared tensor validation with the operation that requested it.
267    ///
268    /// # Examples
269    ///
270    /// ```rust
271    /// use tenferro_tensor::{Error, ValidationError};
272    ///
273    /// let error = Error::validation(
274    ///     "transpose",
275    ///     ValidationError::AxisOutOfBounds { axis: 2, rank: 2 },
276    /// );
277    /// assert!(matches!(error, Error::Validation { op: "transpose", .. }));
278    /// ```
279    pub fn validation(op: &'static str, source: ValidationError) -> Self {
280        Self::Validation { op, source }
281    }
282
283    /// Construct a structured invalid-argument validation error.
284    ///
285    /// # Examples
286    ///
287    /// ```rust
288    /// use tenferro_tensor::{Error, ErrorKind, ValidationKind};
289    ///
290    /// let error = Error::invalid_argument("slice", "step", "must be non-zero");
291    /// assert_eq!(error.kind(), ErrorKind::Validation(ValidationKind::InvalidArgument));
292    /// ```
293    pub fn invalid_argument(
294        op: &'static str,
295        argument: &'static str,
296        message: impl Into<String>,
297    ) -> Self {
298        Self::validation(
299            op,
300            ValidationError::InvalidArgument {
301                argument,
302                message: message.into(),
303            },
304        )
305    }
306
307    /// Construct an unsupported dtype conversion error.
308    ///
309    /// # Examples
310    ///
311    /// ```rust
312    /// let error = tenferro_tensor::Error::unsupported_dtype_conversion(
313    ///     "convert",
314    ///     tenferro_tensor::DType::F64,
315    ///     tenferro_tensor::DType::I32,
316    ///     "lossy conversion is disabled",
317    /// );
318    /// assert!(matches!(
319    ///     error,
320    ///     tenferro_tensor::Error::UnsupportedDTypeConversion { .. }
321    /// ));
322    /// ```
323    pub fn unsupported_dtype_conversion(
324        op: &'static str,
325        from: crate::DType,
326        to: crate::DType,
327        message: impl Into<String>,
328    ) -> Self {
329        Self::UnsupportedDTypeConversion {
330            op,
331            from,
332            to,
333            message: message.into(),
334        }
335    }
336
337    /// Construct an operation-level unsupported-dtype error.
338    ///
339    /// This is for an operation that cannot run for the supplied dtype. It is
340    /// deliberately distinct from [`Error::unsupported_dtype_conversion`],
341    /// which is reserved for an actual from-dtype to to-dtype conversion.
342    ///
343    /// # Examples
344    ///
345    /// ```rust
346    /// let error = tenferro_tensor::Error::unsupported_dtype(
347    ///     "exp",
348    ///     tenferro_tensor::DType::I64,
349    ///     "integer exponentials are not implemented",
350    /// );
351    /// assert!(matches!(
352    ///     error,
353    ///     tenferro_tensor::Error::UnsupportedDType {
354    ///         op: "exp",
355    ///         dtype: tenferro_tensor::DType::I64,
356    ///         ..
357    ///     }
358    /// ));
359    /// ```
360    pub fn unsupported_dtype(
361        op: &'static str,
362        dtype: crate::DType,
363        message: impl Into<String>,
364    ) -> Self {
365        Self::UnsupportedDType {
366            op,
367            dtype,
368            message: message.into(),
369        }
370    }
371
372    /// Construct a structured unsupported-operation error.
373    ///
374    /// Use this for an operation or execution surface that is not implemented
375    /// by the selected backend. Dtype conversion failures use
376    /// [`Error::unsupported_dtype_conversion`] instead, and operation-specific
377    /// typed reasons should use [`Error::extension`] with `ErrorKind::Unsupported`.
378    ///
379    /// # Examples
380    ///
381    /// ```rust
382    /// let error = tenferro_tensor::Error::unsupported(
383    ///     "full_piv_lu",
384    ///     "backend has no implementation",
385    /// );
386    /// assert!(matches!(
387    ///     error,
388    ///     tenferro_tensor::Error::Unsupported { op: "full_piv_lu", .. }
389    /// ));
390    /// ```
391    pub fn unsupported(op: &'static str, message: impl Into<String>) -> Self {
392        Self::Unsupported {
393            op,
394            message: message.into(),
395        }
396    }
397
398    /// Construct a text-only backend failure.
399    ///
400    /// Use [`Error::backend_source`] when a typed source is available.
401    ///
402    /// # Examples
403    ///
404    /// ```rust
405    /// let error = tenferro_tensor::Error::backend_failure(
406    ///     "matmul",
407    ///     "backend rejected launch",
408    /// );
409    /// assert!(matches!(
410    ///     error,
411    ///     tenferro_tensor::Error::BackendFailure { op: "matmul", .. }
412    /// ));
413    /// ```
414    pub fn backend_failure(op: &'static str, message: impl Into<String>) -> Self {
415        Self::BackendFailure {
416            op,
417            message: message.into(),
418        }
419    }
420
421    /// Construct a backend failure while preserving its typed source.
422    ///
423    /// # Examples
424    ///
425    /// ```rust
426    /// let error = tenferro_tensor::Error::backend_source(
427    ///     "load",
428    ///     std::io::Error::other("read failed"),
429    /// );
430    /// assert!(std::error::Error::source(&error).is_some());
431    /// ```
432    pub fn backend_source<E>(op: &'static str, source: E) -> Self
433    where
434        E: StdError + Send + Sync + 'static,
435    {
436        Self::BackendSource {
437            op,
438            source: Box::new(source),
439        }
440    }
441
442    /// Construct an I/O failure while preserving its typed source.
443    ///
444    /// I/O errors are intentionally separate from backend failures: callers
445    /// can classify them as [`ErrorKind::Io`] without parsing a message.
446    ///
447    /// # Examples
448    ///
449    /// ```rust
450    /// use tenferro_tensor::{Error, ErrorKind};
451    ///
452    /// let error = Error::io_source("load", std::io::Error::other("read failed"));
453    /// assert_eq!(error.kind(), ErrorKind::Io);
454    /// assert!(std::error::Error::source(&error).is_some());
455    /// ```
456    pub fn io_source<E>(op: &'static str, source: E) -> Self
457    where
458        E: StdError + Send + Sync + 'static,
459    {
460        Self::IoSource {
461            op,
462            source: Box::new(source),
463        }
464    }
465
466    /// Construct a runtime-state failure when no typed source exists.
467    ///
468    /// Use this for missing, uninitialized, or invalid execution state. It is
469    /// distinct from [`Error::backend_failure`], which is reserved for
470    /// vendor/backend status text.
471    ///
472    /// # Examples
473    ///
474    /// ```rust
475    /// use tenferro_tensor::{Error, ErrorKind};
476    ///
477    /// let error = Error::runtime_state("execute", "backend session is not initialized");
478    /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
479    /// ```
480    pub fn runtime_state(op: &'static str, message: impl Into<String>) -> Self {
481        Self::RuntimeState {
482            op,
483            message: message.into(),
484        }
485    }
486
487    /// Construct a runtime-state failure while preserving a typed source.
488    ///
489    /// # Examples
490    ///
491    /// ```rust
492    /// use tenferro_tensor::{Error, ErrorKind};
493    ///
494    /// let error = Error::runtime_state_source(
495    ///     "execute",
496    ///     std::io::Error::other("executor lock poisoned"),
497    /// );
498    /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
499    /// assert!(std::error::Error::source(&error).is_some());
500    /// ```
501    pub fn runtime_state_source<E>(op: &'static str, source: E) -> Self
502    where
503        E: StdError + Send + Sync + 'static,
504    {
505        Self::RuntimeStateSource {
506            op,
507            source: Box::new(source),
508        }
509    }
510
511    /// Construct an extension failure while preserving its typed source and
512    /// coarse classification.
513    ///
514    /// # Examples
515    ///
516    /// ```rust
517    /// use std::error::Error as _;
518    /// use tenferro_tensor::{Error, ErrorKind};
519    ///
520    /// let error = Error::extension(
521    ///     "einsum",
522    ///     "einsum",
523    ///     ErrorKind::Internal,
524    ///     std::io::Error::other("planner failed"),
525    /// );
526    /// assert!(error.source().is_some());
527    /// ```
528    pub fn extension<E>(op: &'static str, family: &'static str, kind: ErrorKind, source: E) -> Self
529    where
530        E: StdError + Send + Sync + 'static,
531    {
532        Self::Extension {
533            op,
534            family,
535            kind,
536            source: Box::new(source),
537        }
538    }
539
540    /// Preserve a typed guarded-host-access failure.
541    ///
542    /// # Examples
543    ///
544    /// ```rust
545    /// use tenferro_tensor::{Error, HostAccessError};
546    ///
547    /// let error = Error::host_access(
548    ///     "map",
549    ///     HostAccessError::Unsupported { backend: "opaque" },
550    /// );
551    /// assert!(matches!(error, Error::HostAccess { .. }));
552    /// ```
553    pub fn host_access(op: &'static str, source: crate::HostAccessError) -> Self {
554        Self::HostAccess { op, source }
555    }
556
557    /// Return the stable coarse classification for this tensor failure.
558    ///
559    /// # Examples
560    ///
561    /// ```rust
562    /// use tenferro_tensor::{Error, ErrorKind, ValidationError, ValidationKind};
563    /// use tenferro_tensor::core::DType;
564    ///
565    /// let error = Error::validation(
566    ///     "add",
567    ///     ValidationError::DTypeMismatch {
568    ///         expected: DType::F32,
569    ///         actual: DType::F64,
570    ///     },
571    /// );
572    /// assert_eq!(error.kind(), ErrorKind::Validation(ValidationKind::DTypeMismatch));
573    /// ```
574    pub fn kind(&self) -> ErrorKind {
575        match self {
576            Self::Validation { source, .. } => ErrorKind::Validation(source.kind()),
577            Self::UnsupportedDTypeConversion { .. }
578            | Self::UnsupportedDType { .. }
579            | Self::Unsupported { .. } => ErrorKind::Unsupported,
580            Self::BackendFailure { .. } | Self::BackendSource { .. } => ErrorKind::BackendFailure,
581            Self::IoSource { .. } => ErrorKind::Io,
582            Self::RuntimeState { .. }
583            | Self::RuntimeStateSource { .. }
584            | Self::HostAccess { .. } => ErrorKind::RuntimeState,
585            Self::Extension { kind, .. } => *kind,
586            Self::MissingValue { .. } => ErrorKind::RuntimeState,
587            Self::Internal(_) => ErrorKind::Internal,
588        }
589    }
590}
591
592/// Result type alias for runtime tensor operations.
593pub type Result<T> = std::result::Result<T, Error>;