use std::error::Error as StdError;
use std::sync::atomic::{AtomicUsize, Ordering};
use tenferro_ops::{dim_expr::DimExprEvalError, ShapeRelation, SymDimConversionError};
use tenferro_tensor::{DType, ErrorKind, ValidationError, ValidationKind};
use crate::runtime::{PrepareError, UnsupportedReason};
static NEXT_CONTEXT_ID: AtomicUsize = AtomicUsize::new(1);
pub type BoxError = Box<dyn StdError + Send + Sync + 'static>;
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum RuntimeFailureReasonRef<'a> {
MissingExtension { family: &'a str },
NoInputIngress {
input_index: usize,
placement: &'a tenferro_tensor::Placement,
},
UnsupportedOperation { operation: &'a str },
Other,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorPhase {
GraphBuild,
Compile,
Execution,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ShapeConstraintEvalError {
#[error(
"shape expression references input {input_idx}, but only {input_count} inputs were provided"
)]
MissingInput {
input_idx: usize,
input_count: usize,
},
#[error("shape expression references input {input_idx} axis {axis}, but its rank is {rank}")]
AxisOutOfBounds {
input_idx: usize,
axis: usize,
rank: usize,
},
#[error("shape expression arithmetic overflowed")]
Overflow,
#[error("shape expression subtraction underflowed")]
Underflow,
#[error("shape expression divided by zero")]
DivisionByZero,
}
impl From<DimExprEvalError> for ShapeConstraintEvalError {
fn from(error: DimExprEvalError) -> Self {
match error {
DimExprEvalError::InputOutOfBounds {
input_idx,
input_count,
} => Self::MissingInput {
input_idx,
input_count,
},
DimExprEvalError::AxisOutOfBounds {
input_idx,
axis,
rank,
} => Self::AxisOutOfBounds {
input_idx,
axis,
rank,
},
DimExprEvalError::AddOverflow { .. } | DimExprEvalError::MulOverflow { .. } => {
Self::Overflow
}
DimExprEvalError::SubUnderflow { .. } => Self::Underflow,
DimExprEvalError::FloorDivByZero { .. } => Self::DivisionByZero,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{op} ({phase:?}): {source}")]
Validation {
op: &'static str,
phase: ErrorPhase,
#[source]
source: ValidationError,
},
#[error("missing input: {0}")]
MissingInput(String),
#[error("grad requires a scalar output, got shape {shape:?}")]
NonScalarGrad { shape: Vec<usize> },
#[error("{op} ({phase:?}) is unsupported: {message}")]
Unsupported {
op: &'static str,
phase: ErrorPhase,
message: String,
},
#[error(transparent)]
TensorRuntime(#[from] tenferro_tensor::Error),
#[error("extension {family} ({phase:?}) failed for {op}: {source}")]
Extension {
op: &'static str,
phase: ErrorPhase,
family: &'static str,
kind: ErrorKind,
#[source]
source: BoxError,
},
#[error("{op} ({phase:?}): runtime state failure: {message}")]
RuntimeState {
op: &'static str,
phase: ErrorPhase,
message: String,
},
#[error("{op} ({phase:?}): runtime state failure: {source}")]
RuntimeStateSource {
op: &'static str,
phase: ErrorPhase,
#[source]
source: BoxError,
},
#[error("primary error: {primary}; suppressed error: {suppressed}")]
WithSuppressed {
#[source]
primary: Box<Error>,
suppressed: Box<Error>,
},
#[error("event-domain operation failed: {source}")]
EventDomain {
#[from]
#[source]
source: crate::runtime::EventDomainError,
},
#[error(
"binding #{binding_index} is not a placeholder; \
only tensors built via input_concrete_shape / input_symbolic_shape \
can be bound"
)]
UnexpectedBinding { binding_index: usize },
#[error("placeholder {input_key} has no runtime input binding")]
UnboundPlaceholder { input_key: String },
#[error("compiled graph expects {expected} ordered inputs, got {actual}")]
GraphInputCountMismatch { expected: usize, actual: usize },
#[error("placeholder {input_key} was bound more than once")]
DuplicateBinding { input_key: String },
#[error("binding dtype mismatch for placeholder: expected {expected:?}, got {actual:?}")]
PlaceholderDtypeMismatch { expected: DType, actual: DType },
#[error(
"binding shape mismatch for concrete-shape placeholder: \
expected {expected:?}, got {actual:?}"
)]
PlaceholderShapeMismatch {
expected: Vec<usize>,
actual: Vec<usize>,
},
#[error("binding dimension {axis} exceeds semantic input upper bound {bound}: got {actual}")]
PlaceholderShapeBoundExceeded {
axis: usize,
bound: usize,
actual: usize,
},
#[error(
"binding rank mismatch for symbolic-shape placeholder: \
expected rank {expected}, got rank {actual}"
)]
PlaceholderRankMismatch { expected: usize, actual: usize },
#[error(
"tensors belong to different eager AD contexts ({lhs} vs {rhs}); \
detach into the target context before combining them"
)]
ContextMismatch { lhs: ContextId, rhs: ContextId },
#[error("unsupported {transform} AD rule for {op}")]
UnsupportedAdRule {
transform: &'static str,
op: String,
},
#[error("{transform} AD rule failed: {source}")]
AdRuleSource {
transform: &'static str,
#[source]
source: BoxError,
},
#[error(
"extension family {family:?} shape constraint at instruction {instruction_index:?} failed: {lhs_expr} ({lhs_value}) {relation:?} {rhs_expr} ({rhs_value})"
)]
ShapeConstraintViolation {
family: &'static str,
instruction_index: Option<usize>,
relation: ShapeRelation,
lhs_expr: String,
rhs_expr: String,
lhs_value: usize,
rhs_value: usize,
},
#[error(
"extension family {family:?} shape constraint at instruction {instruction_index:?} could not evaluate {expression} for {relation:?}: {cause}"
)]
ShapeConstraintEvaluation {
family: &'static str,
instruction_index: Option<usize>,
relation: ShapeRelation,
expression: String,
#[source]
cause: ShapeConstraintEvalError,
},
#[error("{op} ({phase:?}): symbolic shape conversion failed: {source}")]
SymbolicShapeConversion {
op: &'static str,
phase: ErrorPhase,
#[source]
source: SymDimConversionError,
},
#[error("runtime shape expression {expression} could not evaluate: {cause}")]
ShapeExpressionEvaluation {
expression: String,
#[source]
cause: ShapeConstraintEvalError,
},
#[error("internal error: {0}")]
Internal(String),
}
impl Error {
pub fn validation(op: &'static str, phase: ErrorPhase, source: ValidationError) -> Self {
Self::Validation { op, phase, source }
}
pub fn invalid_argument(
op: &'static str,
phase: ErrorPhase,
argument: &'static str,
message: impl Into<String>,
) -> Self {
Self::validation(
op,
phase,
ValidationError::InvalidArgument {
argument,
message: message.into(),
},
)
}
pub fn dtype_mismatch(
op: &'static str,
phase: ErrorPhase,
expected: DType,
actual: DType,
) -> Self {
Self::validation(
op,
phase,
ValidationError::DTypeMismatch { expected, actual },
)
}
pub fn extension<E>(
op: &'static str,
phase: ErrorPhase,
family: &'static str,
kind: ErrorKind,
source: E,
) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self::Extension {
op,
phase,
family,
kind,
source: Box::new(source),
}
}
pub fn runtime_state(op: &'static str, phase: ErrorPhase, message: impl Into<String>) -> Self {
Self::RuntimeState {
op,
phase,
message: message.into(),
}
}
pub fn runtime_state_source<E>(op: &'static str, phase: ErrorPhase, source: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self::RuntimeStateSource {
op,
phase,
source: Box::new(source),
}
}
pub fn with_suppressed(primary: Self, suppressed: Self) -> Self {
Self::WithSuppressed {
primary: Box::new(primary),
suppressed: Box::new(suppressed),
}
}
pub fn primary(&self) -> Option<&Self> {
match self {
Self::WithSuppressed { primary, .. } => Some(primary),
_ => None,
}
}
pub fn suppressed(&self) -> Option<&Self> {
match self {
Self::WithSuppressed { suppressed, .. } => Some(suppressed),
_ => None,
}
}
pub fn ad_rule_source<E>(transform: &'static str, source: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self::AdRuleSource {
transform,
source: Box::new(source),
}
}
pub fn unsupported(op: &'static str, phase: ErrorPhase, message: impl Into<String>) -> Self {
Self::Unsupported {
op,
phase,
message: message.into(),
}
}
pub fn reason(&self) -> RuntimeFailureReasonRef<'_> {
if let Self::WithSuppressed { primary, .. } = self {
return primary.reason();
}
if let Some(reason) = direct_reason(self) {
return reason;
}
let mut source = StdError::source(self);
while let Some(error) = source {
if let Some(reason) = error.downcast_ref::<Error>().and_then(direct_reason) {
return reason;
}
if let Some(reason) = prepare_reason(error) {
return reason;
}
source = StdError::source(error);
}
RuntimeFailureReasonRef::Other
}
pub fn kind(&self) -> ErrorKind {
match self {
Self::Validation { source, .. } => ErrorKind::Validation(source.kind()),
Self::MissingInput(_)
| Self::UnexpectedBinding { .. }
| Self::UnboundPlaceholder { .. }
| Self::DuplicateBinding { .. }
| Self::ContextMismatch { .. } => ErrorKind::RuntimeState,
Self::NonScalarGrad { .. } => ErrorKind::Validation(ValidationKind::InvalidArgument),
Self::GraphInputCountMismatch { .. } => {
ErrorKind::Validation(ValidationKind::InvalidArgument)
}
Self::Unsupported { .. } | Self::UnsupportedAdRule { .. } => ErrorKind::Unsupported,
Self::AdRuleSource { .. } => ErrorKind::Validation(ValidationKind::InvalidArgument),
Self::TensorRuntime(error) => error.kind(),
Self::Extension { kind, .. } => *kind,
Self::RuntimeState { .. }
| Self::RuntimeStateSource { .. }
| Self::EventDomain { .. } => ErrorKind::RuntimeState,
Self::WithSuppressed { primary, .. } => primary.kind(),
Self::PlaceholderDtypeMismatch { .. } => {
ErrorKind::Validation(ValidationKind::DTypeMismatch)
}
Self::PlaceholderShapeMismatch { .. } | Self::PlaceholderShapeBoundExceeded { .. } => {
ErrorKind::Validation(ValidationKind::ShapeMismatch)
}
Self::PlaceholderRankMismatch { .. } => {
ErrorKind::Validation(ValidationKind::RankMismatch)
}
Self::ShapeConstraintViolation { .. } => {
ErrorKind::Validation(ValidationKind::ShapeMismatch)
}
Self::ShapeConstraintEvaluation { .. } => {
ErrorKind::Validation(ValidationKind::InvalidArgument)
}
Self::SymbolicShapeConversion { .. } => {
ErrorKind::Validation(ValidationKind::InvalidArgument)
}
Self::ShapeExpressionEvaluation { .. } => {
ErrorKind::Validation(ValidationKind::InvalidArgument)
}
Self::Internal(_) => ErrorKind::Internal,
}
}
pub fn phase(&self) -> Option<ErrorPhase> {
match self {
Self::Validation { phase, .. } => Some(*phase),
Self::TensorRuntime(_) => Some(ErrorPhase::Execution),
Self::Unsupported { phase, .. } => Some(*phase),
Self::Extension { phase, .. } => Some(*phase),
Self::RuntimeState { phase, .. } | Self::RuntimeStateSource { phase, .. } => {
Some(*phase)
}
Self::WithSuppressed { primary, .. } => primary.phase(),
Self::AdRuleSource { .. } => Some(ErrorPhase::GraphBuild),
Self::PlaceholderDtypeMismatch { .. }
| Self::PlaceholderShapeMismatch { .. }
| Self::PlaceholderRankMismatch { .. }
| Self::GraphInputCountMismatch { .. }
| Self::UnexpectedBinding { .. }
| Self::UnboundPlaceholder { .. }
| Self::DuplicateBinding { .. } => Some(ErrorPhase::Execution),
Self::EventDomain { .. } => Some(ErrorPhase::Execution),
Self::SymbolicShapeConversion { phase, .. } => Some(*phase),
Self::ShapeExpressionEvaluation { .. } => Some(ErrorPhase::Execution),
_ => None,
}
}
}
fn direct_reason(error: &Error) -> Option<RuntimeFailureReasonRef<'_>> {
match error {
Error::Unsupported { op, .. } => {
Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: op })
}
Error::UnsupportedAdRule { op, .. } => {
Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: op })
}
Error::TensorRuntime(error) => tensor_reason(error),
Error::Extension { family, kind, .. } if *kind == ErrorKind::Unsupported => {
Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: family })
}
_ => None,
}
}
fn tensor_reason(error: &tenferro_tensor::Error) -> Option<RuntimeFailureReasonRef<'_>> {
match error {
tenferro_tensor::Error::Unsupported { op, .. }
| tenferro_tensor::Error::UnsupportedDType { op, .. }
| tenferro_tensor::Error::UnsupportedDTypeConversion { op, .. } => {
Some(RuntimeFailureReasonRef::UnsupportedOperation { operation: op })
}
_ => None,
}
}
fn prepare_reason<'a>(error: &'a (dyn StdError + 'static)) -> Option<RuntimeFailureReasonRef<'a>> {
let prepare = error.downcast_ref::<PrepareError>()?;
Some(match prepare {
PrepareError::MissingExtension { family_id } => {
RuntimeFailureReasonRef::MissingExtension { family: family_id }
}
PrepareError::NoInputIngress {
input_index,
placement,
} => RuntimeFailureReasonRef::NoInputIngress {
input_index: *input_index,
placement,
},
PrepareError::Unsupported {
reason: UnsupportedReason::Operation { operation },
} => RuntimeFailureReasonRef::UnsupportedOperation { operation },
_ => return None,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ContextId(usize);
impl ContextId {
pub fn fresh() -> Self {
let id = NEXT_CONTEXT_ID.fetch_add(1, Ordering::Relaxed);
Self(id)
}
}
impl std::fmt::Display for ContextId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ctx@{:x}", self.0)
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests;