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