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;