1use crate::AsciiStr;
2use core::fmt;
3use core::panic::Location;
4
5#[derive(Debug, Clone)]
13pub struct TraceIter<'a, K, const DEPTH: usize, const REASON_LEN: usize>
14where
15 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
16{
17 error: &'a AnErr<K, DEPTH, REASON_LEN>,
18 pos: usize,
19}
20
21impl<'a, K, const DEPTH: usize, const REASON_LEN: usize> Iterator
22 for TraceIter<'a, K, DEPTH, REASON_LEN>
23where
24 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
25{
26 type Item = (
27 K,
28 &'static Location<'static>,
29 Option<&'a AsciiStr<REASON_LEN>>,
30 );
31
32 fn next(&mut self) -> Option<Self::Item> {
33 if self.pos >= self.error.len as usize {
34 return None;
35 }
36
37 let idx = (self.error.len as usize) - 1 - self.pos;
38 let kind = self.error.kinds[idx]?;
39 let loc = self.error.locations[idx]?;
40 let reason = self.error.reasons[idx].as_ref();
41
42 self.pos += 1;
43 Some((kind, loc, reason))
44 }
45
46 fn size_hint(&self) -> (usize, Option<usize>) {
47 let remaining = (self.error.len as usize).saturating_sub(self.pos);
48 (remaining, Some(remaining))
49 }
50}
51
52impl<'a, K, const DEPTH: usize, const REASON_LEN: usize> ExactSizeIterator
53 for TraceIter<'a, K, DEPTH, REASON_LEN>
54where
55 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
56{
57}
58
59#[derive(Clone, Copy, PartialEq, Eq)]
129#[must_use = "this error should be handled or converted to a different type"]
130pub struct AnErr<K, const DEPTH: usize = 3, const REASON_LEN: usize = 29>
131where
132 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
133{
134 pub reasons: [Option<AsciiStr<REASON_LEN>>; DEPTH],
137
138 pub locations: [Option<&'static Location<'static>>; DEPTH],
141
142 pub kinds: [Option<K>; DEPTH],
145
146 pub len: u8,
148}
149
150impl<K, const DEPTH: usize, const REASON_LEN: usize> AnErr<K, DEPTH, REASON_LEN>
151where
152 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
153{
154 #[inline]
156 #[track_caller]
157 pub fn new(kind: K) -> Self {
158 let mut kinds = [None; DEPTH];
159 let mut locs = [None; DEPTH];
160 let reasons = [None; DEPTH];
161
162 kinds[0] = Some(kind);
163 locs[0] = Some(Location::caller());
164
165 Self {
166 kinds,
167 locations: locs,
168 reasons,
169 len: 1,
170 }
171 }
172
173 #[inline]
177 #[track_caller]
178 pub fn with_reason(kind: K, reason: AsciiStr<REASON_LEN>) -> Self {
179 let mut kinds = [None; DEPTH];
180 let mut locs = [None; DEPTH];
181 let mut reasons = [None; DEPTH];
182
183 kinds[0] = Some(kind);
184 locs[0] = Some(Location::caller());
185 reasons[0] = if reason.is_empty() {
186 None
187 } else {
188 Some(reason)
189 };
190
191 Self {
192 kinds,
193 locations: locs,
194 reasons,
195 len: 1,
196 }
197 }
198
199 #[inline]
203 #[track_caller]
204 pub fn with_fmt(kind: K, args: core::fmt::Arguments<'_>) -> Self {
205 let mut kinds = [None; DEPTH];
206 let mut locs = [None; DEPTH];
207 let mut reasons = [None; DEPTH];
208
209 kinds[0] = Some(kind);
210 locs[0] = Some(Location::caller());
211 let reason = AsciiStr::from_fmt(args);
212 reasons[0] = if reason.is_empty() {
213 None
214 } else {
215 Some(reason)
216 };
217
218 Self {
219 kinds,
220 locations: locs,
221 reasons,
222 len: 1,
223 }
224 }
225
226 #[inline]
228 pub fn depth(&self) -> u8 {
229 self.len
230 }
231
232 #[inline]
234 pub fn kind(&self) -> Option<K> {
235 if self.len == 0 {
236 None
237 } else {
238 let idx = (self.len as usize) - 1;
239 self.kinds[idx]
240 }
241 }
242
243 #[inline]
248 #[track_caller]
249 pub fn context(&mut self, kind: K, new_reason: AsciiStr<REASON_LEN>) {
250 let idx = self.len as usize;
251 if idx < DEPTH {
252 self.reasons[idx] = if new_reason.is_empty() {
253 None
254 } else {
255 Some(new_reason)
256 };
257 self.push(kind, Location::caller());
258 }
259 }
260
261 #[inline]
266 #[track_caller]
267 pub fn context_fmt(&mut self, kind: K, args: core::fmt::Arguments<'_>) {
268 let idx = self.len as usize;
269 if idx < DEPTH {
270 let reason = AsciiStr::from_fmt(args);
271 self.reasons[idx] = if reason.is_empty() {
272 None
273 } else {
274 Some(reason)
275 };
276 self.push(kind, Location::caller());
277 }
278 }
279
280 pub fn trace(&self) -> TraceIter<'_, K, DEPTH, REASON_LEN> {
286 TraceIter {
287 error: self,
288 pos: 0,
289 }
290 }
291
292 #[inline]
293 fn push(&mut self, kind: K, loc: &'static Location<'static>) {
294 if (self.len as usize) < DEPTH {
295 let idx = self.len as usize;
296 self.kinds[idx] = Some(kind);
297 self.locations[idx] = Some(loc);
298 self.len += 1;
299 }
300 }
301
302 #[cfg(feature = "wire")]
318 pub fn to_wire_bytes<const PATH_LEN: usize>(
319 &self,
320 kind_to_u16: impl Fn(K) -> u16,
321 buf: &mut [u8],
322 ) -> Result<usize, ()> {
323 let needed = Self::wire_size::<PATH_LEN>();
324 if buf.len() < needed {
325 return Err(());
326 }
327
328 let mut offset = 0;
329
330 buf[offset] = 1; offset += 1;
333 buf[offset] = self.len;
334 offset += 1;
335
336 for i in 0..DEPTH {
337 if i < self.len as usize {
338 let kind_val = self.kinds[i].map_or(0, &kind_to_u16);
340 buf[offset..offset + 2].copy_from_slice(&kind_val.to_le_bytes());
341 offset += 2;
342
343 let reason = self.reasons[i].as_ref().unwrap_or(&AsciiStr::DEFAULT);
345 buf[offset..offset + REASON_LEN].copy_from_slice(&reason.to_wire_bytes());
346 offset += REASON_LEN;
347
348 if let Some(loc) = self.locations[i] {
350 let file = AsciiStr::<PATH_LEN>::from_str_truncate(loc.file());
351 buf[offset..offset + PATH_LEN].copy_from_slice(&file.to_wire_bytes());
352 offset += PATH_LEN;
353
354 buf[offset..offset + 4].copy_from_slice(&loc.line().to_le_bytes());
355 offset += 4;
356 buf[offset..offset + 4].copy_from_slice(&loc.column().to_le_bytes());
357 offset += 4;
358 } else {
359 offset += PATH_LEN + 8; }
361 } else {
362 offset += 2 + REASON_LEN + PATH_LEN + 8;
364 }
365 }
366
367 Ok(needed)
368 }
369
370 #[cfg(feature = "wire")]
372 pub const fn wire_size<const PATH_LEN: usize>() -> usize {
373 2 + DEPTH * (2 + REASON_LEN + PATH_LEN + 8)
374 }
375}
376
377impl<K, const DEPTH: usize, const REASON_LEN: usize> From<K> for AnErr<K, DEPTH, REASON_LEN>
378where
379 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
380{
381 #[inline]
383 #[track_caller]
384 fn from(kind: K) -> Self {
385 Self::new(kind)
386 }
387}
388
389impl<K, const DEPTH: usize, const REASON_LEN: usize> core::fmt::Display
390 for AnErr<K, DEPTH, REASON_LEN>
391where
392 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
393{
394 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
395 writeln!(f)?;
396 writeln!(f, "--")?;
397 writeln!(f, "Error:")?;
398
399 for (i, (kind, loc, reason_opt)) in self.trace().enumerate() {
400 let num = i + 1;
401
402 write!(f, " {:>2}. {:?}", num, kind)?;
403
404 if let Some(reason) = reason_opt {
405 if let Ok(s) = reason.as_str() {
406 write!(f, ": {}", s)?;
407 } else {
408 write!(f, ": <invalid ascii>")?;
409 }
410 }
411
412 writeln!(f, " @ {}:{}:{}", loc.file(), loc.line(), loc.column())?;
413 }
414
415 Ok(())
416 }
417}
418
419impl<K, const DEPTH: usize, const REASON_LEN: usize> fmt::Debug for AnErr<K, DEPTH, REASON_LEN>
420where
421 K: Copy + Clone + fmt::Debug + PartialEq + Eq,
422{
423 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 fmt::Display::fmt(self, f)
428 }
429}
430
431impl<K, const DEPTH: usize, const REASON_LEN: usize> core::error::Error
432 for AnErr<K, DEPTH, REASON_LEN>
433where
434 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
435{
436}
437
438#[macro_export]
452macro_rules! an_err {
453 ($kind:expr) => {
455 $crate::AnErr::new($kind)
456 };
457
458 ($kind:expr, $fmt:literal $(, $arg:expr)* => $inner:expr $(,)?) => {{
460 let mut e = $inner;
461 e.context_fmt(
462 $kind,
463 format_args!($fmt $(, $arg)*)
464 );
465 e
466 }};
467
468 ($kind:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
470 $crate::AnErr::with_fmt($kind, format_args!($fmt $(, $arg)*))
471 };
472}
473
474#[cfg(feature = "wire")]
476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
477pub struct WireLocation<const N: usize> {
478 pub file: AsciiStr<N>,
479 pub line: u32,
480 pub column: u32,
481}
482
483#[cfg(feature = "wire")]
485#[derive(Debug, Clone, Copy, PartialEq, Eq)]
486pub struct WireErr<const DEPTH: usize = 3, const REASON_LEN: usize = 29, const FILE_LEN: usize = 80>
487{
488 pub len: u8,
489 pub kinds: [Option<u16>; DEPTH],
490 pub reasons: [Option<AsciiStr<REASON_LEN>>; DEPTH],
491 pub locations: [Option<WireLocation<FILE_LEN>>; DEPTH],
492}
493
494#[cfg(feature = "wire")]
495impl<const DEPTH: usize, const REASON_LEN: usize, const FILE_LEN: usize>
496 WireErr<DEPTH, REASON_LEN, FILE_LEN>
497{
498 pub const fn wire_size() -> usize {
500 const fn compute_size<const D: usize, const R: usize, const F: usize>() -> usize {
501 2 + D * (2 + R + F + 8)
502 }
503 compute_size::<DEPTH, REASON_LEN, FILE_LEN>()
504 }
505
506 pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
511 if bytes.len() != Self::wire_size() {
512 return None;
513 }
514
515 let mut offset = 0;
516
517 let version = bytes[offset];
519 if version != 1 {
520 return None; }
522 offset += 1;
523
524 let len = bytes[offset];
525 if len == 0 || len as usize > DEPTH {
526 return None;
527 }
528 offset += 1;
529
530 let mut kinds = [None; DEPTH];
531 let mut reasons = [None; DEPTH];
532 let mut locations = [None; DEPTH];
533
534 for i in 0..(len as usize) {
535 let kind_bytes = <[u8; 2]>::try_from(&bytes[offset..offset + 2]).ok()?;
537 kinds[i] = Some(u16::from_le_bytes(kind_bytes));
538 offset += 2;
539
540 let reason_bytes = &bytes[offset..offset + REASON_LEN];
542 reasons[i] = AsciiStr::from_wire_bytes(reason_bytes);
543 offset += REASON_LEN;
544
545 let file_bytes = &bytes[offset..offset + FILE_LEN];
547 let file = AsciiStr::from_wire_bytes(file_bytes)?;
548
549 offset += FILE_LEN;
550
551 let line_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
552 let line = u32::from_le_bytes(line_bytes);
553 offset += 4;
554
555 let col_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
556 let column = u32::from_le_bytes(col_bytes);
557 offset += 4;
558
559 locations[i] = Some(WireLocation { file, line, column });
560 }
561
562 Some(WireErr {
565 len,
566 kinds,
567 reasons,
568 locations,
569 })
570 }
571}
572
573#[cfg(feature = "alloc")]
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use alloc::format;
578 use alloc::vec::Vec;
579
580 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
581 #[repr(u8)]
582 enum TestKind {
583 Root,
584 Context1,
585 Context2,
586 Parse,
587 Io,
588 }
589
590 fn r<const N: usize>(s: &str) -> AsciiStr<N> {
592 AsciiStr::from_str_truncate(s)
593 }
594
595 type E3 = AnErr<TestKind, 3, 29>;
598
599 #[test]
600 fn test_new_from_and_basic_properties() {
601 let e1: E3 = AnErr::new(TestKind::Root);
602 let e2: E3 = TestKind::Root.into();
603
604 assert_eq!(e1.depth(), e2.depth());
608 assert_eq!(e1.kind(), e2.kind());
609 assert_eq!(e1.depth(), 1);
610 assert_eq!(e1.kind(), Some(TestKind::Root));
611
612 let mut trace = e1.trace();
613 let (kind, _loc, reason) = trace.next().unwrap();
614 assert_eq!(kind, TestKind::Root);
615 assert!(reason.is_none());
616 assert!(trace.next().is_none());
617 }
618
619 #[test]
620 fn test_with_reason_and_with_fmt() {
621 let e: E3 = AnErr::with_reason(TestKind::Parse, r::<29>("bad token"));
623 assert_eq!(e.depth(), 1);
624
625 let items: Vec<_> = e.trace().collect();
626 assert_eq!(items[0].2.unwrap().as_str().unwrap(), "bad token");
627
628 let e2: E3 = AnErr::with_fmt(
629 TestKind::Io,
630 format_args!("file not found: {}", "config.toml"),
631 );
632 let items2: Vec<_> = e2.trace().collect();
633 assert_eq!(
634 items2[0].2.unwrap().as_str().unwrap(),
635 "file not found: config.toml"
636 );
637 }
638
639 #[test]
640 fn test_an_err_macro_all_forms() {
641 let e1: E3 = an_err!(TestKind::Root);
642 assert_eq!(e1.kind(), Some(TestKind::Root));
643
644 let e2: E3 = an_err!(TestKind::Parse, "unexpected {}", "EOF");
645 assert_eq!(
646 e2.trace().next().unwrap().2.unwrap().as_str().unwrap(),
647 "unexpected EOF"
648 );
649
650 let inner: E3 = an_err!(TestKind::Parse, "bad data");
652 let outer: E3 = an_err!(TestKind::Io, "while reading file" => inner);
653
654 assert_eq!(outer.depth(), 2);
655 let mut t = outer.trace();
656 let (k1, _, r1) = t.next().unwrap();
657 assert_eq!(k1, TestKind::Io);
658 assert_eq!(r1.unwrap().as_str().unwrap(), "while reading file");
659
660 let (k2, _, r2) = t.next().unwrap();
661 assert_eq!(k2, TestKind::Parse);
662 assert_eq!(r2.unwrap().as_str().unwrap(), "bad data");
663 }
664
665 #[test]
666 fn test_context_and_context_fmt() {
667 let mut e: E3 = an_err!(TestKind::Root, "initial");
668 e.context(TestKind::Context1, r::<29>("level 1"));
669 e.context_fmt(TestKind::Context2, format_args!("level {}", 2));
670
671 assert_eq!(e.depth(), 3);
672
673 let trace: Vec<_> = e.trace().collect();
674 assert_eq!(trace[0].0, TestKind::Context2);
676 assert_eq!(trace[1].0, TestKind::Context1);
677 assert_eq!(trace[2].0, TestKind::Root);
678
679 assert_eq!(trace[0].2.unwrap().as_str().unwrap(), "level 2");
680 assert_eq!(trace[1].2.unwrap().as_str().unwrap(), "level 1");
681 assert_eq!(trace[2].2.unwrap().as_str().unwrap(), "initial");
682 }
683
684 #[test]
685 fn test_max_depth_is_no_op() {
686 let mut e: E3 = an_err!(TestKind::Root);
687 for i in 0..10 {
688 e.context(TestKind::Context1, r::<29>(&format!("extra {i}")));
689 }
690 assert_eq!(e.depth(), 3); let trace: Vec<_> = e.trace().collect();
693 assert_eq!(trace.len(), 3);
694 assert_eq!(trace[0].0, TestKind::Context1); }
696
697 #[test]
698 fn test_empty_reason_becomes_none() {
699 let e: E3 = an_err!(TestKind::Parse, "");
700 let (_, _, reason) = e.trace().next().unwrap();
701 assert!(reason.is_none());
702
703 let mut e2: E3 = an_err!(TestKind::Root);
704 e2.context(TestKind::Io, r::<29>("")); let items: Vec<_> = e2.trace().collect();
706 assert!(items[0].2.is_none());
707 }
708
709 #[test]
710 fn test_trace_iter_order_exact_size_and_size_hint() {
711 let e: E3 = an_err!(TestKind::Root, "a" => an_err!(TestKind::Io, "b" => an_err!(TestKind::Parse, "c")));
712
713 let trace = e.trace();
714 assert_eq!(trace.len(), 3); assert_eq!(trace.size_hint(), (3, Some(3)));
716
717 let collected: Vec<_> = trace.collect();
718 assert_eq!(collected.len(), 3);
719 assert_eq!(collected[0].0, TestKind::Root); assert_eq!(collected[1].0, TestKind::Io);
721 assert_eq!(collected[2].0, TestKind::Parse); }
723
724 #[test]
725 fn test_kind_returns_most_recent() {
726 let mut e: E3 = an_err!(TestKind::Parse);
727 e.context(TestKind::Context1, r::<29>("ctx1"));
728 e.context(TestKind::Context2, r::<29>("ctx2"));
729
730 assert_eq!(e.kind(), Some(TestKind::Context2)); }
732
733 #[test]
734 fn test_display() {
735 let inner: E3 = an_err!(TestKind::Parse, "bad syntax");
736 let e: E3 = an_err!(TestKind::Io, "while loading config" => inner);
737
738 let display = format!("{}", e);
739 assert!(display.contains("--"));
740 assert!(display.contains("Error:"));
741 assert!(display.contains("Io"));
742 assert!(display.contains("while loading config"));
743 assert!(display.contains("Parse"));
744 assert!(display.contains("bad syntax"));
745 }
746
747 #[cfg(feature = "wire")]
748 type E4 = AnErr<TestKind, 4, 29>;
749 #[cfg(feature = "wire")]
750 use alloc::vec;
751
752 #[cfg(feature = "wire")]
753 #[test]
754 fn test_wire_roundtrip() {
755 let inner: E4 = an_err!(TestKind::Parse, "unexpected char");
756 let e: E4 = an_err!(TestKind::Io, "while processing file" => inner);
757
758 const FILE_LEN: usize = 64;
759 let wire_size = E4::wire_size::<FILE_LEN>();
760 let mut buf = vec![0u8; wire_size];
761
762 let written = e.to_wire_bytes::<FILE_LEN>(|k| k as u16, &mut buf).unwrap();
764 assert_eq!(written, wire_size);
765
766 let wire_err = WireErr::<4, 29, FILE_LEN>::from_wire_bytes(&buf[..written]).unwrap();
767
768 assert_eq!(wire_err.len, 2);
769
770 assert_eq!(wire_err.kinds[0], Some(TestKind::Parse as u16));
772 assert_eq!(wire_err.kinds[1], Some(TestKind::Io as u16));
773
774 assert_eq!(
775 wire_err.reasons[0].as_ref().unwrap().as_str().unwrap(),
776 "unexpected char"
777 );
778 assert_eq!(
779 wire_err.reasons[1].as_ref().unwrap().as_str().unwrap(),
780 "while processing file"
781 );
782 }
783
784 #[cfg(feature = "wire")]
785 #[test]
786 fn test_wire_invalid_cases() {
787 assert!(WireErr::<3, 29, 64>::from_wire_bytes(&[0u8; 10]).is_none());
789
790 let mut buf = vec![0u8; E4::wire_size::<64>()];
792 buf[0] = 99; assert!(WireErr::<4, 29, 64>::from_wire_bytes(&buf).is_none());
794 }
795}