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    /// Consume the failure and return the retained owner with its typed cause.
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// use tenferro_tensor::TypedTensor;
171    ///
172    /// let tensor = TypedTensor::<f32>::from_vec_col_major(vec![1], vec![1.0])?;
173    /// let Err(failure) = tensor.into_complex() else { return Ok(()); };
174    /// let (owner, error) = failure.into_parts();
175    /// assert!(!error.to_string().is_empty());
176    /// assert_eq!(owner.shape(), &[1]);
177    /// # Ok::<(), tenferro_tensor::Error>(())
178    /// ```
179    #[must_use]
180    pub fn into_parts(self) -> (T, Error) {
181        (*self.owner, self.error)
182    }
183}
184
185impl<T: std::fmt::Debug> std::fmt::Display for ReinterpretError<T> {
186    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        write!(formatter, "tensor reinterpretation failed: {}", self.error)
188    }
189}
190
191impl<T: std::fmt::Debug + 'static> std::error::Error for ReinterpretError<T> {}
192
193impl Error {
194    /// Construct an incompatible-shapes validation error.
195    ///
196    /// # Examples
197    ///
198    /// ```rust
199    /// use tenferro_tensor::Error;
200    ///
201    /// let error = Error::shape_mismatch("add", [2, 3], [2, 4]);
202    /// assert!(matches!(error, Error::Validation { .. }));
203    /// ```
204    pub fn shape_mismatch(
205        op: &'static str,
206        lhs: impl Into<Vec<usize>>,
207        rhs: impl Into<Vec<usize>>,
208    ) -> Self {
209        Self::validation(
210            op,
211            tenferro_tensor_core::ShapeMismatch::IncompatibleShapes {
212                lhs: lhs.into().into_iter().collect(),
213                rhs: rhs.into().into_iter().collect(),
214            }
215            .into(),
216        )
217    }
218
219    /// Construct a rank-mismatch validation error.
220    ///
221    /// # Examples
222    ///
223    /// ```rust
224    /// use tenferro_tensor::Error;
225    ///
226    /// let error = Error::rank_mismatch("transpose", 2, 3);
227    /// assert!(matches!(error, Error::Validation { .. }));
228    /// ```
229    pub fn rank_mismatch(op: &'static str, expected: usize, actual: usize) -> Self {
230        Self::validation(op, ValidationError::RankMismatch { expected, actual })
231    }
232
233    /// Construct an axis-out-of-bounds validation error.
234    ///
235    /// # Examples
236    ///
237    /// ```rust
238    /// use tenferro_tensor::Error;
239    ///
240    /// let error = Error::axis_out_of_bounds("sum", 2, 2);
241    /// assert!(matches!(error, Error::Validation { .. }));
242    /// ```
243    pub fn axis_out_of_bounds(op: &'static str, axis: usize, rank: usize) -> Self {
244        Self::validation(op, ValidationError::AxisOutOfBounds { axis, rank })
245    }
246
247    /// Construct a duplicate-axis validation error.
248    ///
249    /// # Examples
250    ///
251    /// ```rust
252    /// use tenferro_tensor::Error;
253    ///
254    /// let error = Error::duplicate_axis("transpose", 1, "permutation");
255    /// assert!(matches!(error, Error::Validation { .. }));
256    /// ```
257    pub fn duplicate_axis(op: &'static str, axis: usize, role: &'static str) -> Self {
258        Self::validation(op, ValidationError::DuplicateAxis { axis, role })
259    }
260
261    /// Construct a dtype-mismatch validation error.
262    ///
263    /// # Examples
264    ///
265    /// ```rust
266    /// use tenferro_tensor::{DType, Error};
267    ///
268    /// let error = Error::dtype_mismatch("add", DType::F32, DType::F64);
269    /// assert!(matches!(error, Error::Validation { .. }));
270    /// ```
271    pub fn dtype_mismatch(op: &'static str, expected: crate::DType, actual: crate::DType) -> Self {
272        Self::validation(op, ValidationError::DTypeMismatch { expected, actual })
273    }
274
275    /// Wrap shared tensor validation with the operation that requested it.
276    ///
277    /// # Examples
278    ///
279    /// ```rust
280    /// use tenferro_tensor::{Error, ValidationError};
281    ///
282    /// let error = Error::validation(
283    ///     "transpose",
284    ///     ValidationError::AxisOutOfBounds { axis: 2, rank: 2 },
285    /// );
286    /// assert!(matches!(error, Error::Validation { op: "transpose", .. }));
287    /// ```
288    pub fn validation(op: &'static str, source: ValidationError) -> Self {
289        Self::Validation { op, source }
290    }
291
292    /// Construct a structured invalid-argument validation error.
293    ///
294    /// # Examples
295    ///
296    /// ```rust
297    /// use tenferro_tensor::{Error, ErrorKind, ValidationKind};
298    ///
299    /// let error = Error::invalid_argument("slice", "step", "must be non-zero");
300    /// assert_eq!(error.kind(), ErrorKind::Validation(ValidationKind::InvalidArgument));
301    /// ```
302    pub fn invalid_argument(
303        op: &'static str,
304        argument: &'static str,
305        message: impl Into<String>,
306    ) -> Self {
307        Self::validation(
308            op,
309            ValidationError::InvalidArgument {
310                argument,
311                message: message.into(),
312            },
313        )
314    }
315
316    /// Construct an unsupported dtype conversion error.
317    ///
318    /// # Examples
319    ///
320    /// ```rust
321    /// let error = tenferro_tensor::Error::unsupported_dtype_conversion(
322    ///     "convert",
323    ///     tenferro_tensor::DType::F64,
324    ///     tenferro_tensor::DType::I32,
325    ///     "lossy conversion is disabled",
326    /// );
327    /// assert!(matches!(
328    ///     error,
329    ///     tenferro_tensor::Error::UnsupportedDTypeConversion { .. }
330    /// ));
331    /// ```
332    pub fn unsupported_dtype_conversion(
333        op: &'static str,
334        from: crate::DType,
335        to: crate::DType,
336        message: impl Into<String>,
337    ) -> Self {
338        Self::UnsupportedDTypeConversion {
339            op,
340            from,
341            to,
342            message: message.into(),
343        }
344    }
345
346    /// Construct an operation-level unsupported-dtype error.
347    ///
348    /// This is for an operation that cannot run for the supplied dtype. It is
349    /// deliberately distinct from [`Error::unsupported_dtype_conversion`],
350    /// which is reserved for an actual from-dtype to to-dtype conversion.
351    ///
352    /// # Examples
353    ///
354    /// ```rust
355    /// let error = tenferro_tensor::Error::unsupported_dtype(
356    ///     "exp",
357    ///     tenferro_tensor::DType::I64,
358    ///     "integer exponentials are not implemented",
359    /// );
360    /// assert!(matches!(
361    ///     error,
362    ///     tenferro_tensor::Error::UnsupportedDType {
363    ///         op: "exp",
364    ///         dtype: tenferro_tensor::DType::I64,
365    ///         ..
366    ///     }
367    /// ));
368    /// ```
369    pub fn unsupported_dtype(
370        op: &'static str,
371        dtype: crate::DType,
372        message: impl Into<String>,
373    ) -> Self {
374        Self::UnsupportedDType {
375            op,
376            dtype,
377            message: message.into(),
378        }
379    }
380
381    /// Construct a structured unsupported-operation error.
382    ///
383    /// Use this for an operation or execution surface that is not implemented
384    /// by the selected backend. Dtype conversion failures use
385    /// [`Error::unsupported_dtype_conversion`] instead, and operation-specific
386    /// typed reasons should use [`Error::extension`] with `ErrorKind::Unsupported`.
387    ///
388    /// # Examples
389    ///
390    /// ```rust
391    /// let error = tenferro_tensor::Error::unsupported(
392    ///     "full_piv_lu",
393    ///     "backend has no implementation",
394    /// );
395    /// assert!(matches!(
396    ///     error,
397    ///     tenferro_tensor::Error::Unsupported { op: "full_piv_lu", .. }
398    /// ));
399    /// ```
400    pub fn unsupported(op: &'static str, message: impl Into<String>) -> Self {
401        Self::Unsupported {
402            op,
403            message: message.into(),
404        }
405    }
406
407    /// Construct a text-only backend failure.
408    ///
409    /// Use [`Error::backend_source`] when a typed source is available.
410    ///
411    /// # Examples
412    ///
413    /// ```rust
414    /// let error = tenferro_tensor::Error::backend_failure(
415    ///     "matmul",
416    ///     "backend rejected launch",
417    /// );
418    /// assert!(matches!(
419    ///     error,
420    ///     tenferro_tensor::Error::BackendFailure { op: "matmul", .. }
421    /// ));
422    /// ```
423    pub fn backend_failure(op: &'static str, message: impl Into<String>) -> Self {
424        Self::BackendFailure {
425            op,
426            message: message.into(),
427        }
428    }
429
430    /// Construct a backend failure while preserving its typed source.
431    ///
432    /// # Examples
433    ///
434    /// ```rust
435    /// let error = tenferro_tensor::Error::backend_source(
436    ///     "load",
437    ///     std::io::Error::other("read failed"),
438    /// );
439    /// assert!(std::error::Error::source(&error).is_some());
440    /// ```
441    pub fn backend_source<E>(op: &'static str, source: E) -> Self
442    where
443        E: StdError + Send + Sync + 'static,
444    {
445        Self::BackendSource {
446            op,
447            source: Box::new(source),
448        }
449    }
450
451    /// Construct an I/O failure while preserving its typed source.
452    ///
453    /// I/O errors are intentionally separate from backend failures: callers
454    /// can classify them as [`ErrorKind::Io`] without parsing a message.
455    ///
456    /// # Examples
457    ///
458    /// ```rust
459    /// use tenferro_tensor::{Error, ErrorKind};
460    ///
461    /// let error = Error::io_source("load", std::io::Error::other("read failed"));
462    /// assert_eq!(error.kind(), ErrorKind::Io);
463    /// assert!(std::error::Error::source(&error).is_some());
464    /// ```
465    pub fn io_source<E>(op: &'static str, source: E) -> Self
466    where
467        E: StdError + Send + Sync + 'static,
468    {
469        Self::IoSource {
470            op,
471            source: Box::new(source),
472        }
473    }
474
475    /// Construct a runtime-state failure when no typed source exists.
476    ///
477    /// Use this for missing, uninitialized, or invalid execution state. It is
478    /// distinct from [`Error::backend_failure`], which is reserved for
479    /// vendor/backend status text.
480    ///
481    /// # Examples
482    ///
483    /// ```rust
484    /// use tenferro_tensor::{Error, ErrorKind};
485    ///
486    /// let error = Error::runtime_state("execute", "backend session is not initialized");
487    /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
488    /// ```
489    pub fn runtime_state(op: &'static str, message: impl Into<String>) -> Self {
490        Self::RuntimeState {
491            op,
492            message: message.into(),
493        }
494    }
495
496    /// Construct a runtime-state failure while preserving a typed source.
497    ///
498    /// # Examples
499    ///
500    /// ```rust
501    /// use tenferro_tensor::{Error, ErrorKind};
502    ///
503    /// let error = Error::runtime_state_source(
504    ///     "execute",
505    ///     std::io::Error::other("executor lock poisoned"),
506    /// );
507    /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
508    /// assert!(std::error::Error::source(&error).is_some());
509    /// ```
510    pub fn runtime_state_source<E>(op: &'static str, source: E) -> Self
511    where
512        E: StdError + Send + Sync + 'static,
513    {
514        Self::RuntimeStateSource {
515            op,
516            source: Box::new(source),
517        }
518    }
519
520    /// Construct an extension failure while preserving its typed source and
521    /// coarse classification.
522    ///
523    /// # Examples
524    ///
525    /// ```rust
526    /// use std::error::Error as _;
527    /// use tenferro_tensor::{Error, ErrorKind};
528    ///
529    /// let error = Error::extension(
530    ///     "einsum",
531    ///     "einsum",
532    ///     ErrorKind::Internal,
533    ///     std::io::Error::other("planner failed"),
534    /// );
535    /// assert!(error.source().is_some());
536    /// ```
537    pub fn extension<E>(op: &'static str, family: &'static str, kind: ErrorKind, source: E) -> Self
538    where
539        E: StdError + Send + Sync + 'static,
540    {
541        Self::Extension {
542            op,
543            family,
544            kind,
545            source: Box::new(source),
546        }
547    }
548
549    /// Preserve a typed guarded-host-access failure.
550    ///
551    /// # Examples
552    ///
553    /// ```rust
554    /// use tenferro_tensor::{Error, HostAccessError};
555    ///
556    /// let error = Error::host_access(
557    ///     "map",
558    ///     HostAccessError::Unsupported { backend: "opaque" },
559    /// );
560    /// assert!(matches!(error, Error::HostAccess { .. }));
561    /// ```
562    pub fn host_access(op: &'static str, source: crate::HostAccessError) -> Self {
563        Self::HostAccess { op, source }
564    }
565
566    /// Return the stable coarse classification for this tensor failure.
567    ///
568    /// # Examples
569    ///
570    /// ```rust
571    /// use tenferro_tensor::{Error, ErrorKind, ValidationError, ValidationKind};
572    /// use tenferro_tensor::core::DType;
573    ///
574    /// let error = Error::validation(
575    ///     "add",
576    ///     ValidationError::DTypeMismatch {
577    ///         expected: DType::F32,
578    ///         actual: DType::F64,
579    ///     },
580    /// );
581    /// assert_eq!(error.kind(), ErrorKind::Validation(ValidationKind::DTypeMismatch));
582    /// ```
583    pub fn kind(&self) -> ErrorKind {
584        match self {
585            Self::Validation { source, .. } => ErrorKind::Validation(source.kind()),
586            Self::UnsupportedDTypeConversion { .. }
587            | Self::UnsupportedDType { .. }
588            | Self::Unsupported { .. } => ErrorKind::Unsupported,
589            Self::BackendFailure { .. } | Self::BackendSource { .. } => ErrorKind::BackendFailure,
590            Self::IoSource { .. } => ErrorKind::Io,
591            Self::RuntimeState { .. }
592            | Self::RuntimeStateSource { .. }
593            | Self::HostAccess { .. } => ErrorKind::RuntimeState,
594            Self::Extension { kind, .. } => *kind,
595            Self::MissingValue { .. } => ErrorKind::RuntimeState,
596            Self::Internal(_) => ErrorKind::Internal,
597        }
598    }
599}
600
601/// Result type alias for runtime tensor operations.
602pub type Result<T> = std::result::Result<T, Error>;