1use comp_cat_rs::collapse::free_category::FreeCategoryError;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct Width(u32);
25
26impl Width {
27 #[must_use]
29 pub fn new(bits: u32) -> Self {
30 Self(bits)
31 }
32
33 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
51pub struct Cycle(u64);
52
53impl Cycle {
54 #[must_use]
56 pub fn new(index: u64) -> Self {
57 Self(index)
58 }
59
60 #[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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
79pub struct TypeName(String);
80
81impl TypeName {
82 pub fn new(name: impl Into<String>) -> Self {
84 Self(name.into())
85 }
86
87 #[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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102pub struct SignalName(String);
103
104impl SignalName {
105 pub fn new(name: impl Into<String>) -> Self {
107 Self(name.into())
108 }
109
110 #[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#[derive(Debug)]
144pub enum Error {
145 Io(std::io::Error),
147
148 Fmt(core::fmt::Error),
150
151 ParseInt(core::num::ParseIntError),
153
154 FreeCategory(FreeCategoryError),
156
157 WidthMismatch {
159 expected: Width,
161 actual: Width,
163 },
164
165 TypeMismatch {
167 expected: TypeName,
169 actual: TypeName,
171 },
172
173 ClockDomainMismatch,
175
176 UndefinedSignal {
178 name: SignalName,
180 },
181
182 ImmatureSim {
185 cycle: Cycle,
187 },
188
189 Overflow {
191 width: Width,
193 },
194
195 UnsupportedInCircom(&'static str),
203
204 CircomWidthOverflow {
212 width: Width,
214 field_bits: Width,
216 },
217}
218
219impl core::fmt::Display for Error {
220 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
221 match self {
222 Self::Io(e) => write!(f, "I/O error: {e}"),
223 Self::Fmt(e) => write!(f, "formatter error: {e}"),
224 Self::ParseInt(e) => write!(f, "parse-int error: {e}"),
225 Self::FreeCategory(e) => write!(f, "free-category error: {e}"),
226 Self::WidthMismatch { expected, actual } => {
227 write!(f, "width mismatch: expected {expected}, got {actual}")
228 }
229 Self::TypeMismatch { expected, actual } => {
230 write!(f, "type mismatch: expected {expected}, got {actual}")
231 }
232 Self::ClockDomainMismatch => f.write_str("clock domain mismatch"),
233 Self::UndefinedSignal { name } => write!(f, "undefined signal: {name}"),
234 Self::ImmatureSim { cycle } => {
235 write!(f, "simulation read before first clock edge at {cycle}")
236 }
237 Self::Overflow { width } => write!(f, "arithmetic overflow at {width}"),
238 Self::UnsupportedInCircom(what) => {
239 write!(f, "op not supported by the Circom backend: {what}")
240 }
241 Self::CircomWidthOverflow { width, field_bits } => write!(
242 f,
243 "Circom signal width {width} exceeds field capacity {field_bits}",
244 ),
245 }
246 }
247}
248
249impl std::error::Error for Error {
250 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
251 match self {
252 Self::Io(e) => Some(e),
253 Self::Fmt(e) => Some(e),
254 Self::ParseInt(e) => Some(e),
255 Self::FreeCategory(e) => Some(e),
256 Self::WidthMismatch { .. }
257 | Self::TypeMismatch { .. }
258 | Self::ClockDomainMismatch
259 | Self::UndefinedSignal { .. }
260 | Self::ImmatureSim { .. }
261 | Self::Overflow { .. }
262 | Self::UnsupportedInCircom(_)
263 | Self::CircomWidthOverflow { .. } => None,
264 }
265 }
266}
267
268impl From<std::io::Error> for Error {
269 fn from(e: std::io::Error) -> Self {
270 Self::Io(e)
271 }
272}
273
274impl From<core::fmt::Error> for Error {
275 fn from(e: core::fmt::Error) -> Self {
276 Self::Fmt(e)
277 }
278}
279
280impl From<core::num::ParseIntError> for Error {
281 fn from(e: core::num::ParseIntError) -> Self {
282 Self::ParseInt(e)
283 }
284}
285
286impl From<FreeCategoryError> for Error {
287 fn from(e: FreeCategoryError) -> Self {
288 Self::FreeCategory(e)
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::{Cycle, Error, SignalName, TypeName, Width};
295
296 #[test]
297 fn width_mismatch_displays_both_widths() {
298 let e = Error::WidthMismatch {
299 expected: Width::new(8),
300 actual: Width::new(4),
301 };
302 assert_eq!(
303 e.to_string(),
304 "width mismatch: expected 8 bit(s), got 4 bit(s)",
305 );
306 }
307
308 #[test]
309 fn type_mismatch_displays_both_names() {
310 let e = Error::TypeMismatch {
311 expected: TypeName::new("Bits<8>"),
312 actual: TypeName::new("Bits<4>"),
313 };
314 assert_eq!(e.to_string(), "type mismatch: expected Bits<8>, got Bits<4>");
315 }
316
317 #[test]
318 fn clock_domain_mismatch_displays_message() {
319 assert_eq!(Error::ClockDomainMismatch.to_string(), "clock domain mismatch");
320 }
321
322 #[test]
323 fn undefined_signal_displays_name() {
324 let e = Error::UndefinedSignal {
325 name: SignalName::new("clk"),
326 };
327 assert_eq!(e.to_string(), "undefined signal: clk");
328 }
329
330 #[test]
331 fn immature_sim_displays_cycle() {
332 let e = Error::ImmatureSim { cycle: Cycle::new(0) };
333 assert_eq!(
334 e.to_string(),
335 "simulation read before first clock edge at cycle 0",
336 );
337 }
338
339 #[test]
340 fn overflow_displays_width() {
341 let e = Error::Overflow { width: Width::new(16) };
342 assert_eq!(e.to_string(), "arithmetic overflow at 16 bit(s)");
343 }
344
345 #[test]
346 fn from_io_error_wraps_without_loss() {
347 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "nope");
348 let e: Error = io.into();
349 let msg = e.to_string();
350 assert!(msg.starts_with("I/O error: "));
351 assert!(msg.contains("nope"));
352 }
353
354 #[test]
355 fn question_mark_propagates_parse_int_into_error() -> Result<(), Error> {
356 let n: i32 = "42".parse()?;
357 assert_eq!(n, 42);
358 Ok(())
359 }
360
361 #[test]
362 fn width_round_trips_through_accessor() {
363 assert_eq!(Width::new(12).bits(), 12);
364 }
365
366 #[test]
367 fn cycle_round_trips_through_accessor() {
368 assert_eq!(Cycle::new(7).index(), 7);
369 }
370
371 #[test]
372 fn type_name_round_trips_through_accessor() {
373 assert_eq!(TypeName::new("Bool").as_str(), "Bool");
374 }
375
376 #[test]
377 fn signal_name_round_trips_through_accessor() {
378 assert_eq!(SignalName::new("rst").as_str(), "rst");
379 }
380
381 #[test]
382 fn unsupported_in_circom_displays_reason() {
383 let e = Error::UnsupportedInCircom("add");
384 assert_eq!(
385 e.to_string(),
386 "op not supported by the Circom backend: add",
387 );
388 }
389
390 #[test]
391 fn circom_width_overflow_displays_both_widths() {
392 let e = Error::CircomWidthOverflow {
393 width: Width::new(300),
394 field_bits: Width::new(252),
395 };
396 assert_eq!(
397 e.to_string(),
398 "Circom signal width 300 bit(s) exceeds field capacity 252 bit(s)",
399 );
400 }
401}