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
23static NEXT_CONTEXT_ID: AtomicUsize = AtomicUsize::new(1);
24
25/// Boxed source used when a runtime registry or compiler subsystem crosses
26/// the runtime error boundary with a concrete error owned by another crate.
27pub type BoxError = Box<dyn StdError + Send + Sync + 'static>;
28
29/// Phase at which a runtime failure was discovered.
30///
31/// The phase is independent from [`ErrorKind`]: the same validation fact can
32/// be discovered while building a graph, compiling it for concrete inputs,
33/// or executing a compiled program.
34///
35/// # Examples
36///
37/// ```rust
38/// use tenferro_runtime::ErrorPhase;
39///
40/// assert_ne!(ErrorPhase::GraphBuild, ErrorPhase::Execution);
41/// ```
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43#[non_exhaustive]
44pub enum ErrorPhase {
45    /// A caller-controlled graph construction check failed.
46    GraphBuild,
47    /// Shape inference or lowering discovered the failure.
48    Compile,
49    /// Input binding or backend execution discovered the failure.
50    Execution,
51}
52
53/// Typed reason that a symbolic shape constraint could not be evaluated.
54///
55/// # Examples
56///
57/// ```rust
58/// use tenferro_runtime::ShapeConstraintEvalError;
59///
60/// let cause = ShapeConstraintEvalError::MissingInput {
61///     input_idx: 2,
62///     input_count: 1,
63/// };
64/// assert_eq!(
65///     cause.to_string(),
66///     "shape expression references input 2, but only 1 inputs were provided"
67/// );
68/// ```
69#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
70pub enum ShapeConstraintEvalError {
71    /// An expression referenced an input shape that was not supplied.
72    #[error(
73        "shape expression references input {input_idx}, but only {input_count} inputs were provided"
74    )]
75    MissingInput {
76        /// Referenced input index.
77        input_idx: usize,
78        /// Number of supplied input shapes.
79        input_count: usize,
80    },
81    /// An expression referenced an axis outside the selected input's rank.
82    #[error("shape expression references input {input_idx} axis {axis}, but its rank is {rank}")]
83    AxisOutOfBounds {
84        /// Referenced input index.
85        input_idx: usize,
86        /// Referenced axis.
87        axis: usize,
88        /// Rank of the selected input.
89        rank: usize,
90    },
91    /// Checked dimension arithmetic overflowed `usize`.
92    #[error("shape expression arithmetic overflowed")]
93    Overflow,
94    /// Checked dimension subtraction underflowed `usize`.
95    #[error("shape expression subtraction underflowed")]
96    Underflow,
97    /// A floor-division divisor evaluated to zero.
98    #[error("shape expression divided by zero")]
99    DivisionByZero,
100}
101
102impl From<DimExprEvalError> for ShapeConstraintEvalError {
103    fn from(error: DimExprEvalError) -> Self {
104        match error {
105            DimExprEvalError::InputOutOfBounds {
106                input_idx,
107                input_count,
108            } => Self::MissingInput {
109                input_idx,
110                input_count,
111            },
112            DimExprEvalError::AxisOutOfBounds {
113                input_idx,
114                axis,
115                rank,
116            } => Self::AxisOutOfBounds {
117                input_idx,
118                axis,
119                rank,
120            },
121            DimExprEvalError::AddOverflow { .. } | DimExprEvalError::MulOverflow { .. } => {
122                Self::Overflow
123            }
124            DimExprEvalError::SubUnderflow { .. } => Self::Underflow,
125            DimExprEvalError::FloorDivByZero { .. } => Self::DivisionByZero,
126        }
127    }
128}
129
130/// Errors produced by einsum, eval, and other tenferro operations.
131///
132/// # Examples
133///
134/// ```rust
135/// use tenferro_runtime::error::{Error, ErrorPhase};
136///
137/// let err = Error::invalid_argument(
138///     "einsum",
139///     ErrorPhase::GraphBuild,
140///     "subscripts",
141///     "rank mismatch",
142/// );
143/// ```
144#[derive(Debug, thiserror::Error)]
145pub enum Error {
146    /// A shared tensor validation fact, annotated with the runtime phase.
147    #[error("{op} ({phase:?}): {source}")]
148    Validation {
149        /// Public operation name.
150        op: &'static str,
151        /// Phase that discovered the validation fact.
152        phase: ErrorPhase,
153        /// Machine-readable validation payload.
154        #[source]
155        source: ValidationError,
156    },
157
158    /// A required input tensor is missing from the inputs map.
159    #[error("missing input: {0}")]
160    MissingInput(String),
161
162    /// Reverse-mode gradient requires a scalar output.
163    #[error("grad requires a scalar output, got shape {shape:?}")]
164    NonScalarGrad { shape: Vec<usize> },
165
166    /// The operation is known not to support the requested input or
167    /// configuration at the phase where it was requested.
168    #[error("{op} ({phase:?}) is unsupported: {message}")]
169    Unsupported {
170        /// Operation that does not provide the requested behavior.
171        op: &'static str,
172        /// Phase that established the unsupported combination.
173        phase: ErrorPhase,
174        /// Human-readable unsupported-operation detail.
175        message: String,
176    },
177
178    /// Runtime tensor execution failed in the backend layer.
179    #[error(transparent)]
180    TensorRuntime(#[from] tenferro_tensor::Error),
181
182    /// A typed extension-domain error crossed a runtime registry boundary.
183    #[error("extension {family} ({phase:?}) failed for {op}: {source}")]
184    Extension {
185        /// Operation that discovered the extension failure.
186        op: &'static str,
187        /// Phase that discovered the extension failure.
188        phase: ErrorPhase,
189        /// Stable extension family identifier.
190        family: &'static str,
191        /// Coarse classification supplied by the extension owner.
192        kind: ErrorKind,
193        /// Original extension-domain source.
194        #[source]
195        source: BoxError,
196    },
197
198    /// Executor, cache, registry, or device state is unavailable or invalid.
199    #[error("{op} ({phase:?}): runtime state failure: {message}")]
200    RuntimeState {
201        /// Operation whose state was unavailable.
202        op: &'static str,
203        /// Phase that discovered the invalid state.
204        phase: ErrorPhase,
205        /// Human-readable state detail.
206        message: String,
207    },
208
209    /// A runtime-state failure retaining a typed source.
210    #[error("{op} ({phase:?}): runtime state failure: {source}")]
211    RuntimeStateSource {
212        /// Operation whose state was unavailable.
213        op: &'static str,
214        /// Phase that discovered the invalid state.
215        phase: ErrorPhase,
216        /// Typed state source.
217        #[source]
218        source: BoxError,
219    },
220
221    /// A primary error with a second typed error retained as suppressed
222    /// metadata.
223    ///
224    /// The standard error source chain follows `primary`. The suppressed
225    /// error is intentionally exposed through [`Error::suppressed`] because
226    /// [`StdError::source`](std::error::Error::source) can represent only one
227    /// source without losing the primary error's semantics.
228    #[error("primary error: {primary}; suppressed error: {suppressed}")]
229    WithSuppressed {
230        /// The operation's primary failure and the standard error-chain source.
231        #[source]
232        primary: Box<Error>,
233        /// A typed secondary failure retained for diagnostics and recovery.
234        suppressed: Box<Error>,
235    },
236
237    /// A runtime event-domain provenance or admission contract failed.
238    #[error("event-domain operation failed: {source}")]
239    EventDomain {
240        /// Structured event-domain failure with expected/actual provenance.
241        #[from]
242        #[source]
243        source: crate::runtime::EventDomainError,
244    },
245
246    /// A `TracedTensor` supplied as a compiled-graph input binding is not a
247    /// placeholder (has attached data).
248    #[error(
249        "binding #{binding_index} is not a placeholder; \
250         only tensors built via input_concrete_shape / input_symbolic_shape \
251         can be bound"
252    )]
253    UnexpectedBinding { binding_index: usize },
254
255    /// A placeholder appearing in the graph has no binding supplied.
256    #[error("placeholder {input_key} has no runtime input binding")]
257    UnboundPlaceholder { input_key: String },
258
259    /// The number of ordered tensors supplied to a compiled graph is invalid.
260    #[error("compiled graph expects {expected} ordered inputs, got {actual}")]
261    GraphInputCountMismatch { expected: usize, actual: usize },
262
263    /// The same placeholder was bound more than once in the `bindings` slice.
264    #[error("placeholder {input_key} was bound more than once")]
265    DuplicateBinding { input_key: String },
266
267    /// A binding tensor's dtype does not match the placeholder's dtype.
268    #[error("binding dtype mismatch for placeholder: expected {expected:?}, got {actual:?}")]
269    PlaceholderDtypeMismatch { expected: DType, actual: DType },
270
271    /// A binding tensor's shape does not match an `input_concrete_shape`
272    /// placeholder's fixed shape.
273    #[error(
274        "binding shape mismatch for concrete-shape placeholder: \
275         expected {expected:?}, got {actual:?}"
276    )]
277    PlaceholderShapeMismatch {
278        expected: Vec<usize>,
279        actual: Vec<usize>,
280    },
281
282    /// A binding tensor dimension exceeds a semantic input's declared bound.
283    #[error("binding dimension {axis} exceeds semantic input upper bound {bound}: got {actual}")]
284    PlaceholderShapeBoundExceeded {
285        /// Axis whose runtime extent exceeded the bound.
286        axis: usize,
287        /// Evaluated upper bound.
288        bound: usize,
289        /// Runtime extent.
290        actual: usize,
291    },
292
293    /// A binding tensor's rank does not match an `input_symbolic_shape`
294    /// placeholder's declared rank.
295    #[error(
296        "binding rank mismatch for symbolic-shape placeholder: \
297         expected rank {expected}, got rank {actual}"
298    )]
299    PlaceholderRankMismatch { expected: usize, actual: usize },
300
301    /// Operation attempted to mix tensors from different eager contexts.
302    #[error(
303        "tensors belong to different eager AD contexts ({lhs} vs {rhs}); \
304         detach into the target context before combining them"
305    )]
306    ContextMismatch { lhs: ContextId, rhs: ContextId },
307
308    /// An AD transform requires a primitive or extension rule that is not
309    /// registered for the requested operation.
310    #[error("unsupported {transform} AD rule for {op}")]
311    UnsupportedAdRule {
312        /// AD transform that requested the rule, such as `grad` or `backward`.
313        transform: &'static str,
314        /// Operation or extension family identifier that has no applicable rule.
315        op: String,
316    },
317
318    /// A typed AD rule source that crossed an external message-only callback.
319    #[error("{transform} AD rule failed: {source}")]
320    AdRuleSource {
321        /// AD transform that requested the rule.
322        transform: &'static str,
323        /// Original typed source from the AD rule context.
324        #[source]
325        source: BoxError,
326    },
327
328    /// A symbolic extension shape equality evaluated to unequal dimensions.
329    #[error(
330        "extension family {family:?} shape constraint at instruction {instruction_index:?} failed: {lhs_expr} ({lhs_value}) {relation:?} {rhs_expr} ({rhs_value})"
331    )]
332    ShapeConstraintViolation {
333        /// Stable extension family identifier.
334        family: &'static str,
335        /// Stable compiled instruction provenance, when assigned.
336        instruction_index: Option<usize>,
337        /// Shape relation that failed.
338        relation: ShapeRelation,
339        /// Normalized left-hand expression.
340        lhs_expr: String,
341        /// Normalized right-hand expression.
342        rhs_expr: String,
343        /// Concrete left-hand value.
344        lhs_value: usize,
345        /// Concrete right-hand value.
346        rhs_value: usize,
347    },
348
349    /// A symbolic extension shape expression could not be evaluated safely.
350    #[error(
351        "extension family {family:?} shape constraint at instruction {instruction_index:?} could not evaluate {expression} for {relation:?}: {cause}"
352    )]
353    ShapeConstraintEvaluation {
354        /// Stable extension family identifier.
355        family: &'static str,
356        /// Stable compiled instruction provenance, when assigned.
357        instruction_index: Option<usize>,
358        /// Shape relation whose expression failed.
359        relation: ShapeRelation,
360        /// Normalized expression that failed.
361        expression: String,
362        /// Typed evaluation failure.
363        #[source]
364        cause: ShapeConstraintEvalError,
365    },
366
367    /// A symbolic dimension could not be converted into the graph's local
368    /// dimension-expression vocabulary.
369    #[error("{op} ({phase:?}): symbolic shape conversion failed: {source}")]
370    SymbolicShapeConversion {
371        /// Operation that requested the symbolic shape conversion.
372        op: &'static str,
373        /// Phase that discovered the invalid symbolic reference.
374        phase: ErrorPhase,
375        /// Typed symbolic-dimension conversion failure.
376        #[source]
377        source: SymDimConversionError,
378    },
379
380    /// A runtime dimension expression could not be evaluated for concrete
381    /// input shapes.
382    #[error("runtime shape expression {expression} could not evaluate: {cause}")]
383    ShapeExpressionEvaluation {
384        /// Expression that failed during execution.
385        expression: String,
386        /// Typed evaluation failure.
387        #[source]
388        cause: ShapeConstraintEvalError,
389    },
390
391    /// An unexpected internal error.
392    #[error("internal error: {0}")]
393    Internal(String),
394}
395
396impl Error {
397    /// Wrap a shared validation payload with its operation and discovery
398    /// phase.
399    ///
400    /// # Errors
401    ///
402    /// This constructor does not fail; callers receive the returned
403    /// [`Error`] value and can inspect its [`Error::kind`] and
404    /// [`Error::phase`].
405    ///
406    /// # Examples
407    ///
408    /// ```rust
409    /// use tenferro_runtime::{Error, ErrorPhase};
410    /// use tenferro_tensor::{ErrorKind, ShapeMismatch, ValidationKind};
411    ///
412    /// let error = Error::validation(
413    ///     "reshape",
414    ///     ErrorPhase::GraphBuild,
415    ///     ShapeMismatch::ReshapeElementCount { from: 2, to: 3 }.into(),
416    /// );
417    /// assert_eq!(
418    ///     error.kind(),
419    ///     ErrorKind::Validation(ValidationKind::ShapeMismatch)
420    /// );
421    /// assert_eq!(error.phase(), Some(ErrorPhase::GraphBuild));
422    /// ```
423    pub fn validation(op: &'static str, phase: ErrorPhase, source: ValidationError) -> Self {
424        Self::Validation { op, phase, source }
425    }
426
427    /// Construct a validation error for a caller-controlled argument whose
428    /// failure does not have a more specific shared payload.
429    ///
430    /// # Examples
431    ///
432    /// ```rust
433    /// use tenferro_runtime::{Error, ErrorPhase};
434    ///
435    /// let error = Error::invalid_argument(
436    ///     "broadcast_in_dim",
437    ///     ErrorPhase::GraphBuild,
438    ///     "dims",
439    ///     "dimension mapping has the wrong length",
440    /// );
441    /// assert!(matches!(error, Error::Validation { .. }));
442    /// ```
443    pub fn invalid_argument(
444        op: &'static str,
445        phase: ErrorPhase,
446        argument: &'static str,
447        message: impl Into<String>,
448    ) -> Self {
449        Self::validation(
450            op,
451            phase,
452            ValidationError::InvalidArgument {
453                argument,
454                message: message.into(),
455            },
456        )
457    }
458
459    /// Construct a dtype-mismatch validation error using the runtime dtype
460    /// vocabulary.
461    ///
462    /// # Examples
463    ///
464    /// ```rust
465    /// use tenferro_runtime::{DType, Error, ErrorPhase};
466    /// use tenferro_tensor::{ErrorKind, ValidationKind};
467    ///
468    /// let error = Error::dtype_mismatch(
469    ///     "add",
470    ///     ErrorPhase::GraphBuild,
471    ///     DType::F32,
472    ///     DType::F64,
473    /// );
474    /// assert_eq!(error.kind(), ErrorKind::Validation(ValidationKind::DTypeMismatch));
475    /// ```
476    pub fn dtype_mismatch(
477        op: &'static str,
478        phase: ErrorPhase,
479        expected: DType,
480        actual: DType,
481    ) -> Self {
482        Self::validation(
483            op,
484            phase,
485            ValidationError::DTypeMismatch {
486                expected: core_dtype(expected),
487                actual: core_dtype(actual),
488            },
489        )
490    }
491
492    /// Preserve a typed extension-domain source at the runtime boundary.
493    ///
494    /// # Examples
495    ///
496    /// ```rust
497    /// use std::error::Error as _;
498    /// use tenferro_runtime::{Error, ErrorPhase};
499    /// use tenferro_tensor::ErrorKind;
500    ///
501    /// let source = std::io::Error::new(std::io::ErrorKind::Other, "extension failed");
502    /// let error = Error::extension(
503    ///     "einsum",
504    ///     ErrorPhase::GraphBuild,
505    ///     "example.extension.v1",
506    ///     ErrorKind::RuntimeState,
507    ///     source,
508    /// );
509    /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
510    /// assert!(error.source().is_some());
511    /// ```
512    pub fn extension<E>(
513        op: &'static str,
514        phase: ErrorPhase,
515        family: &'static str,
516        kind: ErrorKind,
517        source: E,
518    ) -> Self
519    where
520        E: StdError + Send + Sync + 'static,
521    {
522        Self::Extension {
523            op,
524            phase,
525            family,
526            kind,
527            source: Box::new(source),
528        }
529    }
530
531    /// Construct a runtime-state failure for an unavailable or invalid
532    /// executor, cache, registry, or device state.
533    ///
534    /// # Examples
535    ///
536    /// ```rust
537    /// use tenferro_runtime::{Error, ErrorPhase};
538    /// use tenferro_tensor::ErrorKind;
539    ///
540    /// let error = Error::runtime_state(
541    ///     "executor",
542    ///     ErrorPhase::Execution,
543    ///     "the executor is not initialized",
544    /// );
545    /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
546    /// ```
547    pub fn runtime_state(op: &'static str, phase: ErrorPhase, message: impl Into<String>) -> Self {
548        Self::RuntimeState {
549            op,
550            phase,
551            message: message.into(),
552        }
553    }
554
555    /// Preserve a typed source for an unavailable or invalid runtime state.
556    ///
557    /// # Examples
558    ///
559    /// ```rust
560    /// use std::error::Error as _;
561    /// use tenferro_runtime::{Error, ErrorPhase};
562    /// use tenferro_tensor::ErrorKind;
563    ///
564    /// let error = Error::runtime_state_source(
565    ///     "metadata",
566    ///     ErrorPhase::Compile,
567    ///     std::io::Error::other("registry lock poisoned"),
568    /// );
569    /// assert_eq!(error.kind(), ErrorKind::RuntimeState);
570    /// assert!(error.source().is_some());
571    /// ```
572    pub fn runtime_state_source<E>(op: &'static str, phase: ErrorPhase, source: E) -> Self
573    where
574        E: StdError + Send + Sync + 'static,
575    {
576        Self::RuntimeStateSource {
577            op,
578            phase,
579            source: Box::new(source),
580        }
581    }
582
583    /// Retain a typed secondary error while preserving the primary error's
584    /// classification and standard source chain.
585    ///
586    /// # Examples
587    ///
588    /// ```rust
589    /// use tenferro_runtime::{Error, ErrorPhase};
590    ///
591    /// let error = Error::with_suppressed(
592    ///     Error::unsupported("backend", ErrorPhase::Execution, "primary"),
593    ///     Error::runtime_state("runtime", ErrorPhase::Execution, "suppressed"),
594    /// );
595    /// assert!(error.primary().is_some());
596    /// assert!(error.suppressed().is_some());
597    /// ```
598    pub fn with_suppressed(primary: Self, suppressed: Self) -> Self {
599        Self::WithSuppressed {
600            primary: Box::new(primary),
601            suppressed: Box::new(suppressed),
602        }
603    }
604
605    /// Return the primary error when this value is a suppressed-error
606    /// aggregate.
607    ///
608    /// # Examples
609    ///
610    /// ```rust
611    /// use tenferro_runtime::{Error, ErrorPhase};
612    ///
613    /// let error = Error::with_suppressed(
614    ///     Error::unsupported("backend", ErrorPhase::Execution, "primary"),
615    ///     Error::runtime_state("runtime", ErrorPhase::Execution, "suppressed"),
616    /// );
617    /// assert_eq!(error.primary().unwrap().phase(), Some(ErrorPhase::Execution));
618    /// ```
619    pub fn primary(&self) -> Option<&Self> {
620        match self {
621            Self::WithSuppressed { primary, .. } => Some(primary),
622            _ => None,
623        }
624    }
625
626    /// Return the typed suppressed error when this value is an aggregate.
627    ///
628    /// # Examples
629    ///
630    /// ```rust
631    /// use tenferro_runtime::{Error, ErrorPhase};
632    ///
633    /// let error = Error::with_suppressed(
634    ///     Error::unsupported("backend", ErrorPhase::Execution, "primary"),
635    ///     Error::runtime_state("runtime", ErrorPhase::Execution, "suppressed"),
636    /// );
637    /// assert_eq!(error.suppressed().unwrap().phase(), Some(ErrorPhase::Execution));
638    /// ```
639    pub fn suppressed(&self) -> Option<&Self> {
640        match self {
641            Self::WithSuppressed { suppressed, .. } => Some(suppressed),
642            _ => None,
643        }
644    }
645
646    /// Preserve a typed source returned by an AD rule through a callback
647    /// protocol that can carry only a rendered message.
648    ///
649    /// # Examples
650    ///
651    /// ```rust
652    /// use std::error::Error as _;
653    /// use tenferro_runtime::Error;
654    ///
655    /// let error = Error::ad_rule_source(
656    ///     "jvp",
657    ///     std::io::Error::other("shape metadata missing"),
658    /// );
659    /// assert!(error.source().is_some());
660    /// ```
661    pub fn ad_rule_source<E>(transform: &'static str, source: E) -> Self
662    where
663        E: StdError + Send + Sync + 'static,
664    {
665        Self::AdRuleSource {
666            transform,
667            source: Box::new(source),
668        }
669    }
670
671    /// Construct an operation-level unsupported error with an explicit
672    /// discovery phase.
673    ///
674    /// # Examples
675    ///
676    /// ```rust
677    /// use tenferro_runtime::{Error, ErrorPhase};
678    /// use tenferro_tensor::ErrorKind;
679    ///
680    /// let error = Error::unsupported(
681    ///     "compare",
682    ///     ErrorPhase::Compile,
683    ///     "complex values have no total order",
684    /// );
685    /// assert_eq!(error.kind(), ErrorKind::Unsupported);
686    /// assert_eq!(error.phase(), Some(ErrorPhase::Compile));
687    /// ```
688    pub fn unsupported(op: &'static str, phase: ErrorPhase, message: impl Into<String>) -> Self {
689        Self::Unsupported {
690            op,
691            phase,
692            message: message.into(),
693        }
694    }
695
696    /// Return the stable coarse classification of this runtime failure.
697    ///
698    /// # Examples
699    ///
700    /// ```rust
701    /// use tenferro_runtime::{Error, ErrorPhase};
702    /// use tenferro_tensor::{ErrorKind, ValidationError, ValidationKind};
703    ///
704    /// let error = Error::validation(
705    ///     "transpose",
706    ///     ErrorPhase::GraphBuild,
707    ///     ValidationError::AxisOutOfBounds { axis: 2, rank: 2 },
708    /// );
709    /// assert_eq!(error.kind(), ErrorKind::Validation(ValidationKind::AxisOutOfBounds));
710    /// ```
711    pub fn kind(&self) -> ErrorKind {
712        match self {
713            Self::Validation { source, .. } => ErrorKind::Validation(source.kind()),
714            Self::MissingInput(_)
715            | Self::UnexpectedBinding { .. }
716            | Self::UnboundPlaceholder { .. }
717            | Self::DuplicateBinding { .. }
718            | Self::ContextMismatch { .. } => ErrorKind::RuntimeState,
719            Self::NonScalarGrad { .. } => ErrorKind::Validation(ValidationKind::InvalidArgument),
720            Self::GraphInputCountMismatch { .. } => {
721                ErrorKind::Validation(ValidationKind::InvalidArgument)
722            }
723            Self::Unsupported { .. } | Self::UnsupportedAdRule { .. } => ErrorKind::Unsupported,
724            Self::AdRuleSource { .. } => ErrorKind::Validation(ValidationKind::InvalidArgument),
725            Self::TensorRuntime(error) => error.kind(),
726            Self::Extension { kind, .. } => *kind,
727            Self::RuntimeState { .. }
728            | Self::RuntimeStateSource { .. }
729            | Self::EventDomain { .. } => ErrorKind::RuntimeState,
730            Self::WithSuppressed { primary, .. } => primary.kind(),
731            Self::PlaceholderDtypeMismatch { .. } => {
732                ErrorKind::Validation(ValidationKind::DTypeMismatch)
733            }
734            Self::PlaceholderShapeMismatch { .. } | Self::PlaceholderShapeBoundExceeded { .. } => {
735                ErrorKind::Validation(ValidationKind::ShapeMismatch)
736            }
737            Self::PlaceholderRankMismatch { .. } => {
738                ErrorKind::Validation(ValidationKind::RankMismatch)
739            }
740            Self::ShapeConstraintViolation { .. } => {
741                ErrorKind::Validation(ValidationKind::ShapeMismatch)
742            }
743            Self::ShapeConstraintEvaluation { .. } => {
744                ErrorKind::Validation(ValidationKind::InvalidArgument)
745            }
746            Self::SymbolicShapeConversion { .. } => {
747                ErrorKind::Validation(ValidationKind::InvalidArgument)
748            }
749            Self::ShapeExpressionEvaluation { .. } => {
750                ErrorKind::Validation(ValidationKind::InvalidArgument)
751            }
752            Self::Internal(_) => ErrorKind::Internal,
753        }
754    }
755
756    /// Return the discovery phase when this error has one.
757    ///
758    /// # Examples
759    ///
760    /// ```rust
761    /// use tenferro_runtime::{Error, ErrorPhase};
762    /// use tenferro_tensor::ValidationError;
763    ///
764    /// let error = Error::validation(
765    ///     "reshape",
766    ///     ErrorPhase::Compile,
767    ///     ValidationError::RankMismatch { expected: 2, actual: 1 },
768    /// );
769    /// assert_eq!(error.phase(), Some(ErrorPhase::Compile));
770    /// ```
771    pub fn phase(&self) -> Option<ErrorPhase> {
772        match self {
773            Self::Validation { phase, .. } => Some(*phase),
774            Self::TensorRuntime(_) => Some(ErrorPhase::Execution),
775            Self::Unsupported { phase, .. } => Some(*phase),
776            Self::Extension { phase, .. } => Some(*phase),
777            Self::RuntimeState { phase, .. } | Self::RuntimeStateSource { phase, .. } => {
778                Some(*phase)
779            }
780            Self::WithSuppressed { primary, .. } => primary.phase(),
781            Self::AdRuleSource { .. } => Some(ErrorPhase::GraphBuild),
782            Self::PlaceholderDtypeMismatch { .. }
783            | Self::PlaceholderShapeMismatch { .. }
784            | Self::PlaceholderRankMismatch { .. }
785            | Self::GraphInputCountMismatch { .. }
786            | Self::UnexpectedBinding { .. }
787            | Self::UnboundPlaceholder { .. }
788            | Self::DuplicateBinding { .. } => Some(ErrorPhase::Execution),
789            Self::EventDomain { .. } => Some(ErrorPhase::Execution),
790            Self::SymbolicShapeConversion { phase, .. } => Some(*phase),
791            Self::ShapeExpressionEvaluation { .. } => Some(ErrorPhase::Execution),
792            _ => None,
793        }
794    }
795}
796
797fn core_dtype(dtype: DType) -> tenferro_tensor::core::DType {
798    match dtype {
799        DType::F32 => tenferro_tensor::core::DType::F32,
800        DType::F64 => tenferro_tensor::core::DType::F64,
801        DType::I32 => tenferro_tensor::core::DType::I32,
802        DType::I64 => tenferro_tensor::core::DType::I64,
803        DType::Bool => tenferro_tensor::core::DType::Bool,
804        DType::C32 => tenferro_tensor::core::DType::C32,
805        DType::C64 => tenferro_tensor::core::DType::C64,
806    }
807}
808
809/// Opaque identifier for an eager AD runtime, used in [`Error::ContextMismatch`].
810#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
811pub struct ContextId(usize);
812
813impl ContextId {
814    /// Generate a fresh opaque runtime context identifier.
815    ///
816    /// Runtime implementations use this when constructing a new execution
817    /// context. The value is intentionally opaque and is only useful in error
818    /// reporting and equality checks.
819    pub fn fresh() -> Self {
820        let id = NEXT_CONTEXT_ID.fetch_add(1, Ordering::Relaxed);
821        Self(id)
822    }
823}
824
825impl std::fmt::Display for ContextId {
826    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
827        write!(f, "ctx@{:x}", self.0)
828    }
829}
830
831/// Result type alias for tenferro operations.
832pub type Result<T> = std::result::Result<T, Error>;
833
834#[cfg(test)]
835mod tests {
836    use std::error::Error as StdError;
837
838    use tenferro_ops::dim_expr::{DimExpr, DimExprEvalError};
839    use tenferro_tensor::{
840        DType, ErrorKind, ShapeMismatch, ShapeVec, ValidationError, ValidationKind,
841    };
842
843    use super::{ContextId, Error, ErrorPhase, ShapeConstraintEvalError};
844
845    #[test]
846    fn dimension_evaluation_errors_keep_the_runtime_vocabulary() {
847        let cases = [
848            (
849                DimExpr::InputDim {
850                    input_idx: 2,
851                    axis: 0,
852                }
853                .eval(&[&[1usize]])
854                .unwrap_err(),
855                ShapeConstraintEvalError::MissingInput {
856                    input_idx: 2,
857                    input_count: 1,
858                },
859            ),
860            (
861                DimExpr::InputDim {
862                    input_idx: 0,
863                    axis: 2,
864                }
865                .eval(&[&[1usize]])
866                .unwrap_err(),
867                ShapeConstraintEvalError::AxisOutOfBounds {
868                    input_idx: 0,
869                    axis: 2,
870                    rank: 1,
871                },
872            ),
873            (
874                DimExpr::Add(
875                    Box::new(DimExpr::Const(usize::MAX)),
876                    Box::new(DimExpr::Const(1)),
877                )
878                .eval(&[])
879                .unwrap_err(),
880                ShapeConstraintEvalError::Overflow,
881            ),
882            (
883                DimExpr::Mul(
884                    Box::new(DimExpr::Const(usize::MAX)),
885                    Box::new(DimExpr::Const(2)),
886                )
887                .eval(&[])
888                .unwrap_err(),
889                ShapeConstraintEvalError::Overflow,
890            ),
891            (
892                DimExpr::Sub(Box::new(DimExpr::Const(0)), Box::new(DimExpr::Const(1)))
893                    .eval(&[])
894                    .unwrap_err(),
895                ShapeConstraintEvalError::Underflow,
896            ),
897            (
898                DimExpr::FloorDiv(Box::new(DimExpr::Const(1)), Box::new(DimExpr::Const(0)))
899                    .eval(&[])
900                    .unwrap_err(),
901                ShapeConstraintEvalError::DivisionByZero,
902            ),
903        ];
904
905        for (actual, expected) in cases {
906            assert_eq!(ShapeConstraintEvalError::from(actual), expected);
907        }
908
909        assert_eq!(
910            ShapeConstraintEvalError::from(DimExprEvalError::AddOverflow { lhs: 1, rhs: 2 }),
911            ShapeConstraintEvalError::Overflow
912        );
913    }
914
915    #[test]
916    fn constructors_preserve_classification_and_typed_sources() {
917        let shape = Error::validation(
918            "reshape",
919            ErrorPhase::GraphBuild,
920            ShapeMismatch::ExpectedActual {
921                expected: ShapeVec::from_vec(vec![2, 3]),
922                actual: ShapeVec::from_vec(vec![6]),
923            }
924            .into(),
925        );
926        assert_eq!(
927            shape.kind(),
928            ErrorKind::Validation(ValidationKind::ShapeMismatch)
929        );
930        assert_eq!(shape.phase(), Some(ErrorPhase::GraphBuild));
931
932        let invalid =
933            Error::invalid_argument("slice", ErrorPhase::Compile, "step", "must be non-zero");
934        assert!(matches!(
935            invalid,
936            Error::Validation {
937                source: ValidationError::InvalidArgument {
938                    argument: "step",
939                    ..
940                },
941                ..
942            }
943        ));
944
945        for dtype in [
946            DType::F32,
947            DType::F64,
948            DType::I32,
949            DType::I64,
950            DType::Bool,
951            DType::C32,
952            DType::C64,
953        ] {
954            let error = Error::dtype_mismatch("cast", ErrorPhase::GraphBuild, dtype, dtype);
955            assert!(matches!(
956                error,
957                Error::Validation {
958                    source: ValidationError::DTypeMismatch { .. },
959                    ..
960                }
961            ));
962        }
963
964        let extension = Error::extension(
965            "extension",
966            ErrorPhase::Compile,
967            "example.v1",
968            ErrorKind::Io,
969            std::io::Error::other("manifest read failed"),
970        );
971        assert_eq!(extension.kind(), ErrorKind::Io);
972        assert!(StdError::source(&extension).is_some());
973
974        let state = Error::runtime_state(
975            "executor",
976            ErrorPhase::Execution,
977            "executor is not initialized",
978        );
979        assert_eq!(state.kind(), ErrorKind::RuntimeState);
980        let state_source = Error::runtime_state_source(
981            "registry",
982            ErrorPhase::Compile,
983            std::io::Error::other("registry lock poisoned"),
984        );
985        assert_eq!(state_source.kind(), ErrorKind::RuntimeState);
986        assert!(StdError::source(&state_source).is_some());
987        let unsupported = Error::unsupported(
988            "compare",
989            ErrorPhase::Compile,
990            "complex values have no total order",
991        );
992        assert_eq!(unsupported.kind(), ErrorKind::Unsupported);
993    }
994
995    #[test]
996    fn kind_classifies_every_runtime_variant_without_string_inspection() {
997        let errors = [
998            (
999                Error::validation(
1000                    "shape",
1001                    ErrorPhase::GraphBuild,
1002                    ValidationError::RankMismatch {
1003                        expected: 2,
1004                        actual: 1,
1005                    },
1006                ),
1007                ErrorKind::Validation(ValidationKind::RankMismatch),
1008            ),
1009            (Error::MissingInput("x".into()), ErrorKind::RuntimeState),
1010            (
1011                Error::NonScalarGrad { shape: vec![2] },
1012                ErrorKind::Validation(ValidationKind::InvalidArgument),
1013            ),
1014            (
1015                Error::unsupported("op", ErrorPhase::Compile, "missing rule"),
1016                ErrorKind::Unsupported,
1017            ),
1018            (
1019                Error::TensorRuntime(tenferro_tensor::Error::unsupported("op", "not available")),
1020                ErrorKind::Unsupported,
1021            ),
1022            (
1023                Error::extension(
1024                    "op",
1025                    ErrorPhase::Execution,
1026                    "family.v1",
1027                    ErrorKind::NumericalFailure,
1028                    std::io::Error::other("numerical source"),
1029                ),
1030                ErrorKind::NumericalFailure,
1031            ),
1032            (
1033                Error::runtime_state("op", ErrorPhase::Execution, "state"),
1034                ErrorKind::RuntimeState,
1035            ),
1036            (
1037                Error::runtime_state_source(
1038                    "op",
1039                    ErrorPhase::Execution,
1040                    std::io::Error::other("state"),
1041                ),
1042                ErrorKind::RuntimeState,
1043            ),
1044            (
1045                Error::UnexpectedBinding { binding_index: 0 },
1046                ErrorKind::RuntimeState,
1047            ),
1048            (
1049                Error::UnboundPlaceholder {
1050                    input_key: "x".into(),
1051                },
1052                ErrorKind::RuntimeState,
1053            ),
1054            (
1055                Error::DuplicateBinding {
1056                    input_key: "x".into(),
1057                },
1058                ErrorKind::RuntimeState,
1059            ),
1060            (
1061                Error::PlaceholderDtypeMismatch {
1062                    expected: DType::F32,
1063                    actual: DType::F64,
1064                },
1065                ErrorKind::Validation(ValidationKind::DTypeMismatch),
1066            ),
1067            (
1068                Error::PlaceholderShapeMismatch {
1069                    expected: vec![2],
1070                    actual: vec![3],
1071                },
1072                ErrorKind::Validation(ValidationKind::ShapeMismatch),
1073            ),
1074            (
1075                Error::PlaceholderRankMismatch {
1076                    expected: 2,
1077                    actual: 1,
1078                },
1079                ErrorKind::Validation(ValidationKind::RankMismatch),
1080            ),
1081            (
1082                Error::ContextMismatch {
1083                    lhs: ContextId::fresh(),
1084                    rhs: ContextId::fresh(),
1085                },
1086                ErrorKind::RuntimeState,
1087            ),
1088            (
1089                Error::UnsupportedAdRule {
1090                    transform: "vjp",
1091                    op: "example".into(),
1092                },
1093                ErrorKind::Unsupported,
1094            ),
1095            (
1096                Error::ShapeConstraintViolation {
1097                    family: "example.v1",
1098                    instruction_index: Some(3),
1099                    relation: tenferro_ops::ShapeRelation::Equal,
1100                    lhs_expr: "m".into(),
1101                    rhs_expr: "n".into(),
1102                    lhs_value: 2,
1103                    rhs_value: 3,
1104                },
1105                ErrorKind::Validation(ValidationKind::ShapeMismatch),
1106            ),
1107            (
1108                Error::ShapeConstraintEvaluation {
1109                    family: "example.v1",
1110                    instruction_index: None,
1111                    relation: tenferro_ops::ShapeRelation::Equal,
1112                    expression: "m+n".into(),
1113                    cause: ShapeConstraintEvalError::Overflow,
1114                },
1115                ErrorKind::Validation(ValidationKind::InvalidArgument),
1116            ),
1117            (
1118                Error::SymbolicShapeConversion {
1119                    op: "broadcast",
1120                    phase: ErrorPhase::GraphBuild,
1121                    source: tenferro_ops::SymDimConversionError { tensor_id: 7 },
1122                },
1123                ErrorKind::Validation(ValidationKind::InvalidArgument),
1124            ),
1125            (
1126                Error::ShapeExpressionEvaluation {
1127                    expression: "m/0".into(),
1128                    cause: ShapeConstraintEvalError::DivisionByZero,
1129                },
1130                ErrorKind::Validation(ValidationKind::InvalidArgument),
1131            ),
1132            (Error::Internal("invariant".into()), ErrorKind::Internal),
1133        ];
1134
1135        for (error, expected) in errors {
1136            assert_eq!(error.kind(), expected, "classified {error:?}");
1137        }
1138    }
1139
1140    #[test]
1141    fn phase_reports_discovery_axis_separately_from_kind() {
1142        let with_phase = [
1143            Error::validation(
1144                "op",
1145                ErrorPhase::GraphBuild,
1146                ValidationError::InvalidArgument {
1147                    argument: "x",
1148                    message: "bad".into(),
1149                },
1150            ),
1151            Error::TensorRuntime(tenferro_tensor::Error::invalid_argument("op", "x", "bad")),
1152            Error::unsupported("op", ErrorPhase::Compile, "unsupported"),
1153            Error::extension(
1154                "op",
1155                ErrorPhase::GraphBuild,
1156                "family.v1",
1157                ErrorKind::Internal,
1158                std::io::Error::other("extension"),
1159            ),
1160            Error::runtime_state("op", ErrorPhase::Execution, "state"),
1161            Error::runtime_state_source("op", ErrorPhase::Compile, std::io::Error::other("state")),
1162            Error::PlaceholderDtypeMismatch {
1163                expected: DType::F32,
1164                actual: DType::F64,
1165            },
1166            Error::PlaceholderShapeMismatch {
1167                expected: vec![2],
1168                actual: vec![3],
1169            },
1170            Error::PlaceholderRankMismatch {
1171                expected: 2,
1172                actual: 1,
1173            },
1174            Error::UnexpectedBinding { binding_index: 0 },
1175            Error::UnboundPlaceholder {
1176                input_key: "x".into(),
1177            },
1178            Error::DuplicateBinding {
1179                input_key: "x".into(),
1180            },
1181            Error::SymbolicShapeConversion {
1182                op: "op",
1183                phase: ErrorPhase::Compile,
1184                source: tenferro_ops::SymDimConversionError { tensor_id: 1 },
1185            },
1186            Error::ShapeExpressionEvaluation {
1187                expression: "m".into(),
1188                cause: ShapeConstraintEvalError::Overflow,
1189            },
1190        ];
1191        let expected = [
1192            Some(ErrorPhase::GraphBuild),
1193            Some(ErrorPhase::Execution),
1194            Some(ErrorPhase::Compile),
1195            Some(ErrorPhase::GraphBuild),
1196            Some(ErrorPhase::Execution),
1197            Some(ErrorPhase::Compile),
1198            Some(ErrorPhase::Execution),
1199            Some(ErrorPhase::Execution),
1200            Some(ErrorPhase::Execution),
1201            Some(ErrorPhase::Execution),
1202            Some(ErrorPhase::Execution),
1203            Some(ErrorPhase::Execution),
1204            Some(ErrorPhase::Compile),
1205            Some(ErrorPhase::Execution),
1206        ];
1207        for (error, expected) in with_phase.into_iter().zip(expected) {
1208            assert_eq!(error.phase(), expected);
1209        }
1210
1211        let without_phase = [
1212            Error::MissingInput("x".into()),
1213            Error::NonScalarGrad { shape: vec![2] },
1214            Error::ContextMismatch {
1215                lhs: ContextId::fresh(),
1216                rhs: ContextId::fresh(),
1217            },
1218            Error::UnsupportedAdRule {
1219                transform: "jvp",
1220                op: "example".into(),
1221            },
1222            Error::ShapeConstraintViolation {
1223                family: "example.v1",
1224                instruction_index: None,
1225                relation: tenferro_ops::ShapeRelation::Equal,
1226                lhs_expr: "m".into(),
1227                rhs_expr: "n".into(),
1228                lhs_value: 1,
1229                rhs_value: 2,
1230            },
1231            Error::ShapeConstraintEvaluation {
1232                family: "example.v1",
1233                instruction_index: None,
1234                relation: tenferro_ops::ShapeRelation::Equal,
1235                expression: "m".into(),
1236                cause: ShapeConstraintEvalError::Overflow,
1237            },
1238            Error::Internal("invariant".into()),
1239        ];
1240        for error in without_phase {
1241            assert_eq!(error.phase(), None);
1242        }
1243    }
1244
1245    #[test]
1246    fn context_ids_are_opaque_but_displayable() {
1247        let first = ContextId::fresh();
1248        let second = ContextId::fresh();
1249
1250        assert_ne!(first, second);
1251        assert!(first.to_string().starts_with("ctx@"));
1252    }
1253
1254    #[test]
1255    fn suppressed_error_aggregate_delegates_primary_semantics() {
1256        let primary = Error::extension(
1257            "backend_mutation",
1258            ErrorPhase::Compile,
1259            "test.primary.v1",
1260            ErrorKind::Io,
1261            std::io::Error::other("primary source"),
1262        );
1263        let suppressed = Error::runtime_state_source(
1264            "runtime_reconciliation",
1265            ErrorPhase::Execution,
1266            std::io::Error::other("suppressed source"),
1267        );
1268        let primary_kind = primary.kind();
1269        let primary_phase = primary.phase();
1270        let primary_display = primary.to_string();
1271
1272        let aggregate = Error::with_suppressed(primary, suppressed);
1273
1274        assert_eq!(aggregate.kind(), primary_kind);
1275        assert_eq!(aggregate.phase(), primary_phase);
1276        let source = StdError::source(&aggregate).expect("primary error source");
1277        assert_eq!(source.to_string(), primary_display);
1278        assert!(StdError::source(source).is_some());
1279
1280        let primary = aggregate.primary().expect("aggregate primary error");
1281        assert_eq!(primary.kind(), primary_kind);
1282        assert_eq!(primary.phase(), primary_phase);
1283        assert!(StdError::source(primary).is_some());
1284
1285        let suppressed = aggregate
1286            .suppressed()
1287            .expect("typed suppressed error metadata");
1288        assert_eq!(suppressed.kind(), ErrorKind::RuntimeState);
1289        assert_eq!(suppressed.phase(), Some(ErrorPhase::Execution));
1290        assert!(matches!(suppressed, Error::RuntimeStateSource { .. }));
1291        assert!(StdError::source(suppressed).is_some());
1292    }
1293}