Skip to main content

hdl_cat_error/
lib.rs

1//! Shared error type for the `hdl-cat` workspace.
2//!
3//! This crate defines a single [`Error`] enum used throughout every other
4//! `hdl-cat-*` crate.  Each variant wraps an underlying concrete error
5//! from `std`, from `comp-cat-rs`, or from a domain context in hdl-cat.
6//!
7//! # Design
8//!
9//! Error handling is explicit and hand-rolled — no `thiserror`, no
10//! `anyhow`, no silent panics.  Every fallible operation in the
11//! workspace returns `Result<T, Error>`.
12//!
13//! `From` impls are provided for every underlying error type so that
14//! `?` propagates cleanly at every call site.
15
16use comp_cat_rs::collapse::free_category::FreeCategoryError;
17
18/// A bit width, measured in bits.
19///
20/// Used in [`Error::WidthMismatch`] to communicate expected/actual
21/// operand widths when a hardware operation's width constraint is
22/// violated.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct Width(u32);
25
26impl Width {
27    /// Construct a `Width` from a raw bit count.
28    #[must_use]
29    pub fn new(bits: u32) -> Self {
30        Self(bits)
31    }
32
33    /// The underlying bit count.
34    #[must_use]
35    pub fn bits(self) -> u32 {
36        self.0
37    }
38}
39
40impl core::fmt::Display for Width {
41    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
42        write!(f, "{} bit(s)", self.0)
43    }
44}
45
46/// A cycle index in a simulation.
47///
48/// Used by [`Error::ImmatureSim`] and by `hdl-cat-sim`'s
49/// `TimedSample` type.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
51pub struct Cycle(u64);
52
53impl Cycle {
54    /// Construct a `Cycle` from a raw index.
55    #[must_use]
56    pub fn new(index: u64) -> Self {
57        Self(index)
58    }
59
60    /// The underlying cycle index.
61    #[must_use]
62    pub fn index(self) -> u64 {
63        self.0
64    }
65}
66
67impl core::fmt::Display for Cycle {
68    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
69        write!(f, "cycle {}", self.0)
70    }
71}
72
73/// A human-readable type name used in mismatch diagnostics.
74///
75/// Opaque newtype: construct via [`TypeName::new`], display via
76/// [`core::fmt::Display`].  Exists so that `Error::TypeMismatch`
77/// cannot be confused with raw `String` domain primitives.
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
79pub struct TypeName(String);
80
81impl TypeName {
82    /// Construct a `TypeName` from any displayable value.
83    pub fn new(name: impl Into<String>) -> Self {
84        Self(name.into())
85    }
86
87    /// The underlying string.
88    #[must_use]
89    pub fn as_str(&self) -> &str {
90        &self.0
91    }
92}
93
94impl core::fmt::Display for TypeName {
95    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
96        f.write_str(&self.0)
97    }
98}
99
100/// A signal name used in trace / codegen diagnostics.
101#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102pub struct SignalName(String);
103
104impl SignalName {
105    /// Construct a `SignalName` from any displayable value.
106    pub fn new(name: impl Into<String>) -> Self {
107        Self(name.into())
108    }
109
110    /// The underlying string.
111    #[must_use]
112    pub fn as_str(&self) -> &str {
113        &self.0
114    }
115}
116
117impl core::fmt::Display for SignalName {
118    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
119        f.write_str(&self.0)
120    }
121}
122
123/// The workspace-wide error enum.
124///
125/// Every fallible operation in `hdl-cat-*` returns `Result<T, Error>`.
126/// Variants either wrap an underlying error type or encode a
127/// domain-specific failure.
128///
129/// # Examples
130///
131/// ```
132/// use hdl_cat_error::{Error, Width};
133///
134/// let e = Error::WidthMismatch {
135///     expected: Width::new(8),
136///     actual: Width::new(4),
137/// };
138/// assert_eq!(
139///     e.to_string(),
140///     "width mismatch: expected 8 bit(s), got 4 bit(s)",
141/// );
142/// ```
143#[derive(Debug)]
144pub enum Error {
145    /// Underlying I/O failure.
146    Io(std::io::Error),
147
148    /// Underlying `core::fmt` failure (e.g. from a writer).
149    Fmt(core::fmt::Error),
150
151    /// Underlying integer parse failure.
152    ParseInt(core::num::ParseIntError),
153
154    /// A free-category IR construction error from `comp-cat-rs`.
155    FreeCategory(FreeCategoryError),
156
157    /// A value did not fit its declared hardware width.
158    WidthMismatch {
159        /// The width declared by the target type.
160        expected: Width,
161        /// The width actually supplied.
162        actual: Width,
163    },
164
165    /// A hardware value's runtime type does not match the expected type.
166    TypeMismatch {
167        /// The expected type's display name.
168        expected: TypeName,
169        /// The actual type's display name.
170        actual: TypeName,
171    },
172
173    /// Two signals from different clock domains were composed.
174    ClockDomainMismatch,
175
176    /// A signal referenced in an IR or codegen pass was never defined.
177    UndefinedSignal {
178        /// The name that could not be resolved.
179        name: SignalName,
180    },
181
182    /// A simulation attempted to read a register before the first
183    /// clock edge had advanced state beyond its initial value.
184    ImmatureSim {
185        /// The cycle at which the read was attempted.
186        cycle: Cycle,
187    },
188
189    /// A value overflowed its declared range during arithmetic.
190    Overflow {
191        /// The width of the operation that overflowed.
192        width: Width,
193    },
194}
195
196impl core::fmt::Display for Error {
197    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
198        match self {
199            Self::Io(e) => write!(f, "I/O error: {e}"),
200            Self::Fmt(e) => write!(f, "formatter error: {e}"),
201            Self::ParseInt(e) => write!(f, "parse-int error: {e}"),
202            Self::FreeCategory(e) => write!(f, "free-category error: {e}"),
203            Self::WidthMismatch { expected, actual } => {
204                write!(f, "width mismatch: expected {expected}, got {actual}")
205            }
206            Self::TypeMismatch { expected, actual } => {
207                write!(f, "type mismatch: expected {expected}, got {actual}")
208            }
209            Self::ClockDomainMismatch => f.write_str("clock domain mismatch"),
210            Self::UndefinedSignal { name } => write!(f, "undefined signal: {name}"),
211            Self::ImmatureSim { cycle } => {
212                write!(f, "simulation read before first clock edge at {cycle}")
213            }
214            Self::Overflow { width } => write!(f, "arithmetic overflow at {width}"),
215        }
216    }
217}
218
219impl std::error::Error for Error {
220    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
221        match self {
222            Self::Io(e) => Some(e),
223            Self::Fmt(e) => Some(e),
224            Self::ParseInt(e) => Some(e),
225            Self::FreeCategory(e) => Some(e),
226            Self::WidthMismatch { .. }
227            | Self::TypeMismatch { .. }
228            | Self::ClockDomainMismatch
229            | Self::UndefinedSignal { .. }
230            | Self::ImmatureSim { .. }
231            | Self::Overflow { .. } => None,
232        }
233    }
234}
235
236impl From<std::io::Error> for Error {
237    fn from(e: std::io::Error) -> Self {
238        Self::Io(e)
239    }
240}
241
242impl From<core::fmt::Error> for Error {
243    fn from(e: core::fmt::Error) -> Self {
244        Self::Fmt(e)
245    }
246}
247
248impl From<core::num::ParseIntError> for Error {
249    fn from(e: core::num::ParseIntError) -> Self {
250        Self::ParseInt(e)
251    }
252}
253
254impl From<FreeCategoryError> for Error {
255    fn from(e: FreeCategoryError) -> Self {
256        Self::FreeCategory(e)
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::{Cycle, Error, SignalName, TypeName, Width};
263
264    #[test]
265    fn width_mismatch_displays_both_widths() {
266        let e = Error::WidthMismatch {
267            expected: Width::new(8),
268            actual: Width::new(4),
269        };
270        assert_eq!(
271            e.to_string(),
272            "width mismatch: expected 8 bit(s), got 4 bit(s)",
273        );
274    }
275
276    #[test]
277    fn type_mismatch_displays_both_names() {
278        let e = Error::TypeMismatch {
279            expected: TypeName::new("Bits<8>"),
280            actual: TypeName::new("Bits<4>"),
281        };
282        assert_eq!(e.to_string(), "type mismatch: expected Bits<8>, got Bits<4>");
283    }
284
285    #[test]
286    fn clock_domain_mismatch_displays_message() {
287        assert_eq!(Error::ClockDomainMismatch.to_string(), "clock domain mismatch");
288    }
289
290    #[test]
291    fn undefined_signal_displays_name() {
292        let e = Error::UndefinedSignal {
293            name: SignalName::new("clk"),
294        };
295        assert_eq!(e.to_string(), "undefined signal: clk");
296    }
297
298    #[test]
299    fn immature_sim_displays_cycle() {
300        let e = Error::ImmatureSim { cycle: Cycle::new(0) };
301        assert_eq!(
302            e.to_string(),
303            "simulation read before first clock edge at cycle 0",
304        );
305    }
306
307    #[test]
308    fn overflow_displays_width() {
309        let e = Error::Overflow { width: Width::new(16) };
310        assert_eq!(e.to_string(), "arithmetic overflow at 16 bit(s)");
311    }
312
313    #[test]
314    fn from_io_error_wraps_without_loss() {
315        let io = std::io::Error::new(std::io::ErrorKind::NotFound, "nope");
316        let e: Error = io.into();
317        let msg = e.to_string();
318        assert!(msg.starts_with("I/O error: "));
319        assert!(msg.contains("nope"));
320    }
321
322    #[test]
323    fn question_mark_propagates_parse_int_into_error() -> Result<(), Error> {
324        let n: i32 = "42".parse()?;
325        assert_eq!(n, 42);
326        Ok(())
327    }
328
329    #[test]
330    fn width_round_trips_through_accessor() {
331        assert_eq!(Width::new(12).bits(), 12);
332    }
333
334    #[test]
335    fn cycle_round_trips_through_accessor() {
336        assert_eq!(Cycle::new(7).index(), 7);
337    }
338
339    #[test]
340    fn type_name_round_trips_through_accessor() {
341        assert_eq!(TypeName::new("Bool").as_str(), "Bool");
342    }
343
344    #[test]
345    fn signal_name_round_trips_through_accessor() {
346        assert_eq!(SignalName::new("rst").as_str(), "rst");
347    }
348}