Skip to main content

gix_error/exn/
impls.rs

1// Copyright 2025 FastLabs Developers
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::VecDeque;
16use std::error::Error;
17use std::fmt;
18use std::marker::PhantomData;
19use std::ops::Deref;
20use std::panic::Location;
21
22use crate::concrete::chain::ErrorHandle;
23use crate::{Metadata, types::ChainedError, write_location};
24
25/// An exception type that can hold an [error tree](Exn::raise_all) and the call site.
26///
27/// While an error chain, a list, is automatically created when [raise](Exn::raise)
28/// and friends are invoked, one can also use [`Exn::raise_all`] to create an error
29/// that has multiple causes.
30///
31/// # Native error sources
32///
33/// Values reached through [`std::error::Error::source()`] remain owned by their original errors and are traversed by
34/// reference, preserving their concrete types. They aren't exception frames and therefore have no captured call site of
35/// their own.
36///
37/// In diagnostic reports, custom [`std::io::Error`] wrappers show their kind instead of repeating the payload's
38/// diagnostic. The payload is reported separately as a cause; both remain available for inspection and classification.
39///
40/// # `Exn` == `Exn<Untyped>`
41///
42/// `Exn` act's like `Box<dyn std::error::Error + Send + Sync + 'static>`, but with the capability
43/// to store a tree of errors along with their *call sites*.
44///
45/// # Visualisation
46///
47/// Linearized trees during display make a list of 3 children indistinguishable from
48/// 3 errors where each is the child of the other.
49///
50/// ## Debug
51///
52/// * locations: ✔️
53/// * error display: Display
54/// * tree mode: linearized
55///
56/// ## Debug + Alternate
57///
58/// * locations: ❌
59/// * error display: Display
60/// * tree mode: linearized
61///
62/// ## Display
63///
64/// * locations: ❌
65/// * error display: Debug
66/// * tree mode: None
67///
68/// ## Display + Alternate
69///
70/// * locations: ❌
71/// * error display: Debug
72/// * tree mode: verbatim
73pub struct Exn<E: std::error::Error + Send + Sync + 'static = Untyped> {
74    // trade one more indirection for less stack size
75    frame: Box<Frame>,
76    phantom: PhantomData<E>,
77}
78
79impl<E: Error + Send + Sync + 'static> From<E> for Exn<E> {
80    #[track_caller]
81    fn from(error: E) -> Self {
82        Exn::new(error)
83    }
84}
85
86impl<E: Error + Send + Sync + 'static> Exn<E> {
87    /// Create a new exception with the given error.
88    ///
89    /// Its [source chain](Error::source) is retained by `error` and traversed lazily for formatting, downcasting, and
90    /// conversion. Native sources are not copied into owned [`Frame`] values and keep their concrete types.
91    ///
92    /// See also [`ErrorExt::raise`](crate::ErrorExt) for a fluent way to convert an error into an `Exn` instance.
93    #[track_caller]
94    pub fn new(error: E) -> Self {
95        let frame = Frame {
96            error: Box::new(error),
97            location: Location::caller(),
98            children: Vec::new(),
99        };
100
101        Self {
102            frame: Box::new(frame),
103            phantom: PhantomData,
104        }
105    }
106
107    /// Create a new exception with the given error and children.
108    #[track_caller]
109    pub fn raise_all<T, I>(children: I, err: E) -> Self
110    where
111        T: Error + Send + Sync + 'static,
112        I: IntoIterator,
113        I::Item: Into<Exn<T>>,
114    {
115        let mut new_exn = Exn::new(err);
116        for exn in children {
117            let exn = exn.into();
118            new_exn.frame.children.push(*exn.frame);
119        }
120        new_exn
121    }
122
123    /// Raise a new exception; this will make the current exception a child of the new one.
124    #[track_caller]
125    pub fn raise<T: Error + Send + Sync + 'static>(self, err: T) -> Exn<T> {
126        let mut new_exn = Exn::new(err);
127        new_exn.frame.children.push(*self.frame);
128        new_exn
129    }
130
131    /// Use the current exception as the head of a chain, adding `err` to its children.
132    #[track_caller]
133    pub fn chain<T: Error + Send + Sync + 'static>(mut self, err: impl Into<Exn<T>>) -> Exn<E> {
134        let err = err.into();
135        self.frame.children.push(*err.frame);
136        self
137    }
138
139    /// Use the current exception the head of a chain, adding `errors` to its children.
140    #[track_caller]
141    pub fn chain_all<T, I>(mut self, errors: I) -> Exn<E>
142    where
143        T: Error + Send + Sync + 'static,
144        I: IntoIterator,
145        I::Item: Into<Exn<T>>,
146    {
147        for err in errors {
148            let err = err.into();
149            self.frame.children.push(*err.frame);
150        }
151        self
152    }
153
154    /// Drain all explicitly added child frames of this error as untyped [`Exn`].
155    ///
156    /// Native [`Error::source()`] values remain owned by their error and aren't drainable frames. This is useful if one
157    /// wants to re-organise explicitly raised errors and the error layout is well known.
158    pub fn drain_children(&mut self) -> impl Iterator<Item = Exn> + '_ {
159        self.frame.children.drain(..).map(Exn::from)
160    }
161
162    /// Erase the type of this instance and turn it into a bare `Exn`.
163    pub fn erased(self) -> Exn {
164        let untyped_frame = {
165            let Frame {
166                error,
167                location,
168                children,
169            } = *self.frame;
170            // Unfortunately, we have to double-box here.
171            // TODO: figure out tricks to make this unnecessary.
172            let error = Untyped(error);
173            Frame {
174                error: Box::new(error),
175                location,
176                children,
177            }
178        };
179        Exn {
180            frame: Box::new(untyped_frame),
181            phantom: Default::default(),
182        }
183    }
184
185    /// Return the current exception.
186    pub fn error(&self) -> &E {
187        self.frame
188            .error
189            .downcast_ref()
190            .expect("the owned frame always matches the compile-time error type")
191    }
192
193    /// Discard all error context and return the underlying error in a Box.
194    ///
195    /// This is useful to retain the allocation, as internally it's also stored in a box,
196    /// when comparing it to [`Self::into_inner()`].
197    pub fn into_box(self) -> Box<E> {
198        match self.frame.error.downcast() {
199            Ok(err) => err,
200            Err(_) => unreachable!("The type in the frame is always the type of this instance"),
201        }
202    }
203
204    /// Discard all error context and return the underlying error.
205    ///
206    /// This may be needed to obtain something that once again implements `Error`.
207    /// Note that this destroys the internal Box and moves the value back onto the stack.
208    pub fn into_inner(self) -> E {
209        *self.into_box()
210    }
211
212    /// Turn ourselves into a top-level [Error] that implements [`std::error::Error`].
213    ///
214    /// [Error]: crate::Error
215    pub fn into_error(self) -> crate::Error {
216        self.into()
217    }
218
219    /// Convert this error tree into a chain of errors, breadth first, which flattens the tree
220    /// but retains all type dynamic type information.
221    ///
222    /// This is useful for inter-op with `anyhow`.
223    pub fn into_chain(self) -> ChainedError {
224        self.into()
225    }
226
227    /// Return the underlying exception frame.
228    pub fn frame(&self) -> &Frame {
229        &self.frame
230    }
231
232    /// Iterate over all explicitly created frames in breadth-first order. The first frame is this instance, followed by
233    /// all explicitly raised children. Native [`Error::source()`] values are not frames.
234    pub fn iter(&self) -> impl Iterator<Item = &Frame> {
235        self.frame().iter_frames()
236    }
237
238    /// Lazily visit stored errors and native sources in logical breadth-first order, expanding nested [`crate::Error`] values.
239    /// [Classification-only markers](crate::ClassificationMarker) are skipped; other concrete types remain available for downcasting,
240    /// as with [`crate::Error::iter_errors()`].
241    pub fn iter_errors(&self) -> impl Iterator<Item = &(dyn Error + 'static)> + '_ {
242        self.frame.iter_errors_with_locations().map(|source| source.error())
243    }
244
245    /// Visit the non-empty [`Metadata`] dictionaries of [`crate::Message`] contexts in error traversal order.
246    /// Dictionaries remain separate. Functions returning metadata document the keys in each context.
247    ///
248    /// To match a class and values on the same message, use [`Self::classify()`] and
249    /// [`Classification::error()`](crate::types::Classification::error) instead of combining independent classification
250    /// and metadata searches.
251    pub fn metadata(&self) -> impl Iterator<Item = &Metadata> + '_ {
252        self.iter_errors()
253            .filter_map(|error| error.downcast_ref::<crate::Message>())
254            .map(|error| &error.values)
255            .filter(|values| !values.is_empty())
256    }
257
258    /// Return the error that is most likely the root cause, based on [`Frame::probable_cause()`].
259    ///
260    /// Return the stored error if there is no unique causal child. Nested [`crate::Error`] graphs participate alongside
261    /// native sources and explicit children, matching [`crate::Error::probable_cause()`] without consuming this exception.
262    pub fn probable_cause(&self) -> &(dyn Error + 'static) {
263        self.frame.probable_cause().unwrap_or_else(|| self.frame.error())
264    }
265
266    /// Find the first diagnostic error that downcasts to `T` in logical breadth-first order.
267    /// Classification-only markers are omitted, as in [`Self::iter_errors()`].
268    ///
269    /// Nested [`crate::Error`] values are inspected recursively, matching [`crate::Error::downcast_any_ref()`].
270    pub fn downcast_any_ref<T: Error + 'static>(&self) -> Option<&T> {
271        self.iter_errors().find_map(|error| error.downcast_ref())
272    }
273}
274
275impl<E> Deref for Exn<E>
276where
277    E: Error + Send + Sync + 'static,
278{
279    type Target = E;
280
281    fn deref(&self) -> &Self::Target {
282        self.error()
283    }
284}
285
286impl<E: Error + Send + Sync + 'static> fmt::Debug for Exn<E> {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        write_frame_recursive(f, self.frame(), "", ErrorMode::Display, TreeMode::Linearize)
289    }
290}
291
292impl fmt::Debug for Frame {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        write_frame_recursive(f, self, "", ErrorMode::Display, TreeMode::Linearize)
295    }
296}
297
298#[derive(Copy, Clone)]
299pub(crate) enum ErrorMode {
300    Display,
301    Debug,
302}
303
304impl ErrorMode {
305    pub(crate) fn fmt(self, error: &(dyn Error + 'static), f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        if let Some(io) = error.downcast_ref::<std::io::Error>()
307            && io.get_ref().is_some()
308        {
309            // The traversal reports the payload separately, so neither Display nor Debug may expand it here.
310            return write!(f, "I/O error ({:?})", io.kind());
311        }
312        // The outer alternate flag controls report layout, not the formatting of individual diagnostics.
313        match self {
314            ErrorMode::Display => write!(f, "{error}"),
315            ErrorMode::Debug => write!(f, "{error:?}"),
316        }
317    }
318}
319
320#[derive(Copy, Clone)]
321enum TreeMode {
322    Linearize,
323    Verbatim,
324}
325
326fn write_frame_recursive(
327    f: &mut fmt::Formatter<'_>,
328    frame: &Frame,
329    prefix: &str,
330    err_mode: ErrorMode,
331    tree_mode: TreeMode,
332) -> fmt::Result {
333    if crate::error::is_transparent_marker(frame.error()) {
334        let children = ErrorNode::Frame(frame).children();
335        if !children.is_empty() {
336            for (index, child) in children.into_iter().enumerate() {
337                if index != 0 {
338                    writeln!(f)?;
339                }
340                write_error_node_recursive(f, child, prefix, err_mode, tree_mode)?;
341            }
342            return Ok(());
343        }
344    }
345    write_error_node_recursive(f, ErrorNode::Frame(frame), prefix, err_mode, tree_mode)
346}
347
348fn write_error_node_recursive(
349    f: &mut fmt::Formatter<'_>,
350    node: ErrorNode<'_>,
351    prefix: &str,
352    err_mode: ErrorMode,
353    tree_mode: TreeMode,
354) -> fmt::Result {
355    let mut root_error = node.error();
356    while let Some(error) = root_error.downcast_ref::<crate::Error>() {
357        root_error = error.error();
358    }
359    err_mode.fmt(root_error, f)?;
360    if !f.alternate() {
361        write_location(f, node.location())?;
362    }
363
364    if let Some(err) = node.error().downcast_ref::<crate::Error>() {
365        let mut skipped_root = false;
366        for source in err
367            .iter_errors_with_locations()
368            .filter(|source| !source.error().is::<crate::Error>())
369        {
370            // Nested boundaries can have children before the innermost root in breadth-first order.
371            if !skipped_root && std::ptr::eq(source.error(), root_error) {
372                skipped_root = true;
373                continue;
374            }
375            write!(f, "\n{prefix}|\n{prefix}└─ ")?;
376            err_mode.fmt(source.error(), f)?;
377            if !f.alternate() {
378                write_location(f, source.location().unwrap_or_else(|| node.location()))?;
379            }
380        }
381    }
382
383    let children = node.children();
384    let children_len = children.len();
385
386    for (child_index, child) in children.into_iter().enumerate() {
387        write!(f, "\n{prefix}|")?;
388        write!(f, "\n{prefix}└─ ")?;
389
390        let child_child_len = if child
391            .error()
392            .downcast_ref::<crate::Error>()
393            .is_some_and(|err| err.iter_errors().filter(|source| !source.is::<crate::Error>()).count() > 1)
394        {
395            1
396        } else {
397            child.children().len()
398        };
399        let may_linearize_chain = matches!(tree_mode, TreeMode::Linearize) && children_len == 1 && child_child_len == 1;
400        if may_linearize_chain {
401            write_error_node_recursive(f, child, prefix, err_mode, tree_mode)?;
402        } else if child_index < children_len - 1 {
403            write_error_node_recursive(f, child, &format!("{prefix}|   "), err_mode, tree_mode)?;
404        } else {
405            write_error_node_recursive(f, child, &format!("{prefix}    "), err_mode, tree_mode)?;
406        }
407    }
408
409    Ok(())
410}
411
412impl<E: Error + Send + Sync + 'static> fmt::Display for Exn<E> {
413    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
414        fmt::Display::fmt(&self.frame, f)
415    }
416}
417
418impl<E: Error + Send + Sync + 'static> PartialEq<str> for Exn<E> {
419    fn eq(&self, other: &str) -> bool {
420        crate::root_error_eq(self.frame().error(), other)
421    }
422}
423
424impl<E: Error + Send + Sync + 'static> PartialEq<&str> for Exn<E> {
425    fn eq(&self, other: &&str) -> bool {
426        <Self as PartialEq<str>>::eq(self, other)
427    }
428}
429
430impl<E: Error + Send + Sync + 'static> PartialEq<String> for Exn<E> {
431    fn eq(&self, other: &String) -> bool {
432        <Self as PartialEq<str>>::eq(self, other)
433    }
434}
435
436impl fmt::Display for Frame {
437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438        if f.alternate() {
439            // Avoid printing alternate versions of the debug info, keep it in one line, also print the tree.
440            write_frame_recursive(f, self, "", ErrorMode::Debug, TreeMode::Verbatim)
441        } else {
442            if crate::error::is_transparent_marker(self.error())
443                && let Some(diagnostic) = self.iter_errors_with_locations().next()
444            {
445                return fmt::Display::fmt(diagnostic.error(), f);
446            }
447            fmt::Display::fmt(self.error(), f)
448        }
449    }
450}
451
452/// A frame in the exception tree.
453pub struct Frame {
454    /// The error that occurred at this frame.
455    error: Box<dyn Error + Send + Sync + 'static>,
456    /// The source code location where this exception frame was created.
457    location: &'static Location<'static>,
458    /// Explicitly raised child exception frames.
459    children: Vec<Frame>,
460}
461
462impl Frame {
463    /// Return the error as a reference to [`Error`].
464    ///
465    /// If the error was [erased](crate::Exn::erased), this is the original error,
466    /// so it can still be downcast to its actual type.
467    pub fn error(&self) -> &(dyn Error + Send + Sync + 'static) {
468        let mut error = &*self.error;
469        while let Some(erased) = error.downcast_ref::<Untyped>() {
470            error = &*erased.0;
471        }
472        error
473    }
474
475    /// Return the source code location where this exception frame was created.
476    pub fn location(&self) -> &'static Location<'static> {
477        self.location
478    }
479
480    /// Return explicitly raised child frames.
481    ///
482    /// Native [`Error::source()`] values are borrowed from [`Self::error()`] and traversed lazily, so they aren't owned
483    /// `Frame` children.
484    pub fn children(&self) -> &[Frame] {
485        &self.children
486    }
487}
488
489/// A borrowed node that lets one traversal visit both explicit exception frames and native [`Error::source()`] chains.
490///
491/// Explicitly raised errors are stored as [`Frame`] values, whereas native sources remain owned by their errors and
492/// must be borrowed when traversed. `Source` represents such a borrowed native error and carries forward the location
493/// of its owning frame for internal formatting without turning the source into a frame or losing its concrete type.
494#[derive(Clone, Copy)]
495pub(crate) enum ErrorNode<'a> {
496    Frame(&'a Frame),
497    Source {
498        error: &'a (dyn Error + 'static),
499        location: &'static Location<'static>,
500    },
501    /// A source from a nested boundary's already flattened iterator; its descendants are emitted separately.
502    FlatSource {
503        error: &'a (dyn Error + 'static),
504        location: &'static Location<'static>,
505    },
506}
507
508impl<'a> ErrorNode<'a> {
509    pub(crate) fn error(self) -> &'a (dyn Error + 'static) {
510        match self {
511            ErrorNode::Frame(frame) => frame.error(),
512            ErrorNode::Source { error, .. } | ErrorNode::FlatSource { error, .. } => error,
513        }
514    }
515
516    /// Return the frame location used when formatting this node.
517    ///
518    /// A frame returns its own captured location. A native source inherits the location of the frame whose error owns its
519    /// source chain, providing formatting context even though no location was captured for the source itself.
520    pub(crate) fn location(self) -> &'static Location<'static> {
521        match self {
522            ErrorNode::Frame(frame) => frame.location,
523            ErrorNode::Source { location, .. } | ErrorNode::FlatSource { location, .. } => location,
524        }
525    }
526
527    /// Return this node's diagnostic children in traversal order, promoting descendants of classification markers.
528    ///
529    /// A direct native [`Error::source()`] or I/O payload is first and inherits this node's formatting location.
530    /// For a frame, explicitly raised child frames follow it in insertion order. The compatibility `source()` of a nested [`crate::Error`] is
531    /// skipped because that wrapper retains an internal error graph which its own traversal APIs expand separately;
532    /// following the compatibility source here would expose only one path and duplicate that expansion.
533    pub(crate) fn children(self) -> Vec<ErrorNode<'a>> {
534        if matches!(self, ErrorNode::FlatSource { .. }) {
535            return Vec::new();
536        }
537        let error = self.error();
538        let location = self.location();
539        let mut children = Vec::new();
540        if let Some(nested) = error.downcast_ref::<crate::Error>() {
541            if crate::error::is_transparent_marker(error) {
542                children.extend(
543                    nested
544                        .iter_errors_with_locations()
545                        .filter(|source| !source.error().is::<crate::Error>())
546                        .map(|source| ErrorNode::FlatSource {
547                            error: source.error(),
548                            location: source.location().unwrap_or(location),
549                        }),
550                );
551            }
552        } else if let Some(error) = crate::error::native_source(error) {
553            children.push(ErrorNode::Source { error, location });
554        }
555        if let ErrorNode::Frame(frame) = self {
556            children.extend(frame.children.iter().map(ErrorNode::Frame));
557        }
558        let mut diagnostics = Vec::new();
559        for child in children {
560            if crate::error::is_transparent_marker(child.error()) {
561                diagnostics.extend(child.children());
562            } else {
563                diagnostics.push(child);
564            }
565        }
566        diagnostics
567    }
568}
569
570/// Navigation
571impl Frame {
572    /// Follow the unique causal child until reaching a leaf or a branch.
573    ///
574    /// Native [`Error::source()`] values, I/O payloads, nested [`crate::Error`] graphs, and explicitly raised frames all
575    /// participate.
576    ///
577    /// An *aggregate* is the error at a branch that groups two or more diagnostic causes. This is a role in the error
578    /// tree, not a special concrete error type. For example, [`Exn::raise_all`] can attach multiple failed operations
579    /// to a shared `"batch failed"` [`crate::Message`]. That shared message is the aggregate, so selection stops there
580    /// rather than arbitrarily choosing one operation's error:
581    ///
582    /// ```text
583    /// outer context
584    /// └─ batch failed  (aggregate, selected)
585    ///    ├─ first operation failed
586    ///    └─ second operation failed
587    /// ```
588    ///
589    /// All [`crate::ClassificationMarker`] values are ignored. Their frames, including nested boundaries,
590    /// are transparent: their real descendants count as children of the nearest non-marker parent instead.
591    /// Return `None` if selection stays at this frame, allowing callers to fall back to [`Self::error()`], even for a
592    /// classification-only root.
593    pub fn probable_cause(&self) -> Option<&(dyn Error + 'static)> {
594        self.probable_cause_inner()
595    }
596
597    /// Iterate over all explicitly created frames in breadth-first order. The first frame is this instance, followed by
598    /// all explicitly raised children. Native [`Error::source()`] values are not frames.
599    pub fn iter_frames(&self) -> impl Iterator<Item = &Frame> + '_ {
600        let mut queue = std::collections::VecDeque::new();
601        queue.push_back(self);
602        BreadthFirstFrames { queue }
603    }
604}
605
606/// Breadth-first iterator over explicitly created `Frame`s.
607pub struct BreadthFirstFrames<'a> {
608    queue: std::collections::VecDeque<&'a Frame>,
609}
610
611impl<'a> Iterator for BreadthFirstFrames<'a> {
612    type Item = &'a Frame;
613
614    fn next(&mut self) -> Option<Self::Item> {
615        let frame = self.queue.pop_front()?;
616        for child in frame.children() {
617            self.queue.push_back(child);
618        }
619        Some(frame)
620    }
621}
622
623impl<E> From<Exn<E>> for Box<Frame>
624where
625    E: Error + Send + Sync + 'static,
626{
627    fn from(err: Exn<E>) -> Self {
628        err.frame
629    }
630}
631
632impl<E> From<Exn<E>> for Box<dyn Error + Send + Sync + 'static>
633where
634    E: Error + Send + Sync + 'static,
635{
636    fn from(err: Exn<E>) -> Self {
637        Box::new(err.into_error())
638    }
639}
640
641#[cfg(feature = "anyhow")]
642impl<E> From<Exn<E>> for anyhow::Error
643where
644    E: Error + Send + Sync + 'static,
645{
646    fn from(err: Exn<E>) -> Self {
647        anyhow::Error::from(err.into_chain())
648    }
649}
650
651impl<E> From<Exn<E>> for Frame
652where
653    E: Error + Send + Sync + 'static,
654{
655    fn from(err: Exn<E>) -> Self {
656        *err.frame
657    }
658}
659
660impl From<Frame> for Exn {
661    fn from(mut frame: Frame) -> Self {
662        if !frame.error.is::<Untyped>() {
663            frame.error = Box::new(Untyped(frame.error));
664        }
665        Exn {
666            frame: Box::new(frame),
667            phantom: Default::default(),
668        }
669    }
670}
671
672/// A marker to show that type information is not available,
673/// while storing all extractable information about the erased type.
674/// It's the default type for [Exn].
675pub struct Untyped(Box<dyn Error + Send + Sync + 'static>);
676
677impl Untyped {
678    pub(crate) fn from_boxed(error: Box<dyn Error + Send + Sync + 'static>) -> Self {
679        Untyped(error)
680    }
681}
682
683impl fmt::Display for Untyped {
684    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
685        fmt::Display::fmt(&self.0, f)
686    }
687}
688
689impl fmt::Debug for Untyped {
690    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
691        fmt::Debug::fmt(&self.0, f)
692    }
693}
694
695impl Error for Untyped {
696    fn source(&self) -> Option<&(dyn Error + 'static)> {
697        self.0.source()
698    }
699}
700
701impl<E> From<Exn<E>> for ChainedError
702where
703    E: std::error::Error + Send + Sync + 'static,
704{
705    fn from(err: Exn<E>) -> Self {
706        let flattened = flatten_error_nodes(*err.frame);
707        let mut source = None;
708        for node in flattened.into_iter().rev() {
709            source = Some(Box::new(ChainedError {
710                err: node.error,
711                location: node.location,
712                logical_parent: node.logical_parent,
713                source,
714            }));
715        }
716        *source.expect("an Exn always contains its root error")
717    }
718}
719
720struct OwnedErrorNode {
721    error: ErrorHandle,
722    location: &'static Location<'static>,
723    logical_parent: Option<usize>,
724}
725
726/// Consume an exception-frame tree and flatten its errors into logical breadth-first order for [`ChainedError`].
727///
728/// Each frame's direct native [`Error::source()`] is queued before its explicitly raised child frames, and subsequent
729/// native sources continue as children of the preceding source. Every output node retains an owning [`ErrorHandle`], the
730/// frame location used for formatting, and the output index of its logical parent so the tree relationships can later be
731/// reconstructed. Native sources inherit their owning frame's location.
732///
733/// A nested [`crate::Error`] is retained as one node without following its compatibility `source()` chain. Its internal
734/// graph is expanded separately by the [`crate::Error`] traversal APIs, avoiding a partial and duplicated representation.
735fn flatten_error_nodes(root: Frame) -> Vec<OwnedErrorNode> {
736    enum Pending {
737        Frame {
738            frame: Frame,
739            logical_parent: Option<usize>,
740        },
741        Source {
742            error: ErrorHandle,
743            location: &'static Location<'static>,
744            logical_parent: usize,
745        },
746    }
747
748    let mut queue = VecDeque::from([Pending::Frame {
749        frame: root,
750        logical_parent: None,
751    }]);
752    let mut out = Vec::new();
753    while let Some(node) = queue.pop_front() {
754        let node_index = out.len();
755        match node {
756            Pending::Frame {
757                frame:
758                    Frame {
759                        error,
760                        location,
761                        children,
762                    },
763                logical_parent,
764            } => {
765                let error = ErrorHandle::new(unerase(error));
766                if !error.error().is::<crate::Error>()
767                    && let Some(source) = error.source()
768                {
769                    queue.push_back(Pending::Source {
770                        error: source,
771                        location,
772                        logical_parent: node_index,
773                    });
774                }
775                queue.extend(children.into_iter().map(|frame| Pending::Frame {
776                    frame,
777                    logical_parent: Some(node_index),
778                }));
779                out.push(OwnedErrorNode {
780                    error,
781                    location,
782                    logical_parent,
783                });
784            }
785            Pending::Source {
786                error,
787                location,
788                logical_parent,
789            } => {
790                if !error.error().is::<crate::Error>()
791                    && let Some(source) = error.source()
792                {
793                    queue.push_back(Pending::Source {
794                        error: source,
795                        location,
796                        logical_parent: node_index,
797                    });
798                }
799                out.push(OwnedErrorNode {
800                    error,
801                    location,
802                    logical_parent: Some(logical_parent),
803                });
804            }
805        }
806    }
807    out
808}
809
810/// Remove all type-erasure markers before storing an error in a [`ChainedError`].
811///
812/// [`Untyped::source()`] deliberately forwards to the wrapped error's source to keep
813/// the marker transparent. Storing the marker itself in the chain would therefore
814/// hide a wrapped leaf error from source traversal and classification. Unwrapping it
815/// here retains the original runtime type without changing those source semantics.
816fn unerase(mut error: Box<dyn Error + Send + Sync + 'static>) -> Box<dyn Error + Send + Sync + 'static> {
817    loop {
818        match error.downcast::<Untyped>() {
819            Ok(untyped) => error = untyped.0,
820            Err(typed) => return typed,
821        }
822    }
823}