Skip to main content

edgefirst_decoder/
error.rs

1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`DecoderError`] and the [`DecoderResult`] alias returned across the crate.
5
6use core::fmt;
7
8/// `Result` alias defaulting the error type to [`DecoderError`].
9pub type DecoderResult<T, E = DecoderError> = std::result::Result<T, E>;
10
11#[derive(Debug)]
12pub enum DecoderError {
13    /// An internal error occurred
14    Internal(String),
15    /// An operation was requested that is not supported
16    NotSupported(String),
17    /// An invalid tensor shape was given
18    InvalidShape(String),
19    /// An error occurred while parsing YAML
20    Yaml(serde_yaml::Error),
21    /// An error occurred while parsing YAML
22    Json(serde_json::Error),
23    /// Attmpted to use build a decoder without configuration
24    NoConfig,
25    /// The provide decoder configuration was invalid
26    InvalidConfig(String),
27    /// An error occurred with ndarray shape operations
28    NDArrayShape(ndarray::ShapeError),
29    /// Schema-declared per-scale child dtype != bound tensor dtype at run time.
30    DtypeMismatch {
31        expected: edgefirst_tensor::DType,
32        actual: edgefirst_tensor::DType,
33        role: &'static str,
34        level: usize,
35    },
36    /// Integer tensor bound to a role that requires dequantization, but the
37    /// tensor carries no `Quantization` metadata. The upstream inference layer
38    /// must call `tensor.set_quantization(...)` before invoking the decoder,
39    /// or callers may use `per_scale::apply_schema_quant()` as a fallback.
40    QuantMissing {
41        dtype: edgefirst_tensor::DType,
42        role: &'static str,
43        level: usize,
44    },
45    /// Internal logic bug — a dispatch variant was constructed but the
46    /// concrete kernel wasn't matched. Indicates a missing arm in the
47    /// dispatch enum's `run()` impl.
48    KernelDispatchUnreachable(String),
49    /// `EDGEFIRST_DECODER_FORCE_KERNEL` requested a tier whose features
50    /// the running CPU doesn't support.
51    ForcedKernelUnavailable {
52        tier: &'static str,
53        missing_feature: &'static str,
54    },
55    /// A caller tensor refused the operation, e.g. a CPU access its declared
56    /// [`CpuAccess`](edgefirst_tensor::CpuAccess) does not cover.
57    Tensor(edgefirst_tensor::Error),
58}
59
60impl fmt::Display for DecoderError {
61    /// Formats the error for display
62    /// # Arguments
63    /// * `f` - The formatter to write to
64    /// # Returns
65    /// A result indicating success or failure
66    /// # Examples
67    /// ```rust
68    /// use edgefirst_decoder::DecoderError;
69    /// let err = DecoderError::InvalidConfig("The config was invalid".to_string());
70    /// println!("{}", err);
71    /// ```
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self {
74            DecoderError::Tensor(e) => write!(f, "Tensor({e})"),
75            _ => write!(f, "{self:?}"),
76        }
77    }
78}
79
80impl std::error::Error for DecoderError {}
81
82impl DecoderError {
83    /// The [`CpuAccessDenied`](edgefirst_tensor::Error::CpuAccessDenied)
84    /// refusal this error carries, if any.
85    pub fn cpu_access_denied(&self) -> Option<&edgefirst_tensor::Error> {
86        match self {
87            DecoderError::Tensor(e) => e.cpu_access_denied(),
88            _ => None,
89        }
90    }
91
92    /// A failed map: a `CpuAccessDenied` refusal stays typed as
93    /// [`DecoderError::Tensor`], any other error goes through `other`.
94    pub(crate) fn from_map(
95        e: edgefirst_tensor::Error,
96        other: impl FnOnce(edgefirst_tensor::Error) -> DecoderError,
97    ) -> DecoderError {
98        match e {
99            e @ edgefirst_tensor::Error::CpuAccessDenied { .. } => DecoderError::Tensor(e),
100            e => other(e),
101        }
102    }
103}
104
105impl From<edgefirst_tensor::Error> for DecoderError {
106    fn from(err: edgefirst_tensor::Error) -> Self {
107        DecoderError::Tensor(err)
108    }
109}
110
111impl From<serde_yaml::Error> for DecoderError {
112    fn from(err: serde_yaml::Error) -> Self {
113        DecoderError::Yaml(err)
114    }
115}
116
117impl From<serde_json::Error> for DecoderError {
118    fn from(err: serde_json::Error) -> Self {
119        DecoderError::Json(err)
120    }
121}
122
123impl From<ndarray::ShapeError> for DecoderError {
124    fn from(err: ndarray::ShapeError) -> Self {
125        DecoderError::NDArrayShape(err)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_decoder_error_display() {
135        let e = DecoderError::Internal("something broke".to_string());
136        let msg = e.to_string();
137        assert!(!msg.is_empty());
138        assert!(
139            msg.contains("Internal") && msg.contains("something broke"),
140            "unexpected Internal message: {msg}"
141        );
142
143        let e = DecoderError::NotSupported("yolov99".to_string());
144        let msg = e.to_string();
145        assert!(!msg.is_empty());
146        assert!(
147            msg.contains("NotSupported") && msg.contains("yolov99"),
148            "unexpected NotSupported message: {msg}"
149        );
150
151        let e = DecoderError::InvalidShape("expected 3D".to_string());
152        let msg = e.to_string();
153        assert!(!msg.is_empty());
154        assert!(
155            msg.contains("InvalidShape") && msg.contains("expected 3D"),
156            "unexpected InvalidShape message: {msg}"
157        );
158
159        let e = DecoderError::NoConfig;
160        let msg = e.to_string();
161        assert!(!msg.is_empty());
162        assert!(
163            msg.contains("NoConfig"),
164            "unexpected NoConfig message: {msg}"
165        );
166
167        let e = DecoderError::InvalidConfig("missing field".to_string());
168        let msg = e.to_string();
169        assert!(!msg.is_empty());
170        assert!(
171            msg.contains("InvalidConfig") && msg.contains("missing field"),
172            "unexpected InvalidConfig message: {msg}"
173        );
174    }
175}