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