use crate::AsciiStr;
use core::fmt;
use core::panic::Location;
#[derive(Debug, Clone)]
pub struct TraceIter<'a, K, const DEPTH: usize, const REASON_LEN: usize>
where
K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
{
error: &'a AnErr<K, DEPTH, REASON_LEN>,
pos: usize,
}
impl<'a, K, const DEPTH: usize, const REASON_LEN: usize> Iterator
for TraceIter<'a, K, DEPTH, REASON_LEN>
where
K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
{
type Item = (
K,
&'static Location<'static>,
Option<&'a AsciiStr<REASON_LEN>>,
);
fn next(&mut self) -> Option<Self::Item> {
if self.pos >= self.error.len as usize {
return None;
}
let idx = (self.error.len as usize) - 1 - self.pos;
let kind = self.error.kinds[idx]?;
let loc = self.error.locations[idx]?;
let reason = self.error.reasons[idx].as_ref();
self.pos += 1;
Some((kind, loc, reason))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = (self.error.len as usize).saturating_sub(self.pos);
(remaining, Some(remaining))
}
}
impl<'a, K, const DEPTH: usize, const REASON_LEN: usize> ExactSizeIterator
for TraceIter<'a, K, DEPTH, REASON_LEN>
where
K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
{
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[must_use = "this error should be handled or converted to a different type"]
pub struct AnErr<K, const DEPTH: usize = 3, const REASON_LEN: usize = 29>
where
K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
{
pub reasons: [Option<AsciiStr<REASON_LEN>>; DEPTH],
pub locations: [Option<&'static Location<'static>>; DEPTH],
pub kinds: [Option<K>; DEPTH],
pub len: u8,
}
impl<K, const DEPTH: usize, const REASON_LEN: usize> AnErr<K, DEPTH, REASON_LEN>
where
K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
{
#[inline]
#[track_caller]
pub fn new(kind: K) -> Self {
let mut kinds = [None; DEPTH];
let mut locs = [None; DEPTH];
let reasons = [None; DEPTH];
kinds[0] = Some(kind);
locs[0] = Some(Location::caller());
Self {
kinds,
locations: locs,
reasons,
len: 1,
}
}
#[inline]
#[track_caller]
pub fn with_reason(kind: K, reason: AsciiStr<REASON_LEN>) -> Self {
let mut kinds = [None; DEPTH];
let mut locs = [None; DEPTH];
let mut reasons = [None; DEPTH];
kinds[0] = Some(kind);
locs[0] = Some(Location::caller());
reasons[0] = if reason.is_empty() {
None
} else {
Some(reason)
};
Self {
kinds,
locations: locs,
reasons,
len: 1,
}
}
#[inline]
#[track_caller]
pub fn with_fmt(kind: K, args: core::fmt::Arguments<'_>) -> Self {
let mut kinds = [None; DEPTH];
let mut locs = [None; DEPTH];
let mut reasons = [None; DEPTH];
kinds[0] = Some(kind);
locs[0] = Some(Location::caller());
let reason = AsciiStr::from_fmt(args);
reasons[0] = if reason.is_empty() {
None
} else {
Some(reason)
};
Self {
kinds,
locations: locs,
reasons,
len: 1,
}
}
#[inline]
pub fn depth(&self) -> u8 {
self.len
}
#[inline]
pub fn kind(&self) -> Option<K> {
if self.len == 0 {
None
} else {
let idx = (self.len as usize) - 1;
self.kinds[idx]
}
}
#[inline]
#[track_caller]
pub fn context(&mut self, kind: K, new_reason: AsciiStr<REASON_LEN>) {
let idx = self.len as usize;
if idx < DEPTH {
self.reasons[idx] = if new_reason.is_empty() {
None
} else {
Some(new_reason)
};
self.push(kind, Location::caller());
}
}
#[inline]
#[track_caller]
pub fn context_fmt(&mut self, kind: K, args: core::fmt::Arguments<'_>) {
let idx = self.len as usize;
if idx < DEPTH {
let reason = AsciiStr::from_fmt(args);
self.reasons[idx] = if reason.is_empty() {
None
} else {
Some(reason)
};
self.push(kind, Location::caller());
}
}
pub fn trace(&self) -> TraceIter<'_, K, DEPTH, REASON_LEN> {
TraceIter {
error: self,
pos: 0,
}
}
#[inline]
fn push(&mut self, kind: K, loc: &'static Location<'static>) {
if (self.len as usize) < DEPTH {
let idx = self.len as usize;
self.kinds[idx] = Some(kind);
self.locations[idx] = Some(loc);
self.len += 1;
}
}
#[cfg(feature = "wire")]
pub fn to_wire_bytes<const PATH_LEN: usize>(
&self,
kind_to_u16: impl Fn(K) -> u16,
buf: &mut [u8],
) -> Result<usize, ()> {
let needed = Self::wire_size::<PATH_LEN>();
if buf.len() < needed {
return Err(());
}
let mut offset = 0;
buf[offset] = 1; offset += 1;
buf[offset] = self.len;
offset += 1;
for i in 0..DEPTH {
if i < self.len as usize {
let kind_val = self.kinds[i].map_or(0, &kind_to_u16);
buf[offset..offset + 2].copy_from_slice(&kind_val.to_le_bytes());
offset += 2;
let reason = self.reasons[i].as_ref().unwrap_or(&AsciiStr::DEFAULT);
buf[offset..offset + REASON_LEN].copy_from_slice(&reason.to_wire_bytes());
offset += REASON_LEN;
if let Some(loc) = self.locations[i] {
let file = AsciiStr::<PATH_LEN>::from_str_truncate(loc.file());
buf[offset..offset + PATH_LEN].copy_from_slice(&file.to_wire_bytes());
offset += PATH_LEN;
buf[offset..offset + 4].copy_from_slice(&loc.line().to_le_bytes());
offset += 4;
buf[offset..offset + 4].copy_from_slice(&loc.column().to_le_bytes());
offset += 4;
} else {
offset += PATH_LEN + 8; }
} else {
offset += 2 + REASON_LEN + PATH_LEN + 8;
}
}
Ok(needed)
}
#[cfg(feature = "wire")]
pub const fn wire_size<const PATH_LEN: usize>() -> usize {
2 + DEPTH * (2 + REASON_LEN + PATH_LEN + 8)
}
}
impl<K, const DEPTH: usize, const REASON_LEN: usize> From<K> for AnErr<K, DEPTH, REASON_LEN>
where
K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
{
#[inline]
#[track_caller]
fn from(kind: K) -> Self {
Self::new(kind)
}
}
impl<K, const DEPTH: usize, const REASON_LEN: usize> core::fmt::Display
for AnErr<K, DEPTH, REASON_LEN>
where
K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
writeln!(f)?;
writeln!(f, "--")?;
writeln!(f, "Error:")?;
for (i, (kind, loc, reason_opt)) in self.trace().enumerate() {
let num = i + 1;
write!(f, " {:>2}. {:?}", num, kind)?;
if let Some(reason) = reason_opt {
if let Ok(s) = reason.as_str() {
write!(f, ": {}", s)?;
} else {
write!(f, ": <invalid ascii>")?;
}
}
writeln!(f, " @ {}:{}:{}", loc.file(), loc.line(), loc.column())?;
}
Ok(())
}
}
impl<K, const DEPTH: usize, const REASON_LEN: usize> fmt::Debug for AnErr<K, DEPTH, REASON_LEN>
where
K: Copy + Clone + fmt::Debug + PartialEq + Eq,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl<K, const DEPTH: usize, const REASON_LEN: usize> core::error::Error
for AnErr<K, DEPTH, REASON_LEN>
where
K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
{
}
#[macro_export]
macro_rules! an_err {
($kind:expr) => {
$crate::AnErr::new($kind)
};
($kind:expr, $fmt:literal $(, $arg:expr)* => $inner:expr $(,)?) => {{
let mut e = $inner;
e.context_fmt(
$kind,
format_args!($fmt $(, $arg)*)
);
e
}};
($kind:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
$crate::AnErr::with_fmt($kind, format_args!($fmt $(, $arg)*))
};
}
#[cfg(feature = "wire")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WireLocation<const N: usize> {
pub file: AsciiStr<N>,
pub line: u32,
pub column: u32,
}
#[cfg(feature = "wire")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WireErr<const DEPTH: usize = 3, const REASON_LEN: usize = 29, const FILE_LEN: usize = 80>
{
pub len: u8,
pub kinds: [Option<u16>; DEPTH],
pub reasons: [Option<AsciiStr<REASON_LEN>>; DEPTH],
pub locations: [Option<WireLocation<FILE_LEN>>; DEPTH],
}
#[cfg(feature = "wire")]
impl<const DEPTH: usize, const REASON_LEN: usize, const FILE_LEN: usize>
WireErr<DEPTH, REASON_LEN, FILE_LEN>
{
pub const fn wire_size() -> usize {
const fn compute_size<const D: usize, const R: usize, const F: usize>() -> usize {
2 + D * (2 + R + F + 8)
}
compute_size::<DEPTH, REASON_LEN, FILE_LEN>()
}
pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() != Self::wire_size() {
return None;
}
let mut offset = 0;
let version = bytes[offset];
if version != 1 {
return None; }
offset += 1;
let len = bytes[offset];
if len == 0 || len as usize > DEPTH {
return None;
}
offset += 1;
let mut kinds = [None; DEPTH];
let mut reasons = [None; DEPTH];
let mut locations = [None; DEPTH];
for i in 0..(len as usize) {
let kind_bytes = <[u8; 2]>::try_from(&bytes[offset..offset + 2]).ok()?;
kinds[i] = Some(u16::from_le_bytes(kind_bytes));
offset += 2;
let reason_bytes = &bytes[offset..offset + REASON_LEN];
reasons[i] = AsciiStr::from_wire_bytes(reason_bytes);
offset += REASON_LEN;
let file_bytes = &bytes[offset..offset + FILE_LEN];
let file = AsciiStr::from_wire_bytes(file_bytes)?;
offset += FILE_LEN;
let line_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
let line = u32::from_le_bytes(line_bytes);
offset += 4;
let col_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
let column = u32::from_le_bytes(col_bytes);
offset += 4;
locations[i] = Some(WireLocation { file, line, column });
}
Some(WireErr {
len,
kinds,
reasons,
locations,
})
}
}
#[cfg(feature = "alloc")]
#[cfg(test)]
mod tests {
use super::*;
use alloc::format;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
enum TestKind {
Root,
Context1,
Context2,
Parse,
Io,
}
fn r<const N: usize>(s: &str) -> AsciiStr<N> {
AsciiStr::from_str_truncate(s)
}
type E3 = AnErr<TestKind, 3, 29>;
#[test]
fn test_new_from_and_basic_properties() {
let e1: E3 = AnErr::new(TestKind::Root);
let e2: E3 = TestKind::Root.into();
assert_eq!(e1.depth(), e2.depth());
assert_eq!(e1.kind(), e2.kind());
assert_eq!(e1.depth(), 1);
assert_eq!(e1.kind(), Some(TestKind::Root));
let mut trace = e1.trace();
let (kind, _loc, reason) = trace.next().unwrap();
assert_eq!(kind, TestKind::Root);
assert!(reason.is_none());
assert!(trace.next().is_none());
}
#[test]
fn test_with_reason_and_with_fmt() {
let e: E3 = AnErr::with_reason(TestKind::Parse, r::<29>("bad token"));
assert_eq!(e.depth(), 1);
let items: Vec<_> = e.trace().collect();
assert_eq!(items[0].2.unwrap().as_str().unwrap(), "bad token");
let e2: E3 = AnErr::with_fmt(
TestKind::Io,
format_args!("file not found: {}", "config.toml"),
);
let items2: Vec<_> = e2.trace().collect();
assert_eq!(
items2[0].2.unwrap().as_str().unwrap(),
"file not found: config.toml"
);
}
#[test]
fn test_an_err_macro_all_forms() {
let e1: E3 = an_err!(TestKind::Root);
assert_eq!(e1.kind(), Some(TestKind::Root));
let e2: E3 = an_err!(TestKind::Parse, "unexpected {}", "EOF");
assert_eq!(
e2.trace().next().unwrap().2.unwrap().as_str().unwrap(),
"unexpected EOF"
);
let inner: E3 = an_err!(TestKind::Parse, "bad data");
let outer: E3 = an_err!(TestKind::Io, "while reading file" => inner);
assert_eq!(outer.depth(), 2);
let mut t = outer.trace();
let (k1, _, r1) = t.next().unwrap();
assert_eq!(k1, TestKind::Io);
assert_eq!(r1.unwrap().as_str().unwrap(), "while reading file");
let (k2, _, r2) = t.next().unwrap();
assert_eq!(k2, TestKind::Parse);
assert_eq!(r2.unwrap().as_str().unwrap(), "bad data");
}
#[test]
fn test_context_and_context_fmt() {
let mut e: E3 = an_err!(TestKind::Root, "initial");
e.context(TestKind::Context1, r::<29>("level 1"));
e.context_fmt(TestKind::Context2, format_args!("level {}", 2));
assert_eq!(e.depth(), 3);
let trace: Vec<_> = e.trace().collect();
assert_eq!(trace[0].0, TestKind::Context2);
assert_eq!(trace[1].0, TestKind::Context1);
assert_eq!(trace[2].0, TestKind::Root);
assert_eq!(trace[0].2.unwrap().as_str().unwrap(), "level 2");
assert_eq!(trace[1].2.unwrap().as_str().unwrap(), "level 1");
assert_eq!(trace[2].2.unwrap().as_str().unwrap(), "initial");
}
#[test]
fn test_max_depth_is_no_op() {
let mut e: E3 = an_err!(TestKind::Root);
for i in 0..10 {
e.context(TestKind::Context1, r::<29>(&format!("extra {i}")));
}
assert_eq!(e.depth(), 3);
let trace: Vec<_> = e.trace().collect();
assert_eq!(trace.len(), 3);
assert_eq!(trace[0].0, TestKind::Context1); }
#[test]
fn test_empty_reason_becomes_none() {
let e: E3 = an_err!(TestKind::Parse, "");
let (_, _, reason) = e.trace().next().unwrap();
assert!(reason.is_none());
let mut e2: E3 = an_err!(TestKind::Root);
e2.context(TestKind::Io, r::<29>("")); let items: Vec<_> = e2.trace().collect();
assert!(items[0].2.is_none());
}
#[test]
fn test_trace_iter_order_exact_size_and_size_hint() {
let e: E3 = an_err!(TestKind::Root, "a" => an_err!(TestKind::Io, "b" => an_err!(TestKind::Parse, "c")));
let trace = e.trace();
assert_eq!(trace.len(), 3); assert_eq!(trace.size_hint(), (3, Some(3)));
let collected: Vec<_> = trace.collect();
assert_eq!(collected.len(), 3);
assert_eq!(collected[0].0, TestKind::Root); assert_eq!(collected[1].0, TestKind::Io);
assert_eq!(collected[2].0, TestKind::Parse); }
#[test]
fn test_kind_returns_most_recent() {
let mut e: E3 = an_err!(TestKind::Parse);
e.context(TestKind::Context1, r::<29>("ctx1"));
e.context(TestKind::Context2, r::<29>("ctx2"));
assert_eq!(e.kind(), Some(TestKind::Context2)); }
#[test]
fn test_display() {
let inner: E3 = an_err!(TestKind::Parse, "bad syntax");
let e: E3 = an_err!(TestKind::Io, "while loading config" => inner);
let display = format!("{}", e);
assert!(display.contains("--"));
assert!(display.contains("Error:"));
assert!(display.contains("Io"));
assert!(display.contains("while loading config"));
assert!(display.contains("Parse"));
assert!(display.contains("bad syntax"));
}
#[cfg(feature = "wire")]
type E4 = AnErr<TestKind, 4, 29>;
#[cfg(feature = "wire")]
use alloc::vec;
#[cfg(feature = "wire")]
#[test]
fn test_wire_roundtrip() {
let inner: E4 = an_err!(TestKind::Parse, "unexpected char");
let e: E4 = an_err!(TestKind::Io, "while processing file" => inner);
const FILE_LEN: usize = 64;
let wire_size = E4::wire_size::<FILE_LEN>();
let mut buf = vec![0u8; wire_size];
let written = e.to_wire_bytes::<FILE_LEN>(|k| k as u16, &mut buf).unwrap();
assert_eq!(written, wire_size);
let wire_err = WireErr::<4, 29, FILE_LEN>::from_wire_bytes(&buf[..written]).unwrap();
assert_eq!(wire_err.len, 2);
assert_eq!(wire_err.kinds[0], Some(TestKind::Parse as u16));
assert_eq!(wire_err.kinds[1], Some(TestKind::Io as u16));
assert_eq!(
wire_err.reasons[0].as_ref().unwrap().as_str().unwrap(),
"unexpected char"
);
assert_eq!(
wire_err.reasons[1].as_ref().unwrap().as_str().unwrap(),
"while processing file"
);
}
#[cfg(feature = "wire")]
#[test]
fn test_wire_invalid_cases() {
assert!(WireErr::<3, 29, 64>::from_wire_bytes(&[0u8; 10]).is_none());
let mut buf = vec![0u8; E4::wire_size::<64>()];
buf[0] = 99; assert!(WireErr::<4, 29, 64>::from_wire_bytes(&buf).is_none());
}
}