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 { expected, actual },
521 )
522 }
523
524 /// Preserve a typed extension-domain source at the runtime boundary.
525 ///
526 /// # Examples
527 ///
528 /// ```rust
529 /// use std::error::Error as _;
530 /// use tenferro_runtime::{Error, ErrorPhase};
531 /// use tenferro_tensor::ErrorKind;
532 ///
533 /// let source = std::io::Error::new(std::io::ErrorKind::Other, "extension failed");
534 /// let error = Error::extension(
535 /// "einsum",
536 /// ErrorPhase::GraphBuild,
537 /// "example.extension.v1",
538 /// ErrorKind::RuntimeState,
539 /// source,
540 /// );
541 /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
542 /// assert!(error.source().is_some());
543 /// ```
544 pub fn extension<E>(
545 op: &'static str,
546 phase: ErrorPhase,
547 family: &'static str,
548 kind: ErrorKind,
549 source: E,
550 ) -> Self
551 where
552 E: StdError + Send + Sync + 'static,
553 {
554 Self::Extension {
555 op,
556 phase,
557 family,
558 kind,
559 source: Box::new(source),
560 }
561 }
562
563 /// Construct a runtime-state failure for an unavailable or invalid
564 /// executor, cache, registry, or device state.
565 ///
566 /// # Examples
567 ///
568 /// ```rust
569 /// use tenferro_runtime::{Error, ErrorPhase};
570 /// use tenferro_tensor::ErrorKind;
571 ///
572 /// let error = Error::runtime_state(
573 /// "executor",
574 /// ErrorPhase::Execution,
575 /// "the executor is not initialized",
576 /// );
577 /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
578 /// ```
579 pub fn runtime_state(op: &'static str, phase: ErrorPhase, message: impl Into<String>) -> Self {
580 Self::RuntimeState {
581 op,
582 phase,
583 message: message.into(),
584 }
585 }
586
587 /// Preserve a typed source for an unavailable or invalid runtime state.
588 ///
589 /// # Examples
590 ///
591 /// ```rust
592 /// use std::error::Error as _;
593 /// use tenferro_runtime::{Error, ErrorPhase};
594 /// use tenferro_tensor::ErrorKind;
595 ///
596 /// let error = Error::runtime_state_source(
597 /// "metadata",
598 /// ErrorPhase::Compile,
599 /// std::io::Error::other("registry lock poisoned"),
600 /// );
601 /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
602 /// assert!(error.source().is_some());
603 /// ```
604 pub fn runtime_state_source<E>(op: &'static str, phase: ErrorPhase, source: E) -> Self
605 where
606 E: StdError + Send + Sync + 'static,
607 {
608 Self::RuntimeStateSource {
609 op,
610 phase,
611 source: Box::new(source),
612 }
613 }
614
615 /// Retain a typed secondary error while preserving the primary error's
616 /// classification and standard source chain.
617 ///
618 /// # Examples
619 ///
620 /// ```rust
621 /// use tenferro_runtime::{Error, ErrorPhase};
622 ///
623 /// let error = Error::with_suppressed(
624 /// Error::unsupported("backend", ErrorPhase::Execution, "primary"),
625 /// Error::runtime_state("runtime", ErrorPhase::Execution, "suppressed"),
626 /// );
627 /// assert!(error.primary().is_some());
628 /// assert!(error.suppressed().is_some());
629 /// ```
630 pub fn with_suppressed(primary: Self, suppressed: Self) -> Self {
631 Self::WithSuppressed {
632 primary: Box::new(primary),
633 suppressed: Box::new(suppressed),
634 }
635 }
636
637 /// Return the primary error when this value is a suppressed-error
638 /// aggregate.
639 ///
640 /// # Examples
641 ///
642 /// ```rust
643 /// use tenferro_runtime::{Error, ErrorPhase};
644 ///
645 /// let error = Error::with_suppressed(
646 /// Error::unsupported("backend", ErrorPhase::Execution, "primary"),
647 /// Error::runtime_state("runtime", ErrorPhase::Execution, "suppressed"),
648 /// );
649 /// assert_eq!(error.primary().unwrap().phase(), Some(ErrorPhase::Execution));
650 /// ```
651 pub fn primary(&self) -> Option<&Self> {
652 match self {
653 Self::WithSuppressed { primary, .. } => Some(primary),
654 _ => None,
655 }
656 }
657
658 /// Return the typed suppressed error when this value is an aggregate.
659 ///
660 /// # Examples
661 ///
662 /// ```rust
663 /// use tenferro_runtime::{Error, ErrorPhase};
664 ///
665 /// let error = Error::with_suppressed(
666 /// Error::unsupported("backend", ErrorPhase::Execution, "primary"),
667 /// Error::runtime_state("runtime", ErrorPhase::Execution, "suppressed"),
668 /// );
669 /// assert_eq!(error.suppressed().unwrap().phase(), Some(ErrorPhase::Execution));
670 /// ```
671 pub fn suppressed(&self) -> Option<&Self> {
672 match self {
673 Self::WithSuppressed { suppressed, .. } => Some(suppressed),
674 _ => None,
675 }
676 }
677
678 /// Preserve a typed source returned by an AD rule through a callback
679 /// protocol that can carry only a rendered message.
680 ///
681 /// # Examples
682 ///
683 /// ```rust
684 /// use std::error::Error as _;
685 /// use tenferro_runtime::Error;
686 ///
687 /// let error = Error::ad_rule_source(
688 /// "jvp",
689 /// std::io::Error::other("shape metadata missing"),
690 /// );
691 /// assert!(error.source().is_some());
692 /// ```
693 pub fn ad_rule_source<E>(transform: &'static str, source: E) -> Self
694 where
695 E: StdError + Send + Sync + 'static,
696 {
697 Self::AdRuleSource {
698 transform,
699 source: Box::new(source),
700 }
701 }
702
703 /// Construct an operation-level unsupported error with an explicit
704 /// discovery phase.
705 ///
706 /// # Examples
707 ///
708 /// ```rust
709 /// use tenferro_runtime::{Error, ErrorPhase};
710 /// use tenferro_tensor::ErrorKind;
711 ///
712 /// let error = Error::unsupported(
713 /// "compare",
714 /// ErrorPhase::Compile,
715 /// "complex values have no total order",
716 /// );
717 /// assert_eq!(error.kind(), ErrorKind::Unsupported);
718 /// assert_eq!(error.phase(), Some(ErrorPhase::Compile));
719 /// ```
720 pub fn unsupported(op: &'static str, phase: ErrorPhase, message: impl Into<String>) -> Self {
721 Self::Unsupported {
722 op,
723 phase,
724 message: message.into(),
725 }
726 }
727
728 /// Return a borrowed stable reason for this runtime failure.
729 ///
730 /// Classification checks the primary error of [`Error::WithSuppressed`],
731 /// then walks the standard source chain. It never parses display text and
732 /// does not allocate; the original error, kind, phase, display, and source
733 /// chain remain unchanged.
734 ///
735 /// # Examples
736 ///
737 /// ```rust
738 /// use tenferro_runtime::{Error, ErrorPhase, RuntimeFailureReasonRef};
739 ///
740 /// let error = Error::unsupported("compare", ErrorPhase::Compile, "not available");
741 /// assert_eq!(
742 /// error.reason(),
743 /// RuntimeFailureReasonRef::UnsupportedOperation { operation: "compare" }
744 /// );
745 /// assert_eq!(Error::Internal("validation".into()).reason(), RuntimeFailureReasonRef::Other);
746 /// ```
747 pub fn reason(&self) -> RuntimeFailureReasonRef<'_> {
748 if let Self::WithSuppressed { primary, .. } = self {
749 return primary.reason();
750 }
751 if let Some(reason) = direct_reason(self) {
752 return reason;
753 }
754
755 let mut source = StdError::source(self);
756 while let Some(error) = source {
757 if let Some(reason) = error.downcast_ref::<Error>().and_then(direct_reason) {
758 return reason;
759 }
760 if let Some(reason) = prepare_reason(error) {
761 return reason;
762 }
763 source = StdError::source(error);
764 }
765 RuntimeFailureReasonRef::Other
766 }
767
768 /// Return the stable coarse classification of this runtime failure.
769 ///
770 /// # Examples
771 ///
772 /// ```rust
773 /// use tenferro_runtime::{Error, ErrorPhase};
774 /// use tenferro_tensor::{ErrorKind, ValidationError, ValidationKind};
775 ///
776 /// let error = Error::validation(
777 /// "transpose",
778 /// ErrorPhase::GraphBuild,
779 /// ValidationError::AxisOutOfBounds { axis: 2, rank: 2 },
780 /// );
781 /// assert_eq!(error.kind(), ErrorKind::Validation(ValidationKind::AxisOutOfBounds));
782 /// ```
783 pub fn kind(&self) -> ErrorKind {
784 match self {
785 Self::Validation { source, .. } => ErrorKind::Validation(source.kind()),
786 Self::MissingInput(_)
787 | Self::UnexpectedBinding { .. }
788 | Self::UnboundPlaceholder { .. }
789 | Self::DuplicateBinding { .. }
790 | Self::ContextMismatch { .. } => ErrorKind::RuntimeState,
791 Self::NonScalarGrad { .. } => ErrorKind::Validation(ValidationKind::InvalidArgument),
792 Self::GraphInputCountMismatch { .. } => {
793 ErrorKind::Validation(ValidationKind::InvalidArgument)
794 }
795 Self::Unsupported { .. } | Self::UnsupportedAdRule { .. } => ErrorKind::Unsupported,
796 Self::AdRuleSource { .. } => ErrorKind::Validation(ValidationKind::InvalidArgument),
797 Self::TensorRuntime(error) => error.kind(),
798 Self::Extension { kind, .. } => *kind,
799 Self::RuntimeState { .. }
800 | Self::RuntimeStateSource { .. }
801 | Self::EventDomain { .. } => ErrorKind::RuntimeState,
802 Self::WithSuppressed { primary, .. } => primary.kind(),
803 Self::PlaceholderDtypeMismatch { .. } => {
804 ErrorKind::Validation(ValidationKind::DTypeMismatch)
805 }
806 Self::PlaceholderShapeMismatch { .. } | Self::PlaceholderShapeBoundExceeded { .. } => {
807 ErrorKind::Validation(ValidationKind::ShapeMismatch)
808 }
809 Self::PlaceholderRankMismatch { .. } => {
810 ErrorKind::Validation(ValidationKind::RankMismatch)
811 }
812 Self::ShapeConstraintViolation { .. } => {
813 ErrorKind::Validation(ValidationKind::ShapeMismatch)
814 }
815 Self::ShapeConstraintEvaluation { .. } => {
816 ErrorKind::Validation(ValidationKind::InvalidArgument)
817 }
818 Self::SymbolicShapeConversion { .. } => {
819 ErrorKind::Validation(ValidationKind::InvalidArgument)
820 }
821 Self::ShapeExpressionEvaluation { .. } => {
822 ErrorKind::Validation(ValidationKind::InvalidArgument)
823 }
824 Self::Internal(_) => ErrorKind::Internal,
825 }
826 }
827
828 /// Return the discovery phase when this error has one.
829 ///
830 /// # Examples
831 ///
832 /// ```rust
833 /// use tenferro_runtime::{Error, ErrorPhase};
834 /// use tenferro_tensor::ValidationError;
835 ///
836 /// let error = Error::validation(
837 /// "reshape",
838 /// ErrorPhase::Compile,
839 /// ValidationError::RankMismatch { expected: 2, actual: 1 },
840 /// );
841 /// assert_eq!(error.phase(), Some(ErrorPhase::Compile));
842 /// ```
843 pub fn phase(&self) -> Option<ErrorPhase> {
844 match self {
845 Self::Validation { phase, .. } => Some(*phase),
846 Self::TensorRuntime(_) => Some(ErrorPhase::Execution),
847 Self::Unsupported { phase, .. } => Some(*phase),
848 Self::Extension { phase, .. } => Some(*phase),
849 Self::RuntimeState { phase, .. } | Self::RuntimeStateSource { phase, .. } => {
850 Some(*phase)
851 }
852 Self::WithSuppressed { primary, .. } => primary.phase(),
853 Self::AdRuleSource { .. } => Some(ErrorPhase::GraphBuild),
854 Self::PlaceholderDtypeMismatch { .. }
855 | Self::PlaceholderShapeMismatch { .. }
856 | Self::PlaceholderRankMismatch { .. }
857 | Self::GraphInputCountMismatch { .. }
858 | Self::UnexpectedBinding { .. }
859 | Self::UnboundPlaceholder { .. }
860 | Self::DuplicateBinding { .. } => Some(ErrorPhase::Execution),
861 Self::EventDomain { .. } => Some(ErrorPhase::Execution),
862 Self::SymbolicShapeConversion { phase, .. } => Some(*phase),
863 Self::ShapeExpressionEvaluation { .. } => Some(ErrorPhase::Execution),
864 _ => None,
865 }
866 }
867}
868
869fn direct_reason(error: &Error) -> Option<RuntimeFailureReasonRef<'_>> {
870 match error {
871 Error::Unsupported { op, .. } => {
872 Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: op })
873 }
874 Error::UnsupportedAdRule { op, .. } => {
875 Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: op })
876 }
877 Error::TensorRuntime(error) => tensor_reason(error),
878 Error::Extension { family, kind, .. } if *kind == ErrorKind::Unsupported => {
879 Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: family })
880 }
881 _ => None,
882 }
883}
884
885fn tensor_reason(error: &tenferro_tensor::Error) -> Option<RuntimeFailureReasonRef<'_>> {
886 match error {
887 tenferro_tensor::Error::Unsupported { op, .. }
888 | tenferro_tensor::Error::UnsupportedDType { op, .. }
889 | tenferro_tensor::Error::UnsupportedDTypeConversion { op, .. } => {
890 Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: op })
891 }
892 _ => None,
893 }
894}
895
896fn prepare_reason<'a>(error: &'a (dyn StdError + 'static)) -> Option<RuntimeFailureReasonRef<'a>> {
897 let prepare = error.downcast_ref::<PrepareError>()?;
898 Some(match prepare {
899 PrepareError::MissingExtension { family_id } => {
900 RuntimeFailureReasonRef::MissingExtension { family: family_id }
901 }
902 PrepareError::NoInputIngress {
903 input_index,
904 placement,
905 } => RuntimeFailureReasonRef::NoInputIngress {
906 input_index: *input_index,
907 placement,
908 },
909 PrepareError::Unsupported {
910 reason: UnsupportedReason::Operation { operation },
911 } => RuntimeFailureReasonRef::UnsupportedOperation { operation },
912 _ => return None,
913 })
914}
915
916/// Opaque identifier for an eager AD runtime, used in [`Error::ContextMismatch`].
917#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
918pub struct ContextId(usize);
919
920impl ContextId {
921 /// Generate a fresh opaque runtime context identifier.
922 ///
923 /// Runtime implementations use this when constructing a new execution
924 /// context. The value is intentionally opaque and is only useful in error
925 /// reporting and equality checks.
926 pub fn fresh() -> Self {
927 let id = NEXT_CONTEXT_ID.fetch_add(1, Ordering::Relaxed);
928 Self(id)
929 }
930}
931
932impl std::fmt::Display for ContextId {
933 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
934 write!(f, "ctx@{:x}", self.0)
935 }
936}
937
938/// Result type alias for tenferro operations.
939pub type Result<T> = std::result::Result<T, Error>;
940
941#[cfg(test)]
942mod tests;