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>;