Skip to main content

deep_time/
an_err.rs

1use crate::LiteStr;
2use core::fmt;
3use core::fmt::Write;
4use core::panic::Location;
5
6/// Iterator over the error trace levels of an [`AnErr`].
7///
8/// Yields `(kind, location, reason)` tuples **from most recent context to oldest**
9/// (reverse chronological order). Only valid levels are returned.
10///
11/// The `reason` field is `Some` if a non-empty reason was supplied for that level,
12/// otherwise `None`.
13#[derive(Debug, Clone)]
14pub struct TraceIter<'a, K, const DEPTH: usize, const REASON_LEN: usize>
15where
16    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
17{
18    error: &'a AnErr<K, DEPTH, REASON_LEN>,
19    pos: usize,
20}
21
22impl<'a, K, const DEPTH: usize, const REASON_LEN: usize> Iterator
23    for TraceIter<'a, K, DEPTH, REASON_LEN>
24where
25    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
26{
27    type Item = (
28        K,
29        &'static Location<'static>,
30        Option<&'a LiteStr<REASON_LEN>>,
31    );
32
33    fn next(&mut self) -> Option<Self::Item> {
34        if self.pos >= self.error.len as usize {
35            return None;
36        }
37
38        let idx = (self.error.len as usize) - 1 - self.pos;
39        let kind = self.error.kinds[idx]?;
40        let loc = self.error.locations[idx]?;
41        let reason = self.error.reasons[idx].as_ref();
42
43        self.pos += 1;
44        Some((kind, loc, reason))
45    }
46
47    fn size_hint(&self) -> (usize, Option<usize>) {
48        let remaining = (self.error.len as usize).saturating_sub(self.pos);
49        (remaining, Some(remaining))
50    }
51}
52
53impl<'a, K, const DEPTH: usize, const REASON_LEN: usize> ExactSizeIterator
54    for TraceIter<'a, K, DEPTH, REASON_LEN>
55where
56    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
57{
58}
59
60/// A compact, `Copy`, zero-allocation error type that records a parallel stack
61/// of error kinds, source locations, and per-level human-readable reasons.
62///
63/// `AnErr` stores up to `DEPTH` levels of error context. Each level contains:
64/// - an error kind of type `K`,
65/// - the source location where the level was created,
66/// - an optional reason specific to that level in the form of [`LiteStr`].
67///
68/// The kind enum provides the error category while a per-level reason
69/// carries extra info (e.g. a bad value, file path, token, etc.).
70///
71/// ## Accessing the stack of errors
72///
73/// In addition to the top-level convenience methods (`kind()`, `location()`, `reason()`),
74/// you can access any level directly or iterate the entire trace.
75///
76/// ### Direct access
77///
78/// ```rust
79/// use deep_time::{AnErr, an_err};
80///
81/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
82/// pub enum MyKind {
83///     Parse,
84///     Io,
85/// }
86///
87/// pub type MyError = AnErr<MyKind, 3, 29>;
88///
89/// let inner: MyError = an_err!(MyKind::Parse, "bad data");
90/// let err: MyError = an_err!(MyKind::Io, "while reading file" => inner);
91///
92/// let top_kind     = err.kind(); // most recent
93/// let top_loc      = err.location();
94/// let top_reason   = err.reason();
95///
96/// let root_kind    = err.root_kind(); // original error
97/// let root_loc     = err.root_location();
98/// let root_reason  = err.root_reason();
99///
100/// if let Some((kind, loc, reason)) = err.get(1) {
101///     // second level (index 0 = top, index 1 = next, ...)
102///     let _ = (kind, loc, reason);
103/// }
104/// ```
105///
106/// ### Iterating with `trace()`
107///
108/// ```rust
109/// # #[cfg(feature = "std")]
110/// # {
111/// use deep_time::{AnErr, an_err};
112///
113/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
114/// pub enum MyKind {
115///     Parse,
116///     Io,
117/// }
118///
119/// pub type MyError = AnErr<MyKind, 3, 29>;
120///
121/// let inner: MyError = an_err!(MyKind::Parse, "bad data");
122/// let err: MyError = an_err!(MyKind::Io, "while reading file" => inner);
123///
124/// for (kind, location, reason) in err.trace() {
125///     println!("{:?} @ {}:{}", kind, location.file(), location.line());
126///
127///     if let Some(r) = reason {
128///         println!("    reason: {}", r);
129///     }
130/// }
131/// # }
132/// ```
133///
134/// - Iteration order is **most recent → oldest** (same order as `Display`).
135/// - The iterator implements `ExactSizeIterator`, so you can call `.len()`, use it in `for` loops, etc.
136/// - No allocation — it just borrows the `AnErr`.
137///
138/// ## Making and using your own Error type using AnErr
139///
140/// ```rust
141/// use deep_time::{AnErr, an_err};
142///
143/// // Your error variants
144/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
145/// pub enum MyKind {
146///     Parse,
147///     Io,
148///     Validation,
149/// }
150///
151/// // Your error type, wrapper around AnErr
152/// pub type MyError = AnErr<MyKind, 4, 64>;
153///
154/// // A function that returns your new error type
155/// fn parse() -> Result<(), MyError> {
156///     // using the an_err! macro with a variant, and a reason
157///     Err(an_err!(MyKind::Parse, "unexpected token at byte {}", 42))
158/// }
159///
160///
161/// fn load(path: &str) -> Result<(), MyError> {
162///     let inner = parse()
163///         // adding another error to the stack of errors using
164///         // an_err! macro and the `=>` syntax
165///         .map_err(|e| an_err!(MyKind::Io, "while loading config from {}", path => e))?;
166///     Ok(())
167/// }
168/// ```
169///
170/// All constructors and the `context` method capture the call site via `#[track_caller]`.
171///
172/// ## Type Parameters
173///
174/// - `K`: Error kind type. Must implement `Copy + Clone + Debug + PartialEq + Eq`.
175/// - `DEPTH`: Maximum number of context levels (default `3`). Additional context
176///   beyond this limit is silently discarded.
177/// - `REASON_LEN`: Maximum length of each individual reason in bytes
178///   (default `29`). Longer reasons are silently truncated.
179///
180/// ## Display
181///
182/// The `Display` implementation produces output of the following form:
183///
184/// ```text
185/// --
186/// • Trace (2 levels):
187///    1. Io    @ src/io.rs:42:10    while loading config from /etc/foo
188///    2. Parse @ src/parser.rs:17:5  unexpected token at byte 42
189/// ```
190///
191/// Each trace level shows its own reason (if present) immediately after the location.
192///
193/// ## Invariants
194///
195/// Maintained by all constructors and `context`:
196///
197/// - `len` is always in `1..=DEPTH`.
198/// - For every `i` in `0..len`, `kinds[i]` and `locations[i]` are `Some`.
199/// - `reasons[i]` is `Some` only if a non-empty reason was supplied for that level.
200#[derive(Clone, Copy, PartialEq, Eq)]
201#[must_use = "this error should be handled or converted to a different type e.g `pub type DtErr = AnErr<MyError, 2, 49>;`"]
202pub struct AnErr<K, const DEPTH: usize = 3, const REASON_LEN: usize = 29>
203where
204    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
205{
206    /// Per-level reasons. Only the first `len` entries are valid.
207    /// `None` means no reason (or an empty reason) was provided for that level.
208    pub reasons: [Option<LiteStr<REASON_LEN>>; DEPTH],
209
210    /// Parallel stack of source locations.
211    /// Only the first `len` entries are valid.
212    pub locations: [Option<&'static Location<'static>>; DEPTH],
213
214    /// Parallel stack of error kinds (one per call-stack level).
215    /// Only the first `len` entries are valid.
216    pub kinds: [Option<K>; DEPTH],
217
218    /// Current depth of the error trace (1 = original error).
219    pub len: u8,
220}
221
222impl<K, const DEPTH: usize, const REASON_LEN: usize> AnErr<K, DEPTH, REASON_LEN>
223where
224    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
225{
226    /// Creates a new error with the given kind and no reason.
227    #[inline]
228    #[track_caller]
229    pub fn new(kind: K) -> Self {
230        let mut kinds = [None; DEPTH];
231        let mut locs = [None; DEPTH];
232        let reasons = [None; DEPTH];
233
234        kinds[0] = Some(kind);
235        locs[0] = Some(Location::caller());
236
237        Self {
238            kinds,
239            locations: locs,
240            reasons,
241            len: 1,
242        }
243    }
244
245    /// Creates a new error with the given kind and reason.
246    ///
247    /// If the reason is empty, it is stored as `None`.
248    #[inline]
249    #[track_caller]
250    pub fn with_reason(kind: K, reason: LiteStr<REASON_LEN>) -> Self {
251        let mut kinds = [None; DEPTH];
252        let mut locs = [None; DEPTH];
253        let mut reasons = [None; DEPTH];
254
255        kinds[0] = Some(kind);
256        locs[0] = Some(Location::caller());
257        reasons[0] = if reason.as_bytes().is_empty() {
258            None
259        } else {
260            Some(reason)
261        };
262
263        Self {
264            kinds,
265            locations: locs,
266            reasons,
267            len: 1,
268        }
269    }
270
271    /// Creates a new error with the given kind and a formatted reason.
272    ///
273    /// The formatted string is truncated if it exceeds `REASON_LEN` bytes.
274    #[inline]
275    #[track_caller]
276    pub fn with_fmt(kind: K, args: core::fmt::Arguments<'_>) -> Self {
277        let mut kinds = [None; DEPTH];
278        let mut locs = [None; DEPTH];
279        let mut reasons = [None; DEPTH];
280
281        kinds[0] = Some(kind);
282        locs[0] = Some(Location::caller());
283        let mut reason = LiteStr::<REASON_LEN>::default();
284        let _ = write!(&mut reason, "{}", args);
285        reasons[0] = if reason.as_bytes().is_empty() {
286            None
287        } else {
288            Some(reason)
289        };
290
291        Self {
292            kinds,
293            locations: locs,
294            reasons,
295            len: 1,
296        }
297    }
298
299    /// Returns the current depth of the error trace.
300    #[inline]
301    pub fn depth(&self) -> u8 {
302        self.len
303    }
304
305    /// Returns the most recent error kind (the top of the trace).
306    #[inline]
307    pub fn kind(&self) -> Option<K> {
308        if self.len == 0 {
309            None
310        } else {
311            let idx = (self.len as usize) - 1;
312            self.kinds[idx]
313        }
314    }
315
316    /// Appends a new context level and optional reason to this error.
317    ///
318    /// If `new_reason` is empty, no reason is stored for the new level.
319    /// If the maximum depth is already reached, the call is a no-op.
320    #[inline]
321    #[track_caller]
322    pub fn context(&mut self, kind: K, new_reason: LiteStr<REASON_LEN>) {
323        let idx = self.len as usize;
324        if idx < DEPTH {
325            self.reasons[idx] = if new_reason.as_bytes().is_empty() {
326                None
327            } else {
328                Some(new_reason)
329            };
330            self.push(kind, Location::caller());
331        }
332    }
333
334    /// Appends a new context level with a formatted reason.
335    ///
336    /// Used internally by the `an_err!` macro. The formatted string is
337    /// truncated if it exceeds `REASON_LEN` bytes.
338    #[inline]
339    #[track_caller]
340    pub fn context_fmt(&mut self, kind: K, args: core::fmt::Arguments<'_>) {
341        let idx = self.len as usize;
342        if idx < DEPTH {
343            let mut reason = LiteStr::<REASON_LEN>::default();
344            let _ = write!(&mut reason, "{}", args);
345
346            self.reasons[idx] = if reason.as_bytes().is_empty() {
347                None
348            } else {
349                Some(reason)
350            };
351            self.push(kind, Location::caller());
352        }
353    }
354
355    /// Returns an iterator over the error trace, from most recent context
356    /// down to the original error.
357    ///
358    /// Each item is `(kind, location, reason)`. The iterator borrows `self`
359    /// with zero copying.
360    pub fn trace(&self) -> TraceIter<'_, K, DEPTH, REASON_LEN> {
361        TraceIter {
362            error: self,
363            pos: 0,
364        }
365    }
366
367    #[inline]
368    fn push(&mut self, kind: K, loc: &'static Location<'static>) {
369        if (self.len as usize) < DEPTH {
370            let idx = self.len as usize;
371            self.kinds[idx] = Some(kind);
372            self.locations[idx] = Some(loc);
373            self.len += 1;
374        }
375    }
376
377    /// Returns the data for a specific level in the error trace.
378    ///
379    /// `index == 0` is the **most recent** context (top of the stack / newest `context!`).
380    /// `index == self.depth() - 1` is the **root** (original) error.
381    ///
382    /// Returns `None` if `index >= self.depth()`.
383    #[inline]
384    pub fn get(
385        &self,
386        index: usize,
387    ) -> Option<(K, &'static Location<'static>, Option<&LiteStr<REASON_LEN>>)> {
388        let depth = self.len as usize;
389        if index >= depth {
390            return None;
391        }
392        let arr_idx = depth - 1 - index; // 0 in array = root, so we reverse
393        Some((
394            self.kinds[arr_idx]?,
395            self.locations[arr_idx]?,
396            self.reasons[arr_idx].as_ref(),
397        ))
398    }
399
400    /// Returns the source location where the most recent error/context was created.
401    #[inline]
402    pub fn location(&self) -> Option<&'static Location<'static>> {
403        self.get(0).map(|(_, loc, _)| loc)
404    }
405
406    /// Returns the reason (if any) attached to the most recent error/context.
407    #[inline]
408    pub fn reason(&self) -> Option<&LiteStr<REASON_LEN>> {
409        self.get(0).and_then(|(_, _, r)| r)
410    }
411
412    /// Returns the original (root) error kind.
413    #[inline]
414    pub fn root_kind(&self) -> Option<K> {
415        (self.len > 0).then(|| self.kinds[0]).flatten()
416    }
417
418    /// Returns the source location of the original (root) error.
419    #[inline]
420    pub fn root_location(&self) -> Option<&'static Location<'static>> {
421        (self.len > 0).then(|| self.locations[0]).flatten()
422    }
423
424    /// Returns the reason (if any) attached to the root error.
425    #[inline]
426    pub fn root_reason(&self) -> Option<&LiteStr<REASON_LEN>> {
427        (self.len > 0).then(|| self.reasons[0].as_ref()).flatten()
428    }
429}
430
431impl<K, const DEPTH: usize, const REASON_LEN: usize> From<K> for AnErr<K, DEPTH, REASON_LEN>
432where
433    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
434{
435    /// Converts a kind into a new [`AnErr`] with no reason.
436    #[inline]
437    #[track_caller]
438    fn from(kind: K) -> Self {
439        Self::new(kind)
440    }
441}
442
443impl<K, const DEPTH: usize, const REASON_LEN: usize> core::fmt::Display
444    for AnErr<K, DEPTH, REASON_LEN>
445where
446    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
447{
448    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
449        writeln!(f)?;
450        writeln!(f, "--")?;
451        writeln!(f, "Error:")?;
452
453        for (i, (kind, loc, reason_opt)) in self.trace().enumerate() {
454            let num = i + 1;
455
456            write!(f, "  {:>2}. {:?}", num, kind)?;
457
458            if let Some(reason) = reason_opt {
459                write!(f, ": {}", reason.as_str())?;
460            }
461
462            writeln!(f, " @ {}:{}:{}", loc.file(), loc.line(), loc.column())?;
463        }
464
465        Ok(())
466    }
467}
468
469impl<K, const DEPTH: usize, const REASON_LEN: usize> fmt::Debug for AnErr<K, DEPTH, REASON_LEN>
470where
471    K: Copy + Clone + fmt::Debug + PartialEq + Eq,
472{
473    /// Debug prints the same clean, human-readable trace as Display.
474    /// This makes `unwrap()`, `dbg!()`, and panic messages readable instead of
475    /// dumping giant byte arrays.
476    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
477        fmt::Display::fmt(self, f)
478    }
479}
480
481impl<K, const DEPTH: usize, const REASON_LEN: usize> core::error::Error
482    for AnErr<K, DEPTH, REASON_LEN>
483where
484    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
485{
486}
487
488/// Ergonomic constructor and chaining macro for [`AnErr`].
489///
490/// ## Forms
491///
492/// | Form                                              | Equivalent to                                      |
493/// |---------------------------------------------------|----------------------------------------------------|
494/// | `an_err!(Kind)`                                   | `AnErr::new(Kind)`                               |
495/// | `an_err!(Kind, "reason")`                         | `AnErr::with_fmt(Kind, ...)`                     |
496/// | `an_err!(Kind, "reason {}", arg, ...)`            | `AnErr::with_fmt(Kind, ...)`                     |
497/// | `an_err!(Kind, "reason" => inner)`                | `inner.context(Kind, ...)`                         |
498/// | `an_err!(Kind, "reason {}", arg => inner)`        | `inner.context(Kind, ...)`                         |
499///
500/// All forms capture the call site via `#[track_caller]`.
501#[macro_export]
502macro_rules! an_err {
503    // New error, no reason
504    ($kind:expr) => {
505        $crate::AnErr::new($kind)
506    };
507
508    // Chaining form (must appear before the new-error form)
509    ($kind:expr, $fmt:literal $(, $arg:expr)* => $inner:expr $(,)?) => {{
510        let mut e = $inner;
511        e.context_fmt(
512            $kind,
513            format_args!($fmt $(, $arg)*)
514        );
515        e
516    }};
517
518    // New error with reason (literal or formatted)
519    ($kind:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
520        $crate::AnErr::with_fmt($kind, format_args!($fmt $(, $arg)*))
521    };
522}
523
524#[cfg(feature = "wire")]
525impl<K, const DEPTH: usize, const REASON_LEN: usize> AnErr<K, DEPTH, REASON_LEN>
526where
527    K: Copy + Clone + core::fmt::Debug + PartialEq + Eq,
528{
529    /// Serialize this error into a fixed-size byte buffer for transmission.
530    ///
531    /// The caller must provide a buffer that is at least `Self::WIRE_SIZE::<PATH_LEN>()` bytes long.
532    /// Returns the number of bytes actually written (always the same for a given `PATH_LEN`).
533    ///
534    /// Recommended usage:
535    /// ```rust
536    /// use deep_time::{AnErr, an_err};
537    ///
538    /// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
539    /// #[repr(u8)]
540    /// pub enum MyKind {
541    ///     Parse,
542    ///     Io,
543    /// }
544    ///
545    /// let my_error: AnErr<MyKind, 3, 29> = an_err!(MyKind::Io, "example error");
546    /// let mut buf = [0u8; AnErr::<MyKind, 3, 29>::wire_size::<80>()];
547    /// let written = my_error.to_wire_bytes::<80>(|k| k as u16, &mut buf).unwrap();
548    /// let _packet = &buf[..written];
549    /// assert_eq!(written, AnErr::<MyKind, 3, 29>::wire_size::<80>());
550    /// ```
551    pub fn to_wire_bytes<const PATH_LEN: usize>(
552        &self,
553        kind_to_u16: impl Fn(K) -> u16,
554        buf: &mut [u8],
555    ) -> Result<usize, ()> {
556        let needed = Self::wire_size::<PATH_LEN>();
557        if buf.len() < needed {
558            return Err(());
559        }
560
561        let mut offset = 0;
562
563        // Header
564        buf[offset] = 1; // wire format version
565        offset += 1;
566        buf[offset] = self.len;
567        offset += 1;
568
569        for i in 0..DEPTH {
570            if i < self.len as usize {
571                // 1. Kind as u16
572                let kind_val = self.kinds[i].map_or(0, &kind_to_u16);
573                buf[offset..offset + 2].copy_from_slice(&kind_val.to_le_bytes());
574                offset += 2;
575
576                // 2. Reason
577                let defaultx = LiteStr::default();
578                let reason = self.reasons[i].as_ref().unwrap_or(&defaultx);
579                buf[offset..offset + REASON_LEN].copy_from_slice(&reason.bytes);
580                offset += REASON_LEN;
581
582                // 3. Location
583                if let Some(loc) = self.locations[i] {
584                    let file = LiteStr::<PATH_LEN>::new(loc.file());
585                    buf[offset..offset + PATH_LEN].copy_from_slice(&file.bytes);
586                    offset += PATH_LEN;
587
588                    buf[offset..offset + 4].copy_from_slice(&loc.line().to_le_bytes());
589                    offset += 4;
590                    buf[offset..offset + 4].copy_from_slice(&loc.column().to_le_bytes());
591                    offset += 4;
592                } else {
593                    offset += PATH_LEN + 8; // pad
594                }
595            } else {
596                // pad remaining levels
597                offset += 2 + REASON_LEN + PATH_LEN + 8;
598            }
599        }
600
601        Ok(needed)
602    }
603
604    /// Compile-time size of the wire representation for a given `PATH_LEN`.
605    pub const fn wire_size<const PATH_LEN: usize>() -> usize {
606        2 + DEPTH * (2 + REASON_LEN + PATH_LEN + 8)
607    }
608}
609
610/// Portable location for wire transmission.
611#[cfg(feature = "wire")]
612#[derive(Debug, Clone, Copy, PartialEq, Eq)]
613pub struct WireLocation<const N: usize> {
614    pub file: LiteStr<N>,
615    pub line: u32,
616    pub column: u32,
617}
618
619/// Fully portable, zero-allocation error for transmission/reception.
620#[cfg(feature = "wire")]
621#[derive(Debug, Clone, Copy, PartialEq, Eq)]
622pub struct WireErr<const DEPTH: usize = 3, const REASON_LEN: usize = 29, const FILE_LEN: usize = 80>
623{
624    pub len: u8,
625    pub kinds: [Option<u16>; DEPTH],
626    pub reasons: [Option<LiteStr<REASON_LEN>>; DEPTH],
627    pub locations: [Option<WireLocation<FILE_LEN>>; DEPTH],
628}
629
630#[cfg(feature = "wire")]
631impl<const DEPTH: usize, const REASON_LEN: usize, const FILE_LEN: usize>
632    WireErr<DEPTH, REASON_LEN, FILE_LEN>
633{
634    /// Fixed wire size (exactly matches `AnErr::wire_size::<FILE_LEN>()`).
635    pub const fn wire_size() -> usize {
636        const fn compute_size<const D: usize, const R: usize, const F: usize>() -> usize {
637            2 + D * (2 + R + F + 8)
638        }
639        compute_size::<DEPTH, REASON_LEN, FILE_LEN>()
640    }
641
642    /// Parse a wire buffer from `AnErr` into a `WireErr`.
643    ///
644    /// Returns `None` on any corruption, wrong size, unknown version,
645    /// or invalid `LiteStr` data.
646    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
647        if bytes.len() != Self::wire_size() {
648            return None;
649        }
650
651        let mut offset = 0;
652
653        // Version
654        let version = bytes[offset];
655        if version != 1 {
656            return None; // unknown wire format
657        }
658        offset += 1;
659
660        let len = bytes[offset];
661        if len == 0 || len as usize > DEPTH {
662            return None;
663        }
664        offset += 1;
665
666        let mut kinds = [None; DEPTH];
667        let mut reasons = [None; DEPTH];
668        let mut locations = [None; DEPTH];
669
670        for i in 0..(len as usize) {
671            // kind (u16)
672            let kind_bytes = <[u8; 2]>::try_from(&bytes[offset..offset + 2]).ok()?;
673            kinds[i] = Some(u16::from_le_bytes(kind_bytes));
674            offset += 2;
675
676            // reason
677            let reason_bytes = &bytes[offset..offset + REASON_LEN];
678            reasons[i] = Some(LiteStr::from_bytes(reason_bytes));
679            offset += REASON_LEN;
680
681            // location
682            let file_bytes = &bytes[offset..offset + FILE_LEN];
683            let file = LiteStr::from_bytes(file_bytes);
684
685            offset += FILE_LEN;
686
687            let line_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
688            let line = u32::from_le_bytes(line_bytes);
689            offset += 4;
690
691            let col_bytes = <[u8; 4]>::try_from(&bytes[offset..offset + 4]).ok()?;
692            let column = u32::from_le_bytes(col_bytes);
693            offset += 4;
694
695            locations[i] = Some(WireLocation { file, line, column });
696        }
697
698        // remaining bytes are padding (we already checked total length)
699
700        Some(WireErr {
701            len,
702            kinds,
703            reasons,
704            locations,
705        })
706    }
707}
708
709#[cfg(feature = "alloc")]
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use alloc::format;
714    use alloc::vec::Vec;
715
716    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
717    #[repr(u8)]
718    enum TestKind {
719        Root,
720        Context1,
721        Context2,
722        Parse,
723        Io,
724    }
725
726    /// Helper for creating `LiteStr` reasons (turbofish required for const generic).
727    fn r<const N: usize>(s: &str) -> LiteStr<N> {
728        LiteStr::new(s)
729    }
730
731    // Use the crate's exact *default* parameters so the an_err! macro + constructors
732    // match perfectly and inference is unambiguous.
733    type E3 = AnErr<TestKind, 3, 29>;
734
735    #[test]
736    fn test_new_from_and_basic_properties() {
737        let e1: E3 = AnErr::new(TestKind::Root);
738        let e2: E3 = TestKind::Root.into();
739
740        // NOTE: We cannot use assert_eq!(e1, e2) because #[track_caller]
741        // captures different source locations (different lines in this test).
742        // The rest of the data is identical.
743        assert_eq!(e1.depth(), e2.depth());
744        assert_eq!(e1.kind(), e2.kind());
745        assert_eq!(e1.depth(), 1);
746        assert_eq!(e1.kind(), Some(TestKind::Root));
747
748        let mut trace = e1.trace();
749        let (kind, _loc, reason) = trace.next().unwrap();
750        assert_eq!(kind, TestKind::Root);
751        assert!(reason.is_none());
752        assert!(trace.next().is_none());
753    }
754
755    #[test]
756    fn test_with_reason_and_with_fmt() {
757        // Explicit type fixes const-generic inference (DEPTH cannot be inferred from LiteStr alone)
758        let e: E3 = AnErr::with_reason(TestKind::Parse, r::<29>("bad token"));
759        assert_eq!(e.depth(), 1);
760
761        let items: Vec<_> = e.trace().collect();
762        assert_eq!(items[0].2.unwrap().as_str(), "bad token");
763
764        let e2: E3 = AnErr::with_fmt(
765            TestKind::Io,
766            format_args!("file not found: {}", "config.toml"),
767        );
768        let items2: Vec<_> = e2.trace().collect();
769        assert_eq!(items2[0].2.unwrap().as_str(), "file not found: config.toml");
770    }
771
772    #[test]
773    fn test_an_err_macro_all_forms() {
774        let e1: E3 = an_err!(TestKind::Root);
775        assert_eq!(e1.kind(), Some(TestKind::Root));
776
777        let e2: E3 = an_err!(TestKind::Parse, "unexpected {}", "EOF");
778        assert_eq!(
779            e2.trace().next().unwrap().2.unwrap().as_str(),
780            "unexpected EOF"
781        );
782
783        // Chaining form
784        let inner: E3 = an_err!(TestKind::Parse, "bad data");
785        let outer: E3 = an_err!(TestKind::Io, "while reading file" => inner);
786
787        assert_eq!(outer.depth(), 2);
788        let mut t = outer.trace();
789        let (k1, _, r1) = t.next().unwrap();
790        assert_eq!(k1, TestKind::Io);
791        assert_eq!(r1.unwrap().as_str(), "while reading file");
792
793        let (k2, _, r2) = t.next().unwrap();
794        assert_eq!(k2, TestKind::Parse);
795        assert_eq!(r2.unwrap().as_str(), "bad data");
796    }
797
798    #[test]
799    fn test_context_and_context_fmt() {
800        let mut e: E3 = an_err!(TestKind::Root, "initial");
801        e.context(TestKind::Context1, r::<29>("level 1"));
802        e.context_fmt(TestKind::Context2, format_args!("level {}", 2));
803
804        assert_eq!(e.depth(), 3);
805
806        let trace: Vec<_> = e.trace().collect();
807        // Most recent first
808        assert_eq!(trace[0].0, TestKind::Context2);
809        assert_eq!(trace[1].0, TestKind::Context1);
810        assert_eq!(trace[2].0, TestKind::Root);
811
812        assert_eq!(trace[0].2.unwrap().as_str(), "level 2");
813        assert_eq!(trace[1].2.unwrap().as_str(), "level 1");
814        assert_eq!(trace[2].2.unwrap().as_str(), "initial");
815    }
816
817    #[test]
818    fn test_max_depth_is_no_op() {
819        let mut e: E3 = an_err!(TestKind::Root);
820        for i in 0..10 {
821            e.context(TestKind::Context1, r::<29>(&format!("extra {i}")));
822        }
823        assert_eq!(e.depth(), 3); // DEPTH limit reached, further calls ignored
824
825        let trace: Vec<_> = e.trace().collect();
826        assert_eq!(trace.len(), 3);
827        assert_eq!(trace[0].0, TestKind::Context1); // last successful context
828    }
829
830    #[test]
831    fn test_empty_reason_becomes_none() {
832        let e: E3 = an_err!(TestKind::Parse, "");
833        let (_, _, reason) = e.trace().next().unwrap();
834        assert!(reason.is_none());
835
836        let mut e2: E3 = an_err!(TestKind::Root);
837        e2.context(TestKind::Io, r::<29>("")); // empty literal -> None
838        let items: Vec<_> = e2.trace().collect();
839        assert!(items[0].2.is_none());
840    }
841
842    #[test]
843    fn test_trace_iter_order_exact_size_and_size_hint() {
844        let e: E3 = an_err!(TestKind::Root, "a" => an_err!(TestKind::Io, "b" => an_err!(TestKind::Parse, "c")));
845
846        let trace = e.trace();
847        assert_eq!(trace.len(), 3); // ExactSizeIterator
848        assert_eq!(trace.size_hint(), (3, Some(3)));
849
850        let collected: Vec<_> = trace.collect();
851        assert_eq!(collected.len(), 3);
852        assert_eq!(collected[0].0, TestKind::Root); // most recent
853        assert_eq!(collected[1].0, TestKind::Io);
854        assert_eq!(collected[2].0, TestKind::Parse); // original
855    }
856
857    #[test]
858    fn test_kind_returns_most_recent() {
859        let mut e: E3 = an_err!(TestKind::Parse);
860        e.context(TestKind::Context1, r::<29>("ctx1"));
861        e.context(TestKind::Context2, r::<29>("ctx2"));
862
863        assert_eq!(e.kind(), Some(TestKind::Context2)); // top of the trace
864    }
865
866    #[test]
867    fn test_display() {
868        let inner: E3 = an_err!(TestKind::Parse, "bad syntax");
869        let e: E3 = an_err!(TestKind::Io, "while loading config" => inner);
870
871        let display = format!("{}", e);
872        assert!(display.contains("--"));
873        assert!(display.contains("Error:"));
874        assert!(display.contains("Io"));
875        assert!(display.contains("while loading config"));
876        assert!(display.contains("Parse"));
877        assert!(display.contains("bad syntax"));
878    }
879
880    #[cfg(feature = "wire")]
881    type E4 = AnErr<TestKind, 4, 29>;
882    #[cfg(feature = "wire")]
883    use alloc::vec;
884
885    #[cfg(feature = "wire")]
886    #[test]
887    fn test_wire_roundtrip() {
888        let inner: E4 = an_err!(TestKind::Parse, "unexpected char");
889        let e: E4 = an_err!(TestKind::Io, "while processing file" => inner);
890
891        const FILE_LEN: usize = 64;
892        let wire_size = E4::wire_size::<FILE_LEN>();
893        let mut buf = vec![0u8; wire_size];
894
895        // Fixed: turbofish required for the const generic PATH_LEN
896        let written = e.to_wire_bytes::<FILE_LEN>(|k| k as u16, &mut buf).unwrap();
897        assert_eq!(written, wire_size);
898
899        let wire_err = WireErr::<4, 29, FILE_LEN>::from_wire_bytes(&buf[..written]).unwrap();
900
901        assert_eq!(wire_err.len, 2);
902
903        // Wire stores levels oldest-first (index 0 = root)
904        assert_eq!(wire_err.kinds[0], Some(TestKind::Parse as u16));
905        assert_eq!(wire_err.kinds[1], Some(TestKind::Io as u16));
906
907        assert_eq!(
908            wire_err.reasons[0].as_ref().unwrap().as_str(),
909            "unexpected char"
910        );
911        assert_eq!(
912            wire_err.reasons[1].as_ref().unwrap().as_str(),
913            "while processing file"
914        );
915    }
916
917    #[cfg(feature = "wire")]
918    #[test]
919    fn test_wire_invalid_cases() {
920        // Wrong size
921        assert!(WireErr::<3, 29, 64>::from_wire_bytes(&[0u8; 10]).is_none());
922
923        // Bad version
924        let mut buf = vec![0u8; E4::wire_size::<64>()];
925        buf[0] = 99; // invalid version
926        assert!(WireErr::<4, 29, 64>::from_wire_bytes(&buf).is_none());
927    }
928}