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)]
168#[must_use = "this error should be handled or converted to a different type e.g `pub type DtErr = AnErr<MyError, 2, 49>;`"]
169pub struct AnErr<K, const DEPTH: usize = 3, const REASON_LEN: usize = 29>
170where
171 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
172{
173 pub reasons: [Option<AsciiStr<REASON_LEN>>; DEPTH],
176
177 pub locations: [Option<&'static Location<'static>>; DEPTH],
180
181 pub kinds: [Option<K>; DEPTH],
184
185 pub len: u8,
187}
188
189impl<K, const DEPTH: usize, const REASON_LEN: usize> AnErr<K, DEPTH, REASON_LEN>
190where
191 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
192{
193 #[inline]
195 #[track_caller]
196 pub fn new(kind: K) -> Self {
197 let mut kinds = [None; DEPTH];
198 let mut locs = [None; DEPTH];
199 let reasons = [None; DEPTH];
200
201 kinds[0] = Some(kind);
202 locs[0] = Some(Location::caller());
203
204 Self {
205 kinds,
206 locations: locs,
207 reasons,
208 len: 1,
209 }
210 }
211
212 #[inline]
216 #[track_caller]
217 pub fn with_reason(kind: K, reason: AsciiStr<REASON_LEN>) -> Self {
218 let mut kinds = [None; DEPTH];
219 let mut locs = [None; DEPTH];
220 let mut reasons = [None; DEPTH];
221
222 kinds[0] = Some(kind);
223 locs[0] = Some(Location::caller());
224 reasons[0] = if reason.is_empty() {
225 None
226 } else {
227 Some(reason)
228 };
229
230 Self {
231 kinds,
232 locations: locs,
233 reasons,
234 len: 1,
235 }
236 }
237
238 #[inline]
242 #[track_caller]
243 pub fn with_fmt(kind: K, args: core::fmt::Arguments<'_>) -> Self {
244 let mut kinds = [None; DEPTH];
245 let mut locs = [None; DEPTH];
246 let mut reasons = [None; DEPTH];
247
248 kinds[0] = Some(kind);
249 locs[0] = Some(Location::caller());
250 let reason = AsciiStr::from_fmt(args);
251 reasons[0] = if reason.is_empty() {
252 None
253 } else {
254 Some(reason)
255 };
256
257 Self {
258 kinds,
259 locations: locs,
260 reasons,
261 len: 1,
262 }
263 }
264
265 #[inline]
267 pub fn depth(&self) -> u8 {
268 self.len
269 }
270
271 #[inline]
273 pub fn kind(&self) -> Option<K> {
274 if self.len == 0 {
275 None
276 } else {
277 let idx = (self.len as usize) - 1;
278 self.kinds[idx]
279 }
280 }
281
282 #[inline]
287 #[track_caller]
288 pub fn context(&mut self, kind: K, new_reason: AsciiStr<REASON_LEN>) {
289 let idx = self.len as usize;
290 if idx < DEPTH {
291 self.reasons[idx] = if new_reason.is_empty() {
292 None
293 } else {
294 Some(new_reason)
295 };
296 self.push(kind, Location::caller());
297 }
298 }
299
300 #[inline]
305 #[track_caller]
306 pub fn context_fmt(&mut self, kind: K, args: core::fmt::Arguments<'_>) {
307 let idx = self.len as usize;
308 if idx < DEPTH {
309 let reason = AsciiStr::from_fmt(args);
310 self.reasons[idx] = if reason.is_empty() {
311 None
312 } else {
313 Some(reason)
314 };
315 self.push(kind, Location::caller());
316 }
317 }
318
319 pub fn trace(&self) -> TraceIter<'_, K, DEPTH, REASON_LEN> {
325 TraceIter {
326 error: self,
327 pos: 0,
328 }
329 }
330
331 #[inline]
332 fn push(&mut self, kind: K, loc: &'static Location<'static>) {
333 if (self.len as usize) < DEPTH {
334 let idx = self.len as usize;
335 self.kinds[idx] = Some(kind);
336 self.locations[idx] = Some(loc);
337 self.len += 1;
338 }
339 }
340
341 #[inline]
348 pub fn get(
349 &self,
350 index: usize,
351 ) -> Option<(K, &'static Location<'static>, Option<&AsciiStr<REASON_LEN>>)> {
352 let depth = self.len as usize;
353 if index >= depth {
354 return None;
355 }
356 let arr_idx = depth - 1 - index; Some((
358 self.kinds[arr_idx]?,
359 self.locations[arr_idx]?,
360 self.reasons[arr_idx].as_ref(),
361 ))
362 }
363
364 #[inline]
366 pub fn location(&self) -> Option<&'static Location<'static>> {
367 self.get(0).map(|(_, loc, _)| loc)
368 }
369
370 #[inline]
372 pub fn reason(&self) -> Option<&AsciiStr<REASON_LEN>> {
373 self.get(0).and_then(|(_, _, r)| r)
374 }
375
376 #[inline]
378 pub fn root_kind(&self) -> Option<K> {
379 (self.len > 0).then(|| self.kinds[0]).flatten()
380 }
381
382 #[inline]
384 pub fn root_location(&self) -> Option<&'static Location<'static>> {
385 (self.len > 0).then(|| self.locations[0]).flatten()
386 }
387
388 #[inline]
390 pub fn root_reason(&self) -> Option<&AsciiStr<REASON_LEN>> {
391 (self.len > 0).then(|| self.reasons[0].as_ref()).flatten()
392 }
393}
394
395impl<K, const DEPTH: usize, const REASON_LEN: usize> From<K> for AnErr<K, DEPTH, REASON_LEN>
396where
397 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
398{
399 #[inline]
401 #[track_caller]
402 fn from(kind: K) -> Self {
403 Self::new(kind)
404 }
405}
406
407impl<K, const DEPTH: usize, const REASON_LEN: usize> core::fmt::Display
408 for AnErr<K, DEPTH, REASON_LEN>
409where
410 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
411{
412 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
413 writeln!(f)?;
414 writeln!(f, "--")?;
415 writeln!(f, "Error:")?;
416
417 for (i, (kind, loc, reason_opt)) in self.trace().enumerate() {
418 let num = i + 1;
419
420 write!(f, " {:>2}. {:?}", num, kind)?;
421
422 if let Some(reason) = reason_opt {
423 if let Ok(s) = reason.as_str() {
424 write!(f, ": {}", s)?;
425 } else {
426 write!(f, ": <invalid ascii>")?;
427 }
428 }
429
430 writeln!(f, " @ {}:{}:{}", loc.file(), loc.line(), loc.column())?;
431 }
432
433 Ok(())
434 }
435}
436
437impl<K, const DEPTH: usize, const REASON_LEN: usize> fmt::Debug for AnErr<K, DEPTH, REASON_LEN>
438where
439 K: Copy + Clone + fmt::Debug + PartialEq + Eq,
440{
441 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445 fmt::Display::fmt(self, f)
446 }
447}
448
449impl<K, const DEPTH: usize, const REASON_LEN: usize> core::error::Error
450 for AnErr<K, DEPTH, REASON_LEN>
451where
452 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
453{
454}
455
456#[macro_export]
470macro_rules! an_err {
471 ($kind:expr) => {
473 $crate::AnErr::new($kind)
474 };
475
476 ($kind:expr, $fmt:literal $(, $arg:expr)* => $inner:expr $(,)?) => {{
478 let mut e = $inner;
479 e.context_fmt(
480 $kind,
481 format_args!($fmt $(, $arg)*)
482 );
483 e
484 }};
485
486 ($kind:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
488 $crate::AnErr::with_fmt($kind, format_args!($fmt $(, $arg)*))
489 };
490}
491
492#[cfg(feature = "wire")]
493impl<K, const DEPTH: usize, const REASON_LEN: usize> AnErr<K, DEPTH, REASON_LEN>
494where
495 K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
496{
497 pub fn to_wire_bytes<const PATH_LEN: usize>(
513 &self,
514 kind_to_u16: impl Fn(K) -> u16,
515 buf: &mut [u8],
516 ) -> Result<usize, ()> {
517 let needed = Self::wire_size::<PATH_LEN>();
518 if buf.len() < needed {
519 return Err(());
520 }
521
522 let mut offset = 0;
523
524 buf[offset] = 1; offset += 1;
527 buf[offset] = self.len;
528 offset += 1;
529
530 for i in 0..DEPTH {
531 if i < self.len as usize {
532 let kind_val = self.kinds[i].map_or(0, &kind_to_u16);
534 buf[offset..offset + 2].copy_from_slice(&kind_val.to_le_bytes());
535 offset += 2;
536
537 let reason = self.reasons[i].as_ref().unwrap_or(&AsciiStr::DEFAULT);
539 buf[offset..offset + REASON_LEN].copy_from_slice(&reason.to_wire_bytes());
540 offset += REASON_LEN;
541
542 if let Some(loc) = self.locations[i] {
544 let file = AsciiStr::<PATH_LEN>::from_str_truncate(loc.file());
545 buf[offset..offset + PATH_LEN].copy_from_slice(&file.to_wire_bytes());
546 offset += PATH_LEN;
547
548 buf[offset..offset + 4].copy_from_slice(&loc.line().to_le_bytes());
549 offset += 4;
550 buf[offset..offset + 4].copy_from_slice(&loc.column().to_le_bytes());
551 offset += 4;
552 } else {
553 offset += PATH_LEN + 8; }
555 } else {
556 offset += 2 + REASON_LEN + PATH_LEN + 8;
558 }
559 }
560
561 Ok(needed)
562 }
563
564 pub const fn wire_size<const PATH_LEN: usize>() -> usize {
567 2 + DEPTH * (2 + REASON_LEN + PATH_LEN + 8)
568 }
569}
570
571#[cfg(feature = "wire")]
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
574pub struct WireLocation<const N: usize> {
575 pub file: AsciiStr<N>,
576 pub line: u32,
577 pub column: u32,
578}
579
580#[cfg(feature = "wire")]
582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
583pub struct WireErr<const DEPTH: usize = 3, const REASON_LEN: usize = 29, const FILE_LEN: usize = 80>
584{
585 pub len: u8,
586 pub kinds: [Option<u16>; DEPTH],
587 pub reasons: [Option<AsciiStr<REASON_LEN>>; DEPTH],
588 pub locations: [Option<WireLocation<FILE_LEN>>; DEPTH],
589}
590
591#[cfg(feature = "wire")]
592impl<const DEPTH: usize, const REASON_LEN: usize, const FILE_LEN: usize>
593 WireErr<DEPTH, REASON_LEN, FILE_LEN>
594{
595 pub const fn wire_size() -> usize {
597 const fn compute_size<const D: usize, const R: usize, const F: usize>() -> usize {
598 2 + D * (2 + R + F + 8)
599 }
600 compute_size::<DEPTH, REASON_LEN, FILE_LEN>()
601 }
602
603 pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
608 if bytes.len() != Self::wire_size() {
609 return None;
610 }
611
612 let mut offset = 0;
613
614 let version = bytes[offset];
616 if version != 1 {
617 return None; }
619 offset += 1;
620
621 let len = bytes[offset];
622 if len == 0 || len as usize > DEPTH {
623 return None;
624 }
625 offset += 1;
626
627 let mut kinds = [None; DEPTH];
628 let mut reasons = [None; DEPTH];
629 let mut locations = [None; DEPTH];
630
631 for i in 0..(len as usize) {
632 let kind_bytes = <[u8; 2]>::try_from(&bytes[offset..offset + 2]).ok()?;
634 kinds[i] = Some(u16::from_le_bytes(kind_bytes));
635 offset += 2;
636
637 let reason_bytes = &bytes[offset..offset + REASON_LEN];
639 reasons[i] = AsciiStr::from_wire_bytes(reason_bytes);
640 offset += REASON_LEN;
641
642 let file_bytes = &bytes[offset..offset + FILE_LEN];
644 let file = AsciiStr::from_wire_bytes(file_bytes)?;
645
646 offset += FILE_LEN;
647
648 let line_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
649 let line = u32::from_le_bytes(line_bytes);
650 offset += 4;
651
652 let col_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
653 let column = u32::from_le_bytes(col_bytes);
654 offset += 4;
655
656 locations[i] = Some(WireLocation { file, line, column });
657 }
658
659 Some(WireErr {
662 len,
663 kinds,
664 reasons,
665 locations,
666 })
667 }
668}
669
670#[cfg(feature = "alloc")]
671#[cfg(test)]
672mod tests {
673 use super::*;
674 use alloc::format;
675 use alloc::vec::Vec;
676
677 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
678 #[repr(u8)]
679 enum TestKind {
680 Root,
681 Context1,
682 Context2,
683 Parse,
684 Io,
685 }
686
687 fn r<const N: usize>(s: &str) -> AsciiStr<N> {
689 AsciiStr::from_str_truncate(s)
690 }
691
692 type E3 = AnErr<TestKind, 3, 29>;
695
696 #[test]
697 fn test_new_from_and_basic_properties() {
698 let e1: E3 = AnErr::new(TestKind::Root);
699 let e2: E3 = TestKind::Root.into();
700
701 assert_eq!(e1.depth(), e2.depth());
705 assert_eq!(e1.kind(), e2.kind());
706 assert_eq!(e1.depth(), 1);
707 assert_eq!(e1.kind(), Some(TestKind::Root));
708
709 let mut trace = e1.trace();
710 let (kind, _loc, reason) = trace.next().unwrap();
711 assert_eq!(kind, TestKind::Root);
712 assert!(reason.is_none());
713 assert!(trace.next().is_none());
714 }
715
716 #[test]
717 fn test_with_reason_and_with_fmt() {
718 let e: E3 = AnErr::with_reason(TestKind::Parse, r::<29>("bad token"));
720 assert_eq!(e.depth(), 1);
721
722 let items: Vec<_> = e.trace().collect();
723 assert_eq!(items[0].2.unwrap().as_str().unwrap(), "bad token");
724
725 let e2: E3 = AnErr::with_fmt(
726 TestKind::Io,
727 format_args!("file not found: {}", "config.toml"),
728 );
729 let items2: Vec<_> = e2.trace().collect();
730 assert_eq!(
731 items2[0].2.unwrap().as_str().unwrap(),
732 "file not found: config.toml"
733 );
734 }
735
736 #[test]
737 fn test_an_err_macro_all_forms() {
738 let e1: E3 = an_err!(TestKind::Root);
739 assert_eq!(e1.kind(), Some(TestKind::Root));
740
741 let e2: E3 = an_err!(TestKind::Parse, "unexpected {}", "EOF");
742 assert_eq!(
743 e2.trace().next().unwrap().2.unwrap().as_str().unwrap(),
744 "unexpected EOF"
745 );
746
747 let inner: E3 = an_err!(TestKind::Parse, "bad data");
749 let outer: E3 = an_err!(TestKind::Io, "while reading file" => inner);
750
751 assert_eq!(outer.depth(), 2);
752 let mut t = outer.trace();
753 let (k1, _, r1) = t.next().unwrap();
754 assert_eq!(k1, TestKind::Io);
755 assert_eq!(r1.unwrap().as_str().unwrap(), "while reading file");
756
757 let (k2, _, r2) = t.next().unwrap();
758 assert_eq!(k2, TestKind::Parse);
759 assert_eq!(r2.unwrap().as_str().unwrap(), "bad data");
760 }
761
762 #[test]
763 fn test_context_and_context_fmt() {
764 let mut e: E3 = an_err!(TestKind::Root, "initial");
765 e.context(TestKind::Context1, r::<29>("level 1"));
766 e.context_fmt(TestKind::Context2, format_args!("level {}", 2));
767
768 assert_eq!(e.depth(), 3);
769
770 let trace: Vec<_> = e.trace().collect();
771 assert_eq!(trace[0].0, TestKind::Context2);
773 assert_eq!(trace[1].0, TestKind::Context1);
774 assert_eq!(trace[2].0, TestKind::Root);
775
776 assert_eq!(trace[0].2.unwrap().as_str().unwrap(), "level 2");
777 assert_eq!(trace[1].2.unwrap().as_str().unwrap(), "level 1");
778 assert_eq!(trace[2].2.unwrap().as_str().unwrap(), "initial");
779 }
780
781 #[test]
782 fn test_max_depth_is_no_op() {
783 let mut e: E3 = an_err!(TestKind::Root);
784 for i in 0..10 {
785 e.context(TestKind::Context1, r::<29>(&format!("extra {i}")));
786 }
787 assert_eq!(e.depth(), 3); let trace: Vec<_> = e.trace().collect();
790 assert_eq!(trace.len(), 3);
791 assert_eq!(trace[0].0, TestKind::Context1); }
793
794 #[test]
795 fn test_empty_reason_becomes_none() {
796 let e: E3 = an_err!(TestKind::Parse, "");
797 let (_, _, reason) = e.trace().next().unwrap();
798 assert!(reason.is_none());
799
800 let mut e2: E3 = an_err!(TestKind::Root);
801 e2.context(TestKind::Io, r::<29>("")); let items: Vec<_> = e2.trace().collect();
803 assert!(items[0].2.is_none());
804 }
805
806 #[test]
807 fn test_trace_iter_order_exact_size_and_size_hint() {
808 let e: E3 = an_err!(TestKind::Root, "a" => an_err!(TestKind::Io, "b" => an_err!(TestKind::Parse, "c")));
809
810 let trace = e.trace();
811 assert_eq!(trace.len(), 3); assert_eq!(trace.size_hint(), (3, Some(3)));
813
814 let collected: Vec<_> = trace.collect();
815 assert_eq!(collected.len(), 3);
816 assert_eq!(collected[0].0, TestKind::Root); assert_eq!(collected[1].0, TestKind::Io);
818 assert_eq!(collected[2].0, TestKind::Parse); }
820
821 #[test]
822 fn test_kind_returns_most_recent() {
823 let mut e: E3 = an_err!(TestKind::Parse);
824 e.context(TestKind::Context1, r::<29>("ctx1"));
825 e.context(TestKind::Context2, r::<29>("ctx2"));
826
827 assert_eq!(e.kind(), Some(TestKind::Context2)); }
829
830 #[test]
831 fn test_display() {
832 let inner: E3 = an_err!(TestKind::Parse, "bad syntax");
833 let e: E3 = an_err!(TestKind::Io, "while loading config" => inner);
834
835 let display = format!("{}", e);
836 assert!(display.contains("--"));
837 assert!(display.contains("Error:"));
838 assert!(display.contains("Io"));
839 assert!(display.contains("while loading config"));
840 assert!(display.contains("Parse"));
841 assert!(display.contains("bad syntax"));
842 }
843
844 #[cfg(feature = "wire")]
845 type E4 = AnErr<TestKind, 4, 29>;
846 #[cfg(feature = "wire")]
847 use alloc::vec;
848
849 #[cfg(feature = "wire")]
850 #[test]
851 fn test_wire_roundtrip() {
852 let inner: E4 = an_err!(TestKind::Parse, "unexpected char");
853 let e: E4 = an_err!(TestKind::Io, "while processing file" => inner);
854
855 const FILE_LEN: usize = 64;
856 let wire_size = E4::wire_size::<FILE_LEN>();
857 let mut buf = vec![0u8; wire_size];
858
859 let written = e.to_wire_bytes::<FILE_LEN>(|k| k as u16, &mut buf).unwrap();
861 assert_eq!(written, wire_size);
862
863 let wire_err = WireErr::<4, 29, FILE_LEN>::from_wire_bytes(&buf[..written]).unwrap();
864
865 assert_eq!(wire_err.len, 2);
866
867 assert_eq!(wire_err.kinds[0], Some(TestKind::Parse as u16));
869 assert_eq!(wire_err.kinds[1], Some(TestKind::Io as u16));
870
871 assert_eq!(
872 wire_err.reasons[0].as_ref().unwrap().as_str().unwrap(),
873 "unexpected char"
874 );
875 assert_eq!(
876 wire_err.reasons[1].as_ref().unwrap().as_str().unwrap(),
877 "while processing file"
878 );
879 }
880
881 #[cfg(feature = "wire")]
882 #[test]
883 fn test_wire_invalid_cases() {
884 assert!(WireErr::<3, 29, 64>::from_wire_bytes(&[0u8; 10]).is_none());
886
887 let mut buf = vec![0u8; E4::wire_size::<64>()];
889 buf[0] = 99; assert!(WireErr::<4, 29, 64>::from_wire_bytes(&buf).is_none());
891 }
892}