use std::fmt;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, NirError>;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum ParameterError {
#[error("weight rank {found} is not {expected}")]
WeightRank {
found: usize,
expected: usize,
},
#[error("groups must be > 0, found {found}")]
Groups {
found: i64,
},
#[error("output channels {channels} are not divisible by groups {groups}")]
ChannelGroupDivisibility {
channels: usize,
groups: i64,
},
#[error("{field} arity {found} is not {expected}")]
ExtentArity {
field: &'static str,
expected: &'static str,
found: usize,
},
#[error("{field} rank {found} is not 0 or 1")]
ExtentRank {
field: &'static str,
found: usize,
},
#[error("{field} must contain i64 extents, found {dtype}")]
ExtentDType {
field: &'static str,
dtype: &'static str,
},
#[error("{field} extents must be strictly positive, found {value} at index {index}")]
ExtentPositive {
field: &'static str,
index: usize,
value: i64,
},
#[error("{field} extents must be non-negative, found {value} at index {index}")]
ExtentNonNegative {
field: &'static str,
index: usize,
value: i64,
},
#[error("bias length {found} is incompatible with {expected} output channels")]
BiasLength {
found: usize,
expected: usize,
},
#[error("bias rank {found} is not 1")]
BiasRank {
found: usize,
},
#[error("weight extents must be strictly positive, found 0 along axis {axis}")]
WeightExtentPositive {
axis: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ReadLimitResource {
Nodes,
Edges,
NestedGraphs,
}
impl fmt::Display for ReadLimitResource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Nodes => "nodes",
Self::Edges => "edges",
Self::NestedGraphs => "nested graphs",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum NirError {
#[error("not implemented: {0}")]
Unimplemented(&'static str),
#[error("unknown node type: {0}")]
UnknownNodeType(String),
#[error("duplicate node: {0}")]
DuplicateNode(String),
#[error("missing node: {0}")]
MissingNode(String),
#[error("duplicate edge: ({0}, {1})")]
DuplicateEdge(String, String),
#[error("invalid graph: {0}")]
InvalidGraph(String),
#[error("unsupported version: {0}")]
UnsupportedVersion(String),
#[error(
"unsupported version: {} (policy: {policy})",
.observed.as_deref().unwrap_or("<absent>")
)]
IncompatibleVersion {
observed: Option<String>,
policy: String,
},
#[error("missing field: {0}")]
MissingField(String),
#[error("invalid tensor: {0}")]
InvalidTensor(String),
#[error("invalid parameters for {node_type} node {node}: {kind}")]
InvalidNodeParameters {
node: String,
node_type: &'static str,
kind: ParameterError,
},
#[error(
"read allocation limit exceeded at {context}: limit {limit} bytes, used {used} bytes, requested {requested} bytes"
)]
ReadLimitExceeded {
context: String,
limit: usize,
used: usize,
requested: usize,
},
#[error(
"read limit exceeded for {resource} at {context}: limit {limit}, used {used}, requested {requested}"
)]
ReadCountLimitExceeded {
resource: ReadLimitResource,
context: String,
limit: usize,
used: usize,
requested: usize,
},
#[error("io error: {0}")]
Io(String),
}
#[cfg(feature = "hdf5")]
impl From<hdf5::Error> for NirError {
fn from(err: hdf5::Error) -> Self {
Self::Io(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unimplemented_display() {
let err = NirError::Unimplemented("hdf5 read");
assert_eq!(err.to_string(), "not implemented: hdf5 read");
}
#[test]
fn unknown_node_type_display() {
let err = NirError::UnknownNodeType("CurrLIF".into());
assert_eq!(err.to_string(), "unknown node type: CurrLIF");
}
#[test]
fn duplicate_node_display() {
let err = NirError::DuplicateNode("lif".into());
assert_eq!(err.to_string(), "duplicate node: lif");
}
#[test]
fn missing_node_display() {
let err = NirError::MissingNode("missing".into());
assert_eq!(err.to_string(), "missing node: missing");
}
#[test]
fn duplicate_edge_display() {
let err = NirError::DuplicateEdge("a".into(), "b".into());
assert_eq!(err.to_string(), "duplicate edge: (a, b)");
}
#[test]
fn invalid_graph_display() {
let err = NirError::InvalidGraph("empty subgraph".into());
assert_eq!(err.to_string(), "invalid graph: empty subgraph");
}
#[test]
fn unsupported_version_display() {
let err = NirError::UnsupportedVersion("99.0".into());
assert_eq!(err.to_string(), "unsupported version: 99.0");
}
#[test]
fn incompatible_version_display_includes_observed_and_policy() {
let present = NirError::IncompatibleVersion {
observed: Some("99.0.0".into()),
policy: "compatible-major majors=[0, 1]".into(),
};
assert_eq!(
present.to_string(),
"unsupported version: 99.0.0 (policy: compatible-major majors=[0, 1])"
);
let missing = NirError::IncompatibleVersion {
observed: None,
policy: "require-present".into(),
};
assert_eq!(
missing.to_string(),
"unsupported version: <absent> (policy: require-present)"
);
}
#[test]
fn missing_field_display() {
let err = NirError::MissingField("weight".into());
assert_eq!(err.to_string(), "missing field: weight");
}
#[test]
fn invalid_tensor_display() {
let err = NirError::InvalidTensor("shape product 4 != data len 3".into());
assert_eq!(
err.to_string(),
"invalid tensor: shape product 4 != data len 3"
);
}
#[test]
fn invalid_node_parameters_display() {
let err = NirError::InvalidNodeParameters {
node: "conv".into(),
node_type: "Conv1d",
kind: ParameterError::WeightRank {
found: 2,
expected: 3,
},
};
assert_eq!(
err.to_string(),
"invalid parameters for Conv1d node conv: weight rank 2 is not 3"
);
}
#[test]
fn parameter_error_displays() {
let cases = [
(
ParameterError::Groups { found: 0 },
"groups must be > 0, found 0",
),
(
ParameterError::ChannelGroupDivisibility {
channels: 3,
groups: 2,
},
"output channels 3 are not divisible by groups 2",
),
(
ParameterError::ExtentArity {
field: "stride",
expected: "1",
found: 2,
},
"stride arity 2 is not 1",
),
(
ParameterError::ExtentRank {
field: "kernel_size",
found: 2,
},
"kernel_size rank 2 is not 0 or 1",
),
(
ParameterError::ExtentDType {
field: "padding",
dtype: "f32",
},
"padding must contain i64 extents, found f32",
),
(
ParameterError::ExtentPositive {
field: "dilation",
index: 0,
value: 0,
},
"dilation extents must be strictly positive, found 0 at index 0",
),
(
ParameterError::ExtentNonNegative {
field: "padding",
index: 1,
value: -1,
},
"padding extents must be non-negative, found -1 at index 1",
),
(
ParameterError::BiasLength {
found: 1,
expected: 2,
},
"bias length 1 is incompatible with 2 output channels",
),
];
for (err, expected) in cases {
assert_eq!(err.to_string(), expected);
}
}
#[test]
fn read_limit_display() {
let err = NirError::ReadLimitExceeded {
context: "lif.tau".into(),
limit: 1024,
used: 768,
requested: 512,
};
assert_eq!(
err.to_string(),
"read allocation limit exceeded at lif.tau: limit 1024 bytes, used 768 bytes, requested 512 bytes"
);
}
#[test]
fn read_count_limit_display_identifies_resource_and_path() {
let err = NirError::ReadCountLimitExceeded {
resource: ReadLimitResource::Nodes,
context: "/node/nodes".into(),
limit: 3,
used: 2,
requested: 2,
};
assert_eq!(
err.to_string(),
"read limit exceeded for nodes at /node/nodes: limit 3, used 2, requested 2"
);
assert_eq!(ReadLimitResource::Edges.to_string(), "edges");
assert_eq!(ReadLimitResource::NestedGraphs.to_string(), "nested graphs");
}
#[test]
fn io_display() {
let err = NirError::Io("unable to open file: model.nir".into());
assert_eq!(err.to_string(), "io error: unable to open file: model.nir");
}
#[test]
fn error_trait_implemented() {
let err: Box<dyn std::error::Error> = Box::new(NirError::Unimplemented("x"));
assert!(err.to_string().contains("not implemented"));
}
}