Skip to main content

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;