Skip to main content

edgefirst_tensor/
error.rs

1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4pub type Result<T, E = Error> = std::result::Result<T, E>;
5
6#[derive(Debug)]
7pub enum Error {
8    IoError(std::io::Error),
9    #[cfg(unix)]
10    NixError(nix::Error),
11    NotImplemented(String),
12    InvalidSize(usize),
13    ShapeMismatch(String),
14    #[cfg(target_os = "linux")]
15    UnknownDeviceType(u64, u64),
16    /// An imported fd sits on a filesystem we cannot classify as either a
17    /// DMA-BUF (`DMA_BUF_MAGIC`) or shared memory (`TMPFS_MAGIC`).
18    ///
19    /// Carries the `f_type` magic reported by `fstatfs`, normalized to the
20    /// 32-bit unsigned value used by `include/uapi/linux/magic.h`, so it can
21    /// be cross-referenced against that header directly on both 32- and
22    /// 64-bit targets.
23    #[cfg(target_os = "linux")]
24    UnknownBufferType(u32),
25    InvalidMemoryType(String),
26    /// The GL context backing a PBO tensor has been destroyed.
27    PboDisconnected,
28    /// The PBO buffer is currently mapped and cannot be used for GL operations.
29    PboMapped,
30    #[cfg(feature = "ndarray")]
31    NdArrayError(ndarray::ShapeError),
32    InvalidShape(String),
33    InvalidArgument(String),
34    InvalidOperation(String),
35    /// A write-only map is refused only because it cannot express the
36    /// requested window; the identical window mapped `CpuAccess::ReadWrite`
37    /// is not refused (at whatever extra cost that backend's `ReadWrite`
38    /// costs — see the message). Distinct from [`Error::InvalidArgument`] so
39    /// a caller that wants to retry with `ReadWrite` can match on this
40    /// variant instead of a message, and knows the retry will not fail the
41    /// same way again.
42    PartialWriteRequiresReadWrite(String),
43    /// Structured quantization-invariant failure. Round-trippable through
44    /// the C and Python boundaries so callers can diagnose which field
45    /// failed without parsing strings.
46    QuantizationInvalid {
47        /// Which invariant failed: `"scale.len"`, `"zero_point.len"`,
48        /// `"axis"`, `"per_channel_requires_axis"`,
49        /// `"per_tensor_redundant_axis"`, `"dtype_is_integer"`.
50        field: &'static str,
51        /// What the validator expected, e.g. `"length matches scale (48)"`.
52        expected: String,
53        /// What was observed, e.g. `"length 32"`.
54        got: String,
55    },
56    /// A capacity-aware operation needs more bytes than the tensor's
57    /// underlying allocation provides.
58    InsufficientCapacity {
59        /// Bytes the requested layout needs.
60        needed: usize,
61        /// Bytes the allocation provides.
62        capacity: usize,
63    },
64    /// A [`crate::Tensor::view`]-style sub-region extends past the parent's
65    /// bounds. `view`/`batch` reject rather than clamp.
66    RegionOutOfBounds {
67        /// The offending region.
68        region: crate::Region,
69        /// The parent frame `(width, height)` in pixels.
70        bounds: (usize, usize),
71    },
72    /// A `batch(n)` index is `>=` the tensor's leading batch dimension `N`.
73    BatchIndexOutOfBounds {
74        /// The requested element index.
75        index: usize,
76        /// The tensor's leading dimension `N`.
77        batch: usize,
78    },
79    /// A CPU map, pin or sync asked for a direction the tensor's
80    /// [`crate::CpuAccess`] declaration does not cover.
81    CpuAccessDenied {
82        /// The tensor's declaration: set at allocation, or as narrowed on
83        /// import by `restrict_cpu_access`.
84        declared: crate::CpuAccess,
85        /// The access the operation needed.
86        requested: crate::CpuAccess,
87    },
88}
89
90impl Error {
91    /// This error when it is a [`CpuAccessDenied`](Error::CpuAccessDenied)
92    /// refusal, else `None`.
93    pub fn cpu_access_denied(&self) -> Option<&Error> {
94        match self {
95            Error::CpuAccessDenied { .. } => Some(self),
96            _ => None,
97        }
98    }
99}
100
101impl From<std::io::Error> for Error {
102    fn from(err: std::io::Error) -> Self {
103        Error::IoError(err)
104    }
105}
106#[cfg(unix)]
107impl From<nix::Error> for Error {
108    fn from(err: nix::Error) -> Self {
109        Error::NixError(err)
110    }
111}
112
113#[cfg(feature = "ndarray")]
114impl From<ndarray::ShapeError> for Error {
115    fn from(err: ndarray::ShapeError) -> Self {
116        Error::NdArrayError(err)
117    }
118}
119
120impl std::fmt::Display for Error {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        match self {
123            Error::InsufficientCapacity { needed, capacity } => write!(
124                f,
125                "insufficient tensor capacity: need {needed} bytes, have {capacity}"
126            ),
127            Error::RegionOutOfBounds { region, bounds } => write!(
128                f,
129                "region {region:?} out of bounds for {}x{} frame",
130                bounds.0, bounds.1
131            ),
132            Error::BatchIndexOutOfBounds { index, batch } => write!(
133                f,
134                "batch index {index} out of bounds for batch size {batch}"
135            ),
136            Error::CpuAccessDenied {
137                declared,
138                requested,
139            } => {
140                let needed = declared.union(*requested);
141                write!(
142                    f,
143                    "CPU access denied: {} access requested on a tensor declared \
144                     CpuAccess::{declared:?}; declare CpuAccess::{needed:?}",
145                    requested.as_str()
146                )?;
147                if needed == crate::CpuAccess::ReadWrite {
148                    f.write_str(" (the default)")
149                } else {
150                    f.write_str(" or CpuAccess::ReadWrite (the default)")
151                }
152            }
153            #[cfg(target_os = "linux")]
154            Error::UnknownBufferType(magic) => write!(
155                f,
156                "UnknownBufferType: fd is on an unrecognized filesystem \
157                 (magic {magic:#010x}); expected a DMA-BUF or tmpfs/shm fd"
158            ),
159            _ => write!(f, "{self:?}"),
160        }
161    }
162}
163
164impl std::error::Error for Error {}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_error_display() {
172        let e = Error::InvalidSize(0);
173        let msg = e.to_string();
174        assert!(!msg.is_empty());
175        assert!(
176            msg.contains("InvalidSize"),
177            "unexpected InvalidSize message: {msg}"
178        );
179
180        let e = Error::NotImplemented("foo".to_string());
181        let msg = e.to_string();
182        assert!(!msg.is_empty());
183        assert!(
184            msg.contains("NotImplemented") && msg.contains("foo"),
185            "unexpected NotImplemented message: {msg}"
186        );
187
188        let e = Error::ShapeMismatch("expected 3, got 4".to_string());
189        let msg = e.to_string();
190        assert!(!msg.is_empty());
191        assert!(
192            msg.contains("ShapeMismatch") && msg.contains("expected 3"),
193            "unexpected ShapeMismatch message: {msg}"
194        );
195
196        let e = Error::InvalidMemoryType("dma".to_string());
197        let msg = e.to_string();
198        assert!(!msg.is_empty());
199        assert!(
200            msg.contains("InvalidMemoryType") && msg.contains("dma"),
201            "unexpected InvalidMemoryType message: {msg}"
202        );
203
204        let e = Error::PboDisconnected;
205        let msg = e.to_string();
206        assert!(!msg.is_empty());
207        assert!(
208            msg.contains("PboDisconnected"),
209            "unexpected PboDisconnected message: {msg}"
210        );
211
212        let e = Error::PboMapped;
213        let msg = e.to_string();
214        assert!(!msg.is_empty());
215        assert!(
216            msg.contains("PboMapped"),
217            "unexpected PboMapped message: {msg}"
218        );
219
220        let e = Error::InvalidShape("bad shape".to_string());
221        let msg = e.to_string();
222        assert!(!msg.is_empty());
223        assert!(
224            msg.contains("InvalidShape") && msg.contains("bad shape"),
225            "unexpected InvalidShape message: {msg}"
226        );
227
228        let e = Error::InvalidArgument("negative".to_string());
229        let msg = e.to_string();
230        assert!(!msg.is_empty());
231        assert!(
232            msg.contains("InvalidArgument") && msg.contains("negative"),
233            "unexpected InvalidArgument message: {msg}"
234        );
235
236        let e = Error::PartialWriteRequiresReadWrite("retry with ReadWrite".to_string());
237        let msg = e.to_string();
238        assert!(!msg.is_empty());
239        assert!(
240            msg.contains("PartialWriteRequiresReadWrite") && msg.contains("retry with ReadWrite"),
241            "unexpected PartialWriteRequiresReadWrite message: {msg}"
242        );
243
244        let e = Error::InvalidOperation("read-only".to_string());
245        let msg = e.to_string();
246        assert!(!msg.is_empty());
247        assert!(
248            msg.contains("InvalidOperation") && msg.contains("read-only"),
249            "unexpected InvalidOperation message: {msg}"
250        );
251
252        let e = Error::IoError(std::io::Error::new(
253            std::io::ErrorKind::NotFound,
254            "file missing",
255        ));
256        let msg = e.to_string();
257        assert!(!msg.is_empty());
258        assert!(
259            msg.contains("IoError") && msg.contains("file missing"),
260            "unexpected IoError message: {msg}"
261        );
262    }
263
264    #[test]
265    fn cpu_access_denied_message_names_both_accesses_and_the_fix() {
266        let e = Error::CpuAccessDenied {
267            declared: crate::CpuAccess::Write,
268            requested: crate::CpuAccess::Read,
269        };
270        let msg = e.to_string();
271        assert_eq!(
272            msg,
273            "CPU access denied: read access requested on a tensor declared \
274             CpuAccess::Write; declare CpuAccess::ReadWrite (the default)"
275        );
276
277        let e = Error::CpuAccessDenied {
278            declared: crate::CpuAccess::None,
279            requested: crate::CpuAccess::Read,
280        };
281        assert_eq!(
282            e.to_string(),
283            "CPU access denied: read access requested on a tensor declared \
284             CpuAccess::None; declare CpuAccess::Read or CpuAccess::ReadWrite (the default)"
285        );
286    }
287
288    #[test]
289    fn insufficient_capacity_message() {
290        let e = Error::InsufficientCapacity {
291            needed: 100,
292            capacity: 64,
293        };
294        assert_eq!(
295            e.to_string(),
296            "insufficient tensor capacity: need 100 bytes, have 64"
297        );
298    }
299
300    #[test]
301    fn region_out_of_bounds_message_names_the_frame() {
302        let region = crate::Region::new(2, 3, 8, 8);
303        let e = Error::RegionOutOfBounds {
304            region,
305            bounds: (4, 5),
306        };
307        assert_eq!(
308            e.to_string(),
309            format!("region {region:?} out of bounds for 4x5 frame")
310        );
311    }
312
313    #[test]
314    fn batch_index_out_of_bounds_message_names_index_and_size() {
315        let e = Error::BatchIndexOutOfBounds { index: 9, batch: 4 };
316        assert_eq!(
317            e.to_string(),
318            "batch index 9 out of bounds for batch size 4"
319        );
320    }
321
322    #[cfg(target_os = "linux")]
323    #[test]
324    fn unknown_buffer_type_message_names_the_magic() {
325        let e = Error::UnknownBufferType(0x1234);
326        assert_eq!(
327            e.to_string(),
328            "UnknownBufferType: fd is on an unrecognized filesystem (magic 0x00001234); \
329             expected a DMA-BUF or tmpfs/shm fd"
330        );
331    }
332}