1use std::fmt;
9use thiserror::Error;
10
11pub type Result<T> = std::result::Result<T, NirError>;
13
14#[derive(Debug, Clone, PartialEq, Eq, Error)]
19#[non_exhaustive]
20pub enum ParameterError {
21 #[error("weight rank {found} is not {expected}")]
23 WeightRank {
24 found: usize,
26 expected: usize,
28 },
29 #[error("groups must be > 0, found {found}")]
31 Groups {
32 found: i64,
34 },
35 #[error("output channels {channels} are not divisible by groups {groups}")]
37 ChannelGroupDivisibility {
38 channels: usize,
40 groups: i64,
42 },
43 #[error("{field} arity {found} is not {expected}")]
45 ExtentArity {
46 field: &'static str,
48 expected: &'static str,
50 found: usize,
52 },
53 #[error("{field} rank {found} is not 0 or 1")]
55 ExtentRank {
56 field: &'static str,
58 found: usize,
60 },
61 #[error("{field} must contain i64 extents, found {dtype}")]
63 ExtentDType {
64 field: &'static str,
66 dtype: &'static str,
68 },
69 #[error("{field} extents must be strictly positive, found {value} at index {index}")]
71 ExtentPositive {
72 field: &'static str,
74 index: usize,
76 value: i64,
78 },
79 #[error("{field} extents must be non-negative, found {value} at index {index}")]
81 ExtentNonNegative {
82 field: &'static str,
84 index: usize,
86 value: i64,
88 },
89 #[error("bias length {found} is incompatible with {expected} output channels")]
91 BiasLength {
92 found: usize,
94 expected: usize,
96 },
97 #[error("bias rank {found} is not 1")]
99 BiasRank {
100 found: usize,
102 },
103 #[error("weight extents must be strictly positive, found 0 along axis {axis}")]
105 WeightExtentPositive {
106 axis: usize,
108 },
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
117#[non_exhaustive]
118pub enum ReadLimitResource {
119 Nodes,
121 Edges,
123 NestedGraphs,
125}
126
127impl fmt::Display for ReadLimitResource {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 f.write_str(match self {
130 Self::Nodes => "nodes",
131 Self::Edges => "edges",
132 Self::NestedGraphs => "nested graphs",
133 })
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Error)]
139#[non_exhaustive]
140pub enum NirError {
141 #[error("not implemented: {0}")]
145 Unimplemented(&'static str),
146
147 #[error("unknown node type: {0}")]
149 UnknownNodeType(String),
150
151 #[error("duplicate node: {0}")]
153 DuplicateNode(String),
154
155 #[error("missing node: {0}")]
157 MissingNode(String),
158
159 #[error("duplicate edge: ({0}, {1})")]
161 DuplicateEdge(String, String),
162
163 #[error("invalid graph: {0}")]
165 InvalidGraph(String),
166
167 #[error("unsupported version: {0}")]
169 UnsupportedVersion(String),
170
171 #[error(
176 "unsupported version: {} (policy: {policy})",
177 .observed.as_deref().unwrap_or("<absent>")
178 )]
179 IncompatibleVersion {
180 observed: Option<String>,
182 policy: String,
184 },
185
186 #[error("missing field: {0}")]
188 MissingField(String),
189
190 #[error("invalid tensor: {0}")]
192 InvalidTensor(String),
193
194 #[error("invalid parameters for {node_type} node {node}: {kind}")]
201 InvalidNodeParameters {
202 node: String,
206 node_type: &'static str,
208 kind: ParameterError,
210 },
211
212 #[error(
214 "read allocation limit exceeded at {context}: limit {limit} bytes, used {used} bytes, requested {requested} bytes"
215 )]
216 ReadLimitExceeded {
217 context: String,
219 limit: usize,
221 used: usize,
223 requested: usize,
225 },
226
227 #[error(
229 "read limit exceeded for {resource} at {context}: limit {limit}, used {used}, requested {requested}"
230 )]
231 ReadCountLimitExceeded {
232 resource: ReadLimitResource,
234 context: String,
236 limit: usize,
238 used: usize,
240 requested: usize,
242 },
243
244 #[error("io error: {0}")]
249 Io(String),
250}
251
252#[cfg(feature = "hdf5")]
257impl From<hdf5::Error> for NirError {
258 fn from(err: hdf5::Error) -> Self {
259 Self::Io(err.to_string())
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn unimplemented_display() {
269 let err = NirError::Unimplemented("hdf5 read");
270 assert_eq!(err.to_string(), "not implemented: hdf5 read");
271 }
272
273 #[test]
274 fn unknown_node_type_display() {
275 let err = NirError::UnknownNodeType("CurrLIF".into());
276 assert_eq!(err.to_string(), "unknown node type: CurrLIF");
277 }
278
279 #[test]
280 fn duplicate_node_display() {
281 let err = NirError::DuplicateNode("lif".into());
282 assert_eq!(err.to_string(), "duplicate node: lif");
283 }
284
285 #[test]
286 fn missing_node_display() {
287 let err = NirError::MissingNode("missing".into());
288 assert_eq!(err.to_string(), "missing node: missing");
289 }
290
291 #[test]
292 fn duplicate_edge_display() {
293 let err = NirError::DuplicateEdge("a".into(), "b".into());
294 assert_eq!(err.to_string(), "duplicate edge: (a, b)");
295 }
296
297 #[test]
298 fn invalid_graph_display() {
299 let err = NirError::InvalidGraph("empty subgraph".into());
300 assert_eq!(err.to_string(), "invalid graph: empty subgraph");
301 }
302
303 #[test]
304 fn unsupported_version_display() {
305 let err = NirError::UnsupportedVersion("99.0".into());
306 assert_eq!(err.to_string(), "unsupported version: 99.0");
307 }
308
309 #[test]
310 fn incompatible_version_display_includes_observed_and_policy() {
311 let present = NirError::IncompatibleVersion {
312 observed: Some("99.0.0".into()),
313 policy: "compatible-major majors=[0, 1]".into(),
314 };
315 assert_eq!(
316 present.to_string(),
317 "unsupported version: 99.0.0 (policy: compatible-major majors=[0, 1])"
318 );
319 let missing = NirError::IncompatibleVersion {
320 observed: None,
321 policy: "require-present".into(),
322 };
323 assert_eq!(
324 missing.to_string(),
325 "unsupported version: <absent> (policy: require-present)"
326 );
327 }
328
329 #[test]
330 fn missing_field_display() {
331 let err = NirError::MissingField("weight".into());
332 assert_eq!(err.to_string(), "missing field: weight");
333 }
334
335 #[test]
336 fn invalid_tensor_display() {
337 let err = NirError::InvalidTensor("shape product 4 != data len 3".into());
338 assert_eq!(
339 err.to_string(),
340 "invalid tensor: shape product 4 != data len 3"
341 );
342 }
343
344 #[test]
345 fn invalid_node_parameters_display() {
346 let err = NirError::InvalidNodeParameters {
347 node: "conv".into(),
348 node_type: "Conv1d",
349 kind: ParameterError::WeightRank {
350 found: 2,
351 expected: 3,
352 },
353 };
354 assert_eq!(
355 err.to_string(),
356 "invalid parameters for Conv1d node conv: weight rank 2 is not 3"
357 );
358 }
359
360 #[test]
361 fn parameter_error_displays() {
362 let cases = [
363 (
364 ParameterError::Groups { found: 0 },
365 "groups must be > 0, found 0",
366 ),
367 (
368 ParameterError::ChannelGroupDivisibility {
369 channels: 3,
370 groups: 2,
371 },
372 "output channels 3 are not divisible by groups 2",
373 ),
374 (
375 ParameterError::ExtentArity {
376 field: "stride",
377 expected: "1",
378 found: 2,
379 },
380 "stride arity 2 is not 1",
381 ),
382 (
383 ParameterError::ExtentRank {
384 field: "kernel_size",
385 found: 2,
386 },
387 "kernel_size rank 2 is not 0 or 1",
388 ),
389 (
390 ParameterError::ExtentDType {
391 field: "padding",
392 dtype: "f32",
393 },
394 "padding must contain i64 extents, found f32",
395 ),
396 (
397 ParameterError::ExtentPositive {
398 field: "dilation",
399 index: 0,
400 value: 0,
401 },
402 "dilation extents must be strictly positive, found 0 at index 0",
403 ),
404 (
405 ParameterError::ExtentNonNegative {
406 field: "padding",
407 index: 1,
408 value: -1,
409 },
410 "padding extents must be non-negative, found -1 at index 1",
411 ),
412 (
413 ParameterError::BiasLength {
414 found: 1,
415 expected: 2,
416 },
417 "bias length 1 is incompatible with 2 output channels",
418 ),
419 ];
420 for (err, expected) in cases {
421 assert_eq!(err.to_string(), expected);
422 }
423 }
424
425 #[test]
426 fn read_limit_display() {
427 let err = NirError::ReadLimitExceeded {
428 context: "lif.tau".into(),
429 limit: 1024,
430 used: 768,
431 requested: 512,
432 };
433 assert_eq!(
434 err.to_string(),
435 "read allocation limit exceeded at lif.tau: limit 1024 bytes, used 768 bytes, requested 512 bytes"
436 );
437 }
438
439 #[test]
440 fn read_count_limit_display_identifies_resource_and_path() {
441 let err = NirError::ReadCountLimitExceeded {
442 resource: ReadLimitResource::Nodes,
443 context: "/node/nodes".into(),
444 limit: 3,
445 used: 2,
446 requested: 2,
447 };
448 assert_eq!(
449 err.to_string(),
450 "read limit exceeded for nodes at /node/nodes: limit 3, used 2, requested 2"
451 );
452 assert_eq!(ReadLimitResource::Edges.to_string(), "edges");
453 assert_eq!(ReadLimitResource::NestedGraphs.to_string(), "nested graphs");
454 }
455
456 #[test]
457 fn io_display() {
458 let err = NirError::Io("unable to open file: model.nir".into());
459 assert_eq!(err.to_string(), "io error: unable to open file: model.nir");
460 }
461
462 #[test]
463 fn error_trait_implemented() {
464 let err: Box<dyn std::error::Error> = Box::new(NirError::Unimplemented("x"));
465 assert!(err.to_string().contains("not implemented"));
466 }
467}