tenferro_runtime/error.rs
1//! Error types for the tenferro runtime crate.
2//!
3//! # Examples
4//!
5//! ```rust
6//! use tenferro_runtime::error::{Error, ErrorPhase};
7//!
8//! let err = Error::invalid_argument(
9//! "einsum",
10//! ErrorPhase::GraphBuild,
11//! "subscripts",
12//! "bad label",
13//! );
14//! assert!(err.to_string().contains("bad label"));
15//! ```
16
17use std::error::Error as StdError;
18use std::sync::atomic::{AtomicUsize, Ordering};
19
20use tenferro_ops::{dim_expr::DimExprEvalError, ShapeRelation, SymDimConversionError};
21use tenferro_tensor::{DType, ErrorKind, ValidationError, ValidationKind};
22
23use crate::runtime::{PrepareError, UnsupportedReason};
24
25static NEXT_CONTEXT_ID: AtomicUsize = AtomicUsize::new(1);
26
27/// Boxed source used when a runtime registry or compiler subsystem crosses
28/// the runtime error boundary with a concrete error owned by another crate.
29pub type BoxError = Box<dyn StdError + Send + Sync + 'static>;
30
31/// Borrowed, stable classification of a runtime preparation or execution failure.
32///
33/// The view retains references to the original error payloads and performs no
34/// message formatting or allocation. Match a wildcard because this enum is
35/// non-exhaustive.
36///
37/// # Examples
38///
39/// ```rust
40/// use tenferro_runtime::{Error, ErrorPhase, RuntimeFailureReasonRef};
41///
42/// let error = Error::unsupported("compare", ErrorPhase::Compile, "not available");
43/// assert!(matches!(
44/// error.reason(),
45/// RuntimeFailureReasonRef::UnsupportedOperation { operation: "compare" }
46/// ));
47/// ```
48#[derive(Clone, Copy, Debug, PartialEq)]
49#[non_exhaustive]
50pub enum RuntimeFailureReasonRef<'a> {
51 /// An operation belongs to an extension family with no installed module.
52 MissingExtension { family: &'a str },
53 /// No prepared engine accepts an input at its physical placement.
54 NoInputIngress {
55 input_index: usize,
56 placement: &'a tenferro_tensor::Placement,
57 },
58 /// A provider or tensor backend does not implement an operation.
59 UnsupportedOperation { operation: &'a str },
60 /// A failure has no stable structured classification.
61 Other,
62}
63
64/// Phase at which a runtime failure was discovered.
65///
66/// The phase is independent from [`ErrorKind`]: the same validation fact can
67/// be discovered while building a graph, compiling it for concrete inputs,
68/// or executing a compiled program.
69///
70/// # Examples
71///
72/// ```rust
73/// use tenferro_runtime::ErrorPhase;
74///
75/// assert_ne!(ErrorPhase::GraphBuild, ErrorPhase::Execution);
76/// ```
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
78#[non_exhaustive]
79pub enum ErrorPhase {
80 /// A caller-controlled graph construction check failed.
81 GraphBuild,
82 /// Shape inference or lowering discovered the failure.
83 Compile,
84 /// Input binding or backend execution discovered the failure.
85 Execution,
86}
87
88/// Typed reason that a symbolic shape constraint could not be evaluated.
89///
90/// # Examples
91///
92/// ```rust
93/// use tenferro_runtime::ShapeConstraintEvalError;
94///
95/// let cause = ShapeConstraintEvalError::MissingInput {
96/// input_idx: 2,
97/// input_count: 1,
98/// };
99/// assert_eq!(
100/// cause.to_string(),
101/// "shape expression references input 2, but only 1 inputs were provided"
102/// );
103/// ```
104#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
105pub enum ShapeConstraintEvalError {
106 /// An expression referenced an input shape that was not supplied.
107 #[error(
108 "shape expression references input {input_idx}, but only {input_count} inputs were provided"
109 )]
110 MissingInput {
111 /// Referenced input index.
112 input_idx: usize,
113 /// Number of supplied input shapes.
114 input_count: usize,
115 },
116 /// An expression referenced an axis outside the selected input's rank.
117 #[error("shape expression references input {input_idx} axis {axis}, but its rank is {rank}")]
118 AxisOutOfBounds {
119 /// Referenced input index.
120 input_idx: usize,
121 /// Referenced axis.
122 axis: usize,
123 /// Rank of the selected input.
124 rank: usize,
125 },
126 /// Checked dimension arithmetic overflowed `usize`.
127 #[error("shape expression arithmetic overflowed")]
128 Overflow,
129 /// Checked dimension subtraction underflowed `usize`.
130 #[error("shape expression subtraction underflowed")]
131 Underflow,
132 /// A floor-division divisor evaluated to zero.
133 #[error("shape expression divided by zero")]
134 DivisionByZero,
135}
136
137impl From<DimExprEvalError> for ShapeConstraintEvalError {
138 fn from(error: DimExprEvalError) -> Self {
139 match error {
140 DimExprEvalError::InputOutOfBounds {
141 input_idx,
142 input_count,
143 } => Self::MissingInput {
144 input_idx,
145 input_count,
146 },
147 DimExprEvalError::AxisOutOfBounds {
148 input_idx,
149 axis,
150 rank,
151 } => Self::AxisOutOfBounds {
152 input_idx,
153 axis,
154 rank,
155 },
156 DimExprEvalError::AddOverflow { .. } | DimExprEvalError::MulOverflow { .. } => {
157 Self::Overflow
158 }
159 DimExprEvalError::SubUnderflow { .. } => Self::Underflow,
160 DimExprEvalError::FloorDivByZero { .. } => Self::DivisionByZero,
161 }
162 }
163}
164
165/// Errors produced by einsum, eval, and other tenferro operations.
166///
167/// # Examples
168///
169/// ```rust
170/// use tenferro_runtime::error::{Error, ErrorPhase};
171///
172/// let err = Error::invalid_argument(
173/// "einsum",
174/// ErrorPhase::GraphBuild,
175/// "subscripts",
176/// "rank mismatch",
177/// );
178/// ```
179#[derive(Debug, thiserror::Error)]
180pub enum Error {
181 /// A shared tensor validation fact, annotated with the runtime phase.
182 #[error("{op} ({phase:?}): {source}")]
183 Validation {
184 /// Public operation name.
185 op: &'static str,
186 /// Phase that discovered the validation fact.
187 phase: ErrorPhase,
188 /// Machine-readable validation payload.
189 #[source]
190 source: ValidationError,
191 },
192
193 /// A required input tensor is missing from the inputs map.
194 #[error("missing input: {0}")]
195 MissingInput(String),
196
197 /// Reverse-mode gradient requires a scalar output.
198 #[error("grad requires a scalar output, got shape {shape:?}")]
199 NonScalarGrad { shape: Vec<usize> },
200
201 /// The operation is known not to support the requested input or
202 /// configuration at the phase where it was requested.
203 #[error("{op} ({phase:?}) is unsupported: {message}")]
204 Unsupported {
205 /// Operation that does not provide the requested behavior.
206 op: &'static str,
207 /// Phase that established the unsupported combination.
208 phase: ErrorPhase,
209 /// Human-readable unsupported-operation detail.
210 message: String,
211 },
212
213 /// Runtime tensor execution failed in the backend layer.
214 #[error(transparent)]
215 TensorRuntime(#[from] tenferro_tensor::Error),
216
217 /// A typed extension-domain error crossed a runtime registry boundary.
218 #[error("extension {family} ({phase:?}) failed for {op}: {source}")]
219 Extension {
220 /// Operation that discovered the extension failure.
221 op: &'static str,
222 /// Phase that discovered the extension failure.
223 phase: ErrorPhase,
224 /// Stable extension family identifier.
225 family: &'static str,
226 /// Coarse classification supplied by the extension owner.
227 kind: ErrorKind,
228 /// Original extension-domain source.
229 #[source]
230 source: BoxError,
231 },
232
233 /// Executor, cache, registry, or device state is unavailable or invalid.
234 #[error("{op} ({phase:?}): runtime state failure: {message}")]
235 RuntimeState {
236 /// Operation whose state was unavailable.
237 op: &'static str,
238 /// Phase that discovered the invalid state.
239 phase: ErrorPhase,
240 /// Human-readable state detail.
241 message: String,
242 },
243
244 /// A runtime-state failure retaining a typed source.
245 #[error("{op} ({phase:?}): runtime state failure: {source}")]
246 RuntimeStateSource {
247 /// Operation whose state was unavailable.
248 op: &'static str,
249 /// Phase that discovered the invalid state.
250 phase: ErrorPhase,
251 /// Typed state source.
252 #[source]
253 source: BoxError,
254 },
255
256 /// A primary error with a second typed error retained as suppressed
257 /// metadata.
258 ///
259 /// The standard error source chain follows `primary`. The suppressed
260 /// error is intentionally exposed through [`Error::suppressed`] because
261 /// [`StdError::source`](std::error::Error::source) can represent only one
262 /// source without losing the primary error's semantics.
263 #[error("primary error: {primary}; suppressed error: {suppressed}")]
264 WithSuppressed {
265 /// The operation's primary failure and the standard error-chain source.
266 #[source]
267 primary: Box<Error>,
268 /// A typed secondary failure retained for diagnostics and recovery.
269 suppressed: Box<Error>,
270 },
271
272 /// A runtime event-domain provenance or admission contract failed.
273 #[error("event-domain operation failed: {source}")]
274 EventDomain {
275 /// Structured event-domain failure with expected/actual provenance.
276 #[from]
277 #[source]
278 source: crate::runtime::EventDomainError,
279 },
280
281 /// A `TracedTensor` supplied as a compiled-graph input binding is not a
282 /// placeholder (has attached data).
283 #[error(
284 "binding #{binding_index} is not a placeholder; \
285 only tensors built via input_concrete_shape / input_symbolic_shape \
286 can be bound"
287 )]
288 UnexpectedBinding { binding_index: usize },
289
290 /// A placeholder appearing in the graph has no binding supplied.
291 #[error("placeholder {input_key} has no runtime input binding")]
292 UnboundPlaceholder { input_key: String },
293
294 /// The number of ordered tensors supplied to a compiled graph is invalid.
295 #[error("compiled graph expects {expected} ordered inputs, got {actual}")]
296 GraphInputCountMismatch { expected: usize, actual: usize },
297
298 /// The same placeholder was bound more than once in the `bindings` slice.
299 #[error("placeholder {input_key} was bound more than once")]
300 DuplicateBinding { input_key: String },
301
302 /// A binding tensor's dtype does not match the placeholder's dtype.
303 #[error("binding dtype mismatch for placeholder: expected {expected:?}, got {actual:?}")]
304 PlaceholderDtypeMismatch { expected: DType, actual: DType },
305
306 /// A binding tensor's shape does not match an `input_concrete_shape`
307 /// placeholder's fixed shape.
308 #[error(
309 "binding shape mismatch for concrete-shape placeholder: \
310 expected {expected:?}, got {actual:?}"
311 )]
312 PlaceholderShapeMismatch {
313 expected: Vec<usize>,
314 actual: Vec<usize>,
315 },
316
317 /// A binding tensor dimension exceeds a semantic input's declared bound.
318 #[error("binding dimension {axis} exceeds semantic input upper bound {bound}: got {actual}")]
319 PlaceholderShapeBoundExceeded {
320 /// Axis whose runtime extent exceeded the bound.
321 axis: usize,
322 /// Evaluated upper bound.
323 bound: usize,
324 /// Runtime extent.
325 actual: usize,
326 },
327
328 /// A binding tensor's rank does not match an `input_symbolic_shape`
329 /// placeholder's declared rank.
330 #[error(
331 "binding rank mismatch for symbolic-shape placeholder: \
332 expected rank {expected}, got rank {actual}"
333 )]
334 PlaceholderRankMismatch { expected: usize, actual: usize },
335
336 /// Operation attempted to mix tensors from different eager contexts.
337 #[error(
338 "tensors belong to different eager AD contexts ({lhs} vs {rhs}); \
339 detach into the target context before combining them"
340 )]
341 ContextMismatch { lhs: ContextId, rhs: ContextId },
342
343 /// An AD transform requires a primitive or extension rule that is not
344 /// registered for the requested operation.
345 #[error("unsupported {transform} AD rule for {op}")]
346 UnsupportedAdRule {
347 /// AD transform that requested the rule, such as `grad` or `backward`.
348 transform: &'static str,
349 /// Operation or extension family identifier that has no applicable rule.
350 op: String,
351 },
352
353 /// A typed AD rule source that crossed an external message-only callback.
354 #[error("{transform} AD rule failed: {source}")]
355 AdRuleSource {
356 /// AD transform that requested the rule.
357 transform: &'static str,
358 /// Original typed source from the AD rule context.
359 #[source]
360 source: BoxError,
361 },
362
363 /// A symbolic extension shape equality evaluated to unequal dimensions.
364 #[error(
365 "extension family {family:?} shape constraint at instruction {instruction_index:?} failed: {lhs_expr} ({lhs_value}) {relation:?} {rhs_expr} ({rhs_value})"
366 )]
367 ShapeConstraintViolation {
368 /// Stable extension family identifier.
369 family: &'static str,
370 /// Stable compiled instruction provenance, when assigned.
371 instruction_index: Option<usize>,
372 /// Shape relation that failed.
373 relation: ShapeRelation,
374 /// Normalized left-hand expression.
375 lhs_expr: String,
376 /// Normalized right-hand expression.
377 rhs_expr: String,
378 /// Concrete left-hand value.
379 lhs_value: usize,
380 /// Concrete right-hand value.
381 rhs_value: usize,
382 },
383
384 /// A symbolic extension shape expression could not be evaluated safely.
385 #[error(
386 "extension family {family:?} shape constraint at instruction {instruction_index:?} could not evaluate {expression} for {relation:?}: {cause}"
387 )]
388 ShapeConstraintEvaluation {
389 /// Stable extension family identifier.
390 family: &'static str,
391 /// Stable compiled instruction provenance, when assigned.
392 instruction_index: Option<usize>,
393 /// Shape relation whose expression failed.
394 relation: ShapeRelation,
395 /// Normalized expression that failed.
396 expression: String,
397 /// Typed evaluation failure.
398 #[source]
399 cause: ShapeConstraintEvalError,
400 },
401
402 /// A symbolic dimension could not be converted into the graph's local
403 /// dimension-expression vocabulary.
404 #[error("{op} ({phase:?}): symbolic shape conversion failed: {source}")]
405 SymbolicShapeConversion {
406 /// Operation that requested the symbolic shape conversion.
407 op: &'static str,
408 /// Phase that discovered the invalid symbolic reference.
409 phase: ErrorPhase,
410 /// Typed symbolic-dimension conversion failure.
411 #[source]
412 source: SymDimConversionError,
413 },
414
415 /// A runtime dimension expression could not be evaluated for concrete
416 /// input shapes.
417 #[error("runtime shape expression {expression} could not evaluate: {cause}")]
418 ShapeExpressionEvaluation {
419 /// Expression that failed during execution.
420 expression: String,
421 /// Typed evaluation failure.
422 #[source]
423 cause: ShapeConstraintEvalError,
424 },
425
426 /// An unexpected internal error.
427 #[error("internal error: {0}")]
428 Internal(String),
429}
430
431impl Error {
432 /// Wrap a shared validation payload with its operation and discovery
433 /// phase.
434 ///
435 /// # Errors
436 ///
437 /// This constructor does not fail; callers receive the returned
438 /// [`Error`] value and can inspect its [`Error::kind`] and
439 /// [`Error::phase`].
440 ///
441 /// # Examples
442 ///
443 /// ```rust
444 /// use tenferro_runtime::{Error, ErrorPhase};
445 /// use tenferro_tensor::{ErrorKind, ShapeMismatch, ValidationKind};
446 ///
447 /// let error = Error::validation(
448 /// "reshape",
449 /// ErrorPhase::GraphBuild,
450 /// ShapeMismatch::ReshapeElementCount { from: 2, to: 3 }.into(),
451 /// );
452 /// assert_eq!(
453 /// error.kind(),
454 /// ErrorKind::Validation(ValidationKind::ShapeMismatch)
455 /// );
456 /// assert_eq!(error.phase(), Some(ErrorPhase::GraphBuild));
457 /// ```
458 pub fn validation(op: &'static str, phase: ErrorPhase, source: ValidationError) -> Self {
459 Self::Validation { op, phase, source }
460 }
461
462 /// Construct a validation error for a caller-controlled argument whose
463 /// failure does not have a more specific shared payload.
464 ///
465 /// # Examples
466 ///
467 /// ```rust
468 /// use tenferro_runtime::{Error, ErrorPhase};
469 ///
470 /// let error = Error::invalid_argument(
471 /// "broadcast_in_dim",
472 /// ErrorPhase::GraphBuild,
473 /// "dims",
474 /// "dimension mapping has the wrong length",
475 /// );
476 /// assert!(matches!(error, Error::Validation { .. }));
477 /// ```
478 pub fn invalid_argument(
479 op: &'static str,
480 phase: ErrorPhase,
481 argument: &'static str,
482 message: impl Into<String>,
483 ) -> Self {
484 Self::validation(
485 op,
486 phase,
487 ValidationError::InvalidArgument {
488 argument,
489 message: message.into(),
490 },
491 )
492 }
493
494 /// Construct a dtype-mismatch validation error using the runtime dtype
495 /// vocabulary.
496 ///
497 /// # Examples
498 ///
499 /// ```rust
500 /// use tenferro_runtime::{DType, Error, ErrorPhase};
501 /// use tenferro_tensor::{ErrorKind, ValidationKind};
502 ///
503 /// let error = Error::dtype_mismatch(
504 /// "add",
505 /// ErrorPhase::GraphBuild,
506 /// DType::F32,
507 /// DType::F64,
508 /// );
509 /// assert_eq!(error.kind(), ErrorKind::Validation(ValidationKind::DTypeMismatch));
510 /// ```
511 pub fn dtype_mismatch(
512 op: &'static str,
513 phase: ErrorPhase,
514 expected: DType,
515 actual: DType,
516 ) -> Self {
517 Self::validation(
518 op,
519 phase,
520 ValidationError::DTypeMismatch {
521 expected: core_dtype(expected),
522 actual: core_dtype(actual),
523 },
524 )
525 }
526
527 /// Preserve a typed extension-domain source at the runtime boundary.
528 ///
529 /// # Examples
530 ///
531 /// ```rust
532 /// use std::error::Error as _;
533 /// use tenferro_runtime::{Error, ErrorPhase};
534 /// use tenferro_tensor::ErrorKind;
535 ///
536 /// let source = std::io::Error::new(std::io::ErrorKind::Other, "extension failed");
537 /// let error = Error::extension(
538 /// "einsum",
539 /// ErrorPhase::GraphBuild,
540 /// "example.extension.v1",
541 /// ErrorKind::RuntimeState,
542 /// source,
543 /// );
544 /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
545 /// assert!(error.source().is_some());
546 /// ```
547 pub fn extension<E>(
548 op: &'static str,
549 phase: ErrorPhase,
550 family: &'static str,
551 kind: ErrorKind,
552 source: E,
553 ) -> Self
554 where
555 E: StdError + Send + Sync + 'static,
556 {
557 Self::Extension {
558 op,
559 phase,
560 family,
561 kind,
562 source: Box::new(source),
563 }
564 }
565
566 /// Construct a runtime-state failure for an unavailable or invalid
567 /// executor, cache, registry, or device state.
568 ///
569 /// # Examples
570 ///
571 /// ```rust
572 /// use tenferro_runtime::{Error, ErrorPhase};
573 /// use tenferro_tensor::ErrorKind;
574 ///
575 /// let error = Error::runtime_state(
576 /// "executor",
577 /// ErrorPhase::Execution,
578 /// "the executor is not initialized",
579 /// );
580 /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
581 /// ```
582 pub fn runtime_state(op: &'static str, phase: ErrorPhase, message: impl Into<String>) -> Self {
583 Self::RuntimeState {
584 op,
585 phase,
586 message: message.into(),
587 }
588 }
589
590 /// Preserve a typed source for an unavailable or invalid runtime state.
591 ///
592 /// # Examples
593 ///
594 /// ```rust
595 /// use std::error::Error as _;
596 /// use tenferro_runtime::{Error, ErrorPhase};
597 /// use tenferro_tensor::ErrorKind;
598 ///
599 /// let error = Error::runtime_state_source(
600 /// "metadata",
601 /// ErrorPhase::Compile,
602 /// std::io::Error::other("registry lock poisoned"),
603 /// );
604 /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
605 /// assert!(error.source().is_some());
606 /// ```
607 pub fn runtime_state_source<E>(op: &'static str, phase: ErrorPhase, source: E) -> Self
608 where
609 E: StdError + Send + Sync + 'static,
610 {
611 Self::RuntimeStateSource {
612 op,
613 phase,
614 source: Box::new(source),
615 }
616 }
617
618 /// Retain a typed secondary error while preserving the primary error's
619 /// classification and standard source chain.
620 ///
621 /// # Examples
622 ///
623 /// ```rust
624 /// use tenferro_runtime::{Error, ErrorPhase};
625 ///
626 /// let error = Error::with_suppressed(
627 /// Error::unsupported("backend", ErrorPhase::Execution, "primary"),
628 /// Error::runtime_state("runtime", ErrorPhase::Execution, "suppressed"),
629 /// );
630 /// assert!(error.primary().is_some());
631 /// assert!(error.suppressed().is_some());
632 /// ```
633 pub fn with_suppressed(primary: Self, suppressed: Self) -> Self {
634 Self::WithSuppressed {
635 primary: Box::new(primary),
636 suppressed: Box::new(suppressed),
637 }
638 }
639
640 /// Return the primary error when this value is a suppressed-error
641 /// aggregate.
642 ///
643 /// # Examples
644 ///
645 /// ```rust
646 /// use tenferro_runtime::{Error, ErrorPhase};
647 ///
648 /// let error = Error::with_suppressed(
649 /// Error::unsupported("backend", ErrorPhase::Execution, "primary"),
650 /// Error::runtime_state("runtime", ErrorPhase::Execution, "suppressed"),
651 /// );
652 /// assert_eq!(error.primary().unwrap().phase(), Some(ErrorPhase::Execution));
653 /// ```
654 pub fn primary(&self) -> Option<&Self> {
655 match self {
656 Self::WithSuppressed { primary, .. } => Some(primary),
657 _ => None,
658 }
659 }
660
661 /// Return the typed suppressed error when this value is an aggregate.
662 ///
663 /// # Examples
664 ///
665 /// ```rust
666 /// use tenferro_runtime::{Error, ErrorPhase};
667 ///
668 /// let error = Error::with_suppressed(
669 /// Error::unsupported("backend", ErrorPhase::Execution, "primary"),
670 /// Error::runtime_state("runtime", ErrorPhase::Execution, "suppressed"),
671 /// );
672 /// assert_eq!(error.suppressed().unwrap().phase(), Some(ErrorPhase::Execution));
673 /// ```
674 pub fn suppressed(&self) -> Option<&Self> {
675 match self {
676 Self::WithSuppressed { suppressed, .. } => Some(suppressed),
677 _ => None,
678 }
679 }
680
681 /// Preserve a typed source returned by an AD rule through a callback
682 /// protocol that can carry only a rendered message.
683 ///
684 /// # Examples
685 ///
686 /// ```rust
687 /// use std::error::Error as _;
688 /// use tenferro_runtime::Error;
689 ///
690 /// let error = Error::ad_rule_source(
691 /// "jvp",
692 /// std::io::Error::other("shape metadata missing"),
693 /// );
694 /// assert!(error.source().is_some());
695 /// ```
696 pub fn ad_rule_source<E>(transform: &'static str, source: E) -> Self
697 where
698 E: StdError + Send + Sync + 'static,
699 {
700 Self::AdRuleSource {
701 transform,
702 source: Box::new(source),
703 }
704 }
705
706 /// Construct an operation-level unsupported error with an explicit
707 /// discovery phase.
708 ///
709 /// # Examples
710 ///
711 /// ```rust
712 /// use tenferro_runtime::{Error, ErrorPhase};
713 /// use tenferro_tensor::ErrorKind;
714 ///
715 /// let error = Error::unsupported(
716 /// "compare",
717 /// ErrorPhase::Compile,
718 /// "complex values have no total order",
719 /// );
720 /// assert_eq!(error.kind(), ErrorKind::Unsupported);
721 /// assert_eq!(error.phase(), Some(ErrorPhase::Compile));
722 /// ```
723 pub fn unsupported(op: &'static str, phase: ErrorPhase, message: impl Into<String>) -> Self {
724 Self::Unsupported {
725 op,
726 phase,
727 message: message.into(),
728 }
729 }
730
731 /// Return a borrowed stable reason for this runtime failure.
732 ///
733 /// Classification checks the primary error of [`Error::WithSuppressed`],
734 /// then walks the standard source chain. It never parses display text and
735 /// does not allocate; the original error, kind, phase, display, and source
736 /// chain remain unchanged.
737 ///
738 /// # Examples
739 ///
740 /// ```rust
741 /// use tenferro_runtime::{Error, ErrorPhase, RuntimeFailureReasonRef};
742 ///
743 /// let error = Error::unsupported("compare", ErrorPhase::Compile, "not available");
744 /// assert_eq!(
745 /// error.reason(),
746 /// RuntimeFailureReasonRef::UnsupportedOperation { operation: "compare" }
747 /// );
748 /// assert_eq!(Error::Internal("validation".into()).reason(), RuntimeFailureReasonRef::Other);
749 /// ```
750 pub fn reason(&self) -> RuntimeFailureReasonRef<'_> {
751 if let Self::WithSuppressed { primary, .. } = self {
752 return primary.reason();
753 }
754 if let Some(reason) = direct_reason(self) {
755 return reason;
756 }
757
758 let mut source = StdError::source(self);
759 while let Some(error) = source {
760 if let Some(reason) = error.downcast_ref::<Error>().and_then(direct_reason) {
761 return reason;
762 }
763 if let Some(reason) = prepare_reason(error) {
764 return reason;
765 }
766 source = StdError::source(error);
767 }
768 RuntimeFailureReasonRef::Other
769 }
770
771 /// Return the stable coarse classification of this runtime failure.
772 ///
773 /// # Examples
774 ///
775 /// ```rust
776 /// use tenferro_runtime::{Error, ErrorPhase};
777 /// use tenferro_tensor::{ErrorKind, ValidationError, ValidationKind};
778 ///
779 /// let error = Error::validation(
780 /// "transpose",
781 /// ErrorPhase::GraphBuild,
782 /// ValidationError::AxisOutOfBounds { axis: 2, rank: 2 },
783 /// );
784 /// assert_eq!(error.kind(), ErrorKind::Validation(ValidationKind::AxisOutOfBounds));
785 /// ```
786 pub fn kind(&self) -> ErrorKind {
787 match self {
788 Self::Validation { source, .. } => ErrorKind::Validation(source.kind()),
789 Self::MissingInput(_)
790 | Self::UnexpectedBinding { .. }
791 | Self::UnboundPlaceholder { .. }
792 | Self::DuplicateBinding { .. }
793 | Self::ContextMismatch { .. } => ErrorKind::RuntimeState,
794 Self::NonScalarGrad { .. } => ErrorKind::Validation(ValidationKind::InvalidArgument),
795 Self::GraphInputCountMismatch { .. } => {
796 ErrorKind::Validation(ValidationKind::InvalidArgument)
797 }
798 Self::Unsupported { .. } | Self::UnsupportedAdRule { .. } => ErrorKind::Unsupported,
799 Self::AdRuleSource { .. } => ErrorKind::Validation(ValidationKind::InvalidArgument),
800 Self::TensorRuntime(error) => error.kind(),
801 Self::Extension { kind, .. } => *kind,
802 Self::RuntimeState { .. }
803 | Self::RuntimeStateSource { .. }
804 | Self::EventDomain { .. } => ErrorKind::RuntimeState,
805 Self::WithSuppressed { primary, .. } => primary.kind(),
806 Self::PlaceholderDtypeMismatch { .. } => {
807 ErrorKind::Validation(ValidationKind::DTypeMismatch)
808 }
809 Self::PlaceholderShapeMismatch { .. } | Self::PlaceholderShapeBoundExceeded { .. } => {
810 ErrorKind::Validation(ValidationKind::ShapeMismatch)
811 }
812 Self::PlaceholderRankMismatch { .. } => {
813 ErrorKind::Validation(ValidationKind::RankMismatch)
814 }
815 Self::ShapeConstraintViolation { .. } => {
816 ErrorKind::Validation(ValidationKind::ShapeMismatch)
817 }
818 Self::ShapeConstraintEvaluation { .. } => {
819 ErrorKind::Validation(ValidationKind::InvalidArgument)
820 }
821 Self::SymbolicShapeConversion { .. } => {
822 ErrorKind::Validation(ValidationKind::InvalidArgument)
823 }
824 Self::ShapeExpressionEvaluation { .. } => {
825 ErrorKind::Validation(ValidationKind::InvalidArgument)
826 }
827 Self::Internal(_) => ErrorKind::Internal,
828 }
829 }
830
831 /// Return the discovery phase when this error has one.
832 ///
833 /// # Examples
834 ///
835 /// ```rust
836 /// use tenferro_runtime::{Error, ErrorPhase};
837 /// use tenferro_tensor::ValidationError;
838 ///
839 /// let error = Error::validation(
840 /// "reshape",
841 /// ErrorPhase::Compile,
842 /// ValidationError::RankMismatch { expected: 2, actual: 1 },
843 /// );
844 /// assert_eq!(error.phase(), Some(ErrorPhase::Compile));
845 /// ```
846 pub fn phase(&self) -> Option<ErrorPhase> {
847 match self {
848 Self::Validation { phase, .. } => Some(*phase),
849 Self::TensorRuntime(_) => Some(ErrorPhase::Execution),
850 Self::Unsupported { phase, .. } => Some(*phase),
851 Self::Extension { phase, .. } => Some(*phase),
852 Self::RuntimeState { phase, .. } | Self::RuntimeStateSource { phase, .. } => {
853 Some(*phase)
854 }
855 Self::WithSuppressed { primary, .. } => primary.phase(),
856 Self::AdRuleSource { .. } => Some(ErrorPhase::GraphBuild),
857 Self::PlaceholderDtypeMismatch { .. }
858 | Self::PlaceholderShapeMismatch { .. }
859 | Self::PlaceholderRankMismatch { .. }
860 | Self::GraphInputCountMismatch { .. }
861 | Self::UnexpectedBinding { .. }
862 | Self::UnboundPlaceholder { .. }
863 | Self::DuplicateBinding { .. } => Some(ErrorPhase::Execution),
864 Self::EventDomain { .. } => Some(ErrorPhase::Execution),
865 Self::SymbolicShapeConversion { phase, .. } => Some(*phase),
866 Self::ShapeExpressionEvaluation { .. } => Some(ErrorPhase::Execution),
867 _ => None,
868 }
869 }
870}
871
872fn direct_reason(error: &Error) -> Option<RuntimeFailureReasonRef<'_>> {
873 match error {
874 Error::Unsupported { op, .. } => {
875 Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: op })
876 }
877 Error::UnsupportedAdRule { op, .. } => {
878 Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: op })
879 }
880 Error::TensorRuntime(error) => tensor_reason(error),
881 Error::Extension { family, kind, .. } if *kind == ErrorKind::Unsupported => {
882 Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: family })
883 }
884 _ => None,
885 }
886}
887
888fn tensor_reason(error: &tenferro_tensor::Error) -> Option<RuntimeFailureReasonRef<'_>> {
889 match error {
890 tenferro_tensor::Error::Unsupported { op, .. }
891 | tenferro_tensor::Error::UnsupportedDType { op, .. }
892 | tenferro_tensor::Error::UnsupportedDTypeConversion { op, .. } => {
893 Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: op })
894 }
895 _ => None,
896 }
897}
898
899fn prepare_reason<'a>(error: &'a (dyn StdError + 'static)) -> Option<RuntimeFailureReasonRef<'a>> {
900 let prepare = error.downcast_ref::<PrepareError>()?;
901 Some(match prepare {
902 PrepareError::MissingExtension { family_id } => {
903 RuntimeFailureReasonRef::MissingExtension { family: family_id }
904 }
905 PrepareError::NoInputIngress {
906 input_index,
907 placement,
908 } => RuntimeFailureReasonRef::NoInputIngress {
909 input_index: *input_index,
910 placement,
911 },
912 PrepareError::Unsupported {
913 reason: UnsupportedReason::Operation { operation },
914 } => RuntimeFailureReasonRef::UnsupportedOperation { operation },
915 _ => return None,
916 })
917}
918
919fn core_dtype(dtype: DType) -> tenferro_tensor::core::DType {
920 match dtype {
921 DType::F32 => tenferro_tensor::core::DType::F32,
922 DType::F64 => tenferro_tensor::core::DType::F64,
923 DType::I32 => tenferro_tensor::core::DType::I32,
924 DType::I64 => tenferro_tensor::core::DType::I64,
925 DType::Bool => tenferro_tensor::core::DType::Bool,
926 DType::C32 => tenferro_tensor::core::DType::C32,
927 DType::C64 => tenferro_tensor::core::DType::C64,
928 }
929}
930
931/// Opaque identifier for an eager AD runtime, used in [`Error::ContextMismatch`].
932#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
933pub struct ContextId(usize);
934
935impl ContextId {
936 /// Generate a fresh opaque runtime context identifier.
937 ///
938 /// Runtime implementations use this when constructing a new execution
939 /// context. The value is intentionally opaque and is only useful in error
940 /// reporting and equality checks.
941 pub fn fresh() -> Self {
942 let id = NEXT_CONTEXT_ID.fetch_add(1, Ordering::Relaxed);
943 Self(id)
944 }
945}
946
947impl std::fmt::Display for ContextId {
948 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
949 write!(f, "ctx@{:x}", self.0)
950 }
951}
952
953/// Result type alias for tenferro operations.
954pub type Result<T> = std::result::Result<T, Error>;
955
956#[cfg(test)]
957mod tests;