1use 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
25pub type BoxError = Box<dyn StdError + Send + Sync + 'static>;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43#[non_exhaustive]
44pub enum ErrorPhase {
45 GraphBuild,
47 Compile,
49 Execution,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
70pub enum ShapeConstraintEvalError {
71 #[error(
73 "shape expression references input {input_idx}, but only {input_count} inputs were provided"
74 )]
75 MissingInput {
76 input_idx: usize,
78 input_count: usize,
80 },
81 #[error("shape expression references input {input_idx} axis {axis}, but its rank is {rank}")]
83 AxisOutOfBounds {
84 input_idx: usize,
86 axis: usize,
88 rank: usize,
90 },
91 #[error("shape expression arithmetic overflowed")]
93 Overflow,
94 #[error("shape expression subtraction underflowed")]
96 Underflow,
97 #[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#[derive(Debug, thiserror::Error)]
145pub enum Error {
146 #[error("{op} ({phase:?}): {source}")]
148 Validation {
149 op: &'static str,
151 phase: ErrorPhase,
153 #[source]
155 source: ValidationError,
156 },
157
158 #[error("missing input: {0}")]
160 MissingInput(String),
161
162 #[error("grad requires a scalar output, got shape {shape:?}")]
164 NonScalarGrad { shape: Vec<usize> },
165
166 #[error("{op} ({phase:?}) is unsupported: {message}")]
169 Unsupported {
170 op: &'static str,
172 phase: ErrorPhase,
174 message: String,
176 },
177
178 #[error(transparent)]
180 TensorRuntime(#[from] tenferro_tensor::Error),
181
182 #[error("extension {family} ({phase:?}) failed for {op}: {source}")]
184 Extension {
185 op: &'static str,
187 phase: ErrorPhase,
189 family: &'static str,
191 kind: ErrorKind,
193 #[source]
195 source: BoxError,
196 },
197
198 #[error("{op} ({phase:?}): runtime state failure: {message}")]
200 RuntimeState {
201 op: &'static str,
203 phase: ErrorPhase,
205 message: String,
207 },
208
209 #[error("{op} ({phase:?}): runtime state failure: {source}")]
211 RuntimeStateSource {
212 op: &'static str,
214 phase: ErrorPhase,
216 #[source]
218 source: BoxError,
219 },
220
221 #[error("primary error: {primary}; suppressed error: {suppressed}")]
229 WithSuppressed {
230 #[source]
232 primary: Box<Error>,
233 suppressed: Box<Error>,
235 },
236
237 #[error("event-domain operation failed: {source}")]
239 EventDomain {
240 #[from]
242 #[source]
243 source: crate::runtime::EventDomainError,
244 },
245
246 #[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 #[error("placeholder {input_key} has no runtime input binding")]
257 UnboundPlaceholder { input_key: String },
258
259 #[error("compiled graph expects {expected} ordered inputs, got {actual}")]
261 GraphInputCountMismatch { expected: usize, actual: usize },
262
263 #[error("placeholder {input_key} was bound more than once")]
265 DuplicateBinding { input_key: String },
266
267 #[error("binding dtype mismatch for placeholder: expected {expected:?}, got {actual:?}")]
269 PlaceholderDtypeMismatch { expected: DType, actual: DType },
270
271 #[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 #[error("binding dimension {axis} exceeds semantic input upper bound {bound}: got {actual}")]
284 PlaceholderShapeBoundExceeded {
285 axis: usize,
287 bound: usize,
289 actual: usize,
291 },
292
293 #[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 #[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 #[error("unsupported {transform} AD rule for {op}")]
311 UnsupportedAdRule {
312 transform: &'static str,
314 op: String,
316 },
317
318 #[error("{transform} AD rule failed: {source}")]
320 AdRuleSource {
321 transform: &'static str,
323 #[source]
325 source: BoxError,
326 },
327
328 #[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 family: &'static str,
335 instruction_index: Option<usize>,
337 relation: ShapeRelation,
339 lhs_expr: String,
341 rhs_expr: String,
343 lhs_value: usize,
345 rhs_value: usize,
347 },
348
349 #[error(
351 "extension family {family:?} shape constraint at instruction {instruction_index:?} could not evaluate {expression} for {relation:?}: {cause}"
352 )]
353 ShapeConstraintEvaluation {
354 family: &'static str,
356 instruction_index: Option<usize>,
358 relation: ShapeRelation,
360 expression: String,
362 #[source]
364 cause: ShapeConstraintEvalError,
365 },
366
367 #[error("{op} ({phase:?}): symbolic shape conversion failed: {source}")]
370 SymbolicShapeConversion {
371 op: &'static str,
373 phase: ErrorPhase,
375 #[source]
377 source: SymDimConversionError,
378 },
379
380 #[error("runtime shape expression {expression} could not evaluate: {cause}")]
383 ShapeExpressionEvaluation {
384 expression: String,
386 #[source]
388 cause: ShapeConstraintEvalError,
389 },
390
391 #[error("internal error: {0}")]
393 Internal(String),
394}
395
396impl Error {
397 pub fn validation(op: &'static str, phase: ErrorPhase, source: ValidationError) -> Self {
424 Self::Validation { op, phase, source }
425 }
426
427 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 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 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 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 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 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 pub fn primary(&self) -> Option<&Self> {
620 match self {
621 Self::WithSuppressed { primary, .. } => Some(primary),
622 _ => None,
623 }
624 }
625
626 pub fn suppressed(&self) -> Option<&Self> {
640 match self {
641 Self::WithSuppressed { suppressed, .. } => Some(suppressed),
642 _ => None,
643 }
644 }
645
646 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
811pub struct ContextId(usize);
812
813impl ContextId {
814 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
831pub 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}