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/// Reports list causes under `Caused by:`, numbering the main chain `0`, `1`, and so on.
50/// Branches number siblings locally from `0`, using two columns per level for `├─`, `└─`, and `│ ` hierarchy guides.
51/// Each branch head stays at its level, with any linear chain of causes flattened beneath it. Only forks add levels.
52/// Guides replace indentation without shifting numbers or diagnostics at a given level.
53///
54/// ## Debug
55///
56/// * locations: ✔️
57/// * error display: Display
58/// * tree mode: numbered, with chains linearized beneath their branch heads
59///
60/// ## Debug + Alternate
61///
62/// * locations: ❌
63/// * error display: Display
64/// * tree mode: numbered, with chains linearized beneath their branch heads
65///
66/// ## Display
67///
68/// * locations: ❌
69/// * error display: Display
70/// * tree mode: None
71///
72/// ## Display + Alternate
73///
74/// * locations: ❌
75/// * error display: Debug
76/// * tree mode: numbered, with chains linearized beneath their branch heads
77pub struct Exn<E: std::error::Error + Send + Sync + 'static = Untyped> {
78    // trade one more indirection for less stack size
79    frame: Box<Frame>,
80    phantom: PhantomData<E>,
81}
82
83/// Reuse an existing public error's tree only where its concrete wrapper type is no longer required.
84#[track_caller]
85#[expect(
86    clippy::unnecessary_box_returns,
87    reason = "erasure retains the existing frame allocation"
88)]
89pub(super) fn into_frame<E: Error + Send + Sync + 'static>(error: E) -> Box<Frame> {
90    // Keep chains wrapped so adding context doesn't reconstruct their existing nodes.
91    #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))]
92    {
93        crate::ErrorExt::raise(error).into_frame()
94    }
95    #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
96    {
97        Exn::new(error).frame
98    }
99}
100
101impl<E: Error + Send + Sync + 'static> From<E> for Exn<E> {
102    #[track_caller]
103    fn from(error: E) -> Self {
104        Exn::new(error)
105    }
106}
107
108impl<E: Error + Send + Sync + 'static> Exn<E> {
109    /// Create a new exception with the given error.
110    ///
111    /// Its [source chain](Error::source) is retained by `error` and traversed lazily for formatting, downcasting, and
112    /// conversion. Native sources are not copied into owned [`Frame`] values and keep their concrete types.
113    ///
114    /// See also [`ErrorExt::raise_typed`](crate::ErrorExt::raise_typed) for a fluent way to construct a typed exception.
115    #[track_caller]
116    pub fn new(error: E) -> Self {
117        let frame = Frame {
118            source: FrameSource {
119                error: Box::new(error),
120                location: Location::caller(),
121                children: Vec::new(),
122            },
123        };
124
125        Self {
126            frame: Box::new(frame),
127            phantom: PhantomData,
128        }
129    }
130
131    #[track_caller]
132    pub(super) fn with_cause(cause: impl Error + Send + Sync + 'static, error: E) -> Self {
133        let cause = into_frame(cause);
134        let mut exn = Exn::new(error);
135        exn.frame.source.children.push(*cause);
136        exn
137    }
138
139    /// Create a new exception with the given error and children.
140    #[track_caller]
141    pub fn raise_all<T, I>(children: I, err: E) -> Self
142    where
143        T: Error + Send + Sync + 'static,
144        I: IntoIterator,
145        I::Item: Into<Exn<T>>,
146    {
147        let mut new_exn = Exn::new(err);
148        for exn in children {
149            let exn = exn.into();
150            new_exn.frame.source.children.push(*exn.frame);
151        }
152        new_exn
153    }
154
155    /// Raise a new exception; this will make the current exception a child of the new one.
156    #[track_caller]
157    pub fn raise<T: Error + Send + Sync + 'static>(self, err: T) -> Exn<T> {
158        let mut new_exn = Exn::new(err);
159        new_exn.frame.source.children.push(*self.frame);
160        new_exn
161    }
162
163    /// Use the current exception as the head of a chain, adding `err` to its children.
164    #[track_caller]
165    pub fn chain<T: Error + Send + Sync + 'static>(mut self, err: impl Into<Exn<T>>) -> Exn<E> {
166        let err = err.into();
167        self.frame.source.children.push(*err.frame);
168        self
169    }
170
171    /// Use the current exception the head of a chain, adding `errors` to its children.
172    #[track_caller]
173    pub fn chain_all<T, I>(mut self, errors: I) -> Exn<E>
174    where
175        T: Error + Send + Sync + 'static,
176        I: IntoIterator,
177        I::Item: Into<Exn<T>>,
178    {
179        for err in errors {
180            let err = err.into();
181            self.frame.source.children.push(*err.frame);
182        }
183        self
184    }
185
186    /// Drain all explicitly added child frames of this error as untyped [`Exn`].
187    ///
188    /// Native [`Error::source()`] values remain owned by their error and aren't drainable frames. This is useful if one
189    /// wants to re-organise explicitly raised errors and the error layout is well known.
190    pub fn drain_children(&mut self) -> impl Iterator<Item = Exn> + '_ {
191        self.frame.source.children.drain(..).map(Exn::from)
192    }
193
194    /// Erase the type of this instance and turn it into a bare `Exn`.
195    /// Reuse the frame allocation; already erased exceptions require no new allocation.
196    pub fn erased(self) -> Exn {
197        Exn::from_boxed_frame(self.frame)
198    }
199
200    /// Return the current exception.
201    pub fn error(&self) -> &E {
202        self.frame
203            .source
204            .error
205            .downcast_ref()
206            .expect("the owned frame always matches the compile-time error type")
207    }
208
209    /// Discard all error context and return the underlying error in a Box.
210    ///
211    /// This is useful to retain the allocation, as internally it's also stored in a box,
212    /// when comparing it to [`Self::into_inner()`].
213    pub fn into_box(self) -> Box<E> {
214        match self.frame.source.error.downcast() {
215            Ok(err) => err,
216            Err(_) => unreachable!("The type in the frame is always the type of this instance"),
217        }
218    }
219
220    /// Discard all error context and return the underlying error.
221    ///
222    /// This may be needed to obtain something that once again implements `Error`.
223    /// Note that this destroys the internal Box and moves the value back onto the stack.
224    pub fn into_inner(self) -> E {
225        *self.into_box()
226    }
227
228    /// Turn ourselves into a top-level [Error] that implements [`std::error::Error`].
229    ///
230    /// [Error]: crate::Error
231    pub fn into_error(self) -> crate::Error {
232        self.into()
233    }
234
235    /// Convert this error tree into a chain of errors, breadth first, which flattens the tree
236    /// but retains all type dynamic type information.
237    ///
238    /// This is useful for inter-op with error-chain consumers.
239    pub fn into_chain(self) -> ChainedError {
240        self.into()
241    }
242
243    /// Return the underlying exception frame.
244    pub fn frame(&self) -> &Frame {
245        &self.frame
246    }
247
248    /// Iterate over all explicitly created frames in breadth-first order. The first frame is this instance, followed by
249    /// all explicitly raised children. Native [`Error::source()`] values are not frames.
250    pub fn iter(&self) -> impl Iterator<Item = &Frame> {
251        self.frame().iter_frames()
252    }
253
254    /// Lazily visit stored errors and native sources in logical breadth-first order, expanding nested [`crate::Error`] values.
255    /// [Classification-only markers](crate::ClassificationMarker) are skipped; other concrete types remain available for downcasting,
256    /// as with [`crate::Error::iter_errors()`].
257    pub fn iter_errors(&self) -> impl Iterator<Item = &(dyn Error + 'static)> + '_ {
258        self.frame.iter_errors_with_locations().map(|source| source.error())
259    }
260
261    /// Visit the non-empty [`Metadata`] dictionaries of [`crate::Message`] contexts in error traversal order.
262    /// Dictionaries remain separate; use [`Self::metadata_merged()`] to combine them.
263    /// Functions returning metadata document the keys in each context.
264    ///
265    /// To match a class and values on the same message, use [`Self::classify()`] and
266    /// [`Classification::error()`](crate::types::Classification::error) instead of combining independent classification
267    /// and metadata searches.
268    pub fn metadata(&self) -> impl Iterator<Item = &Metadata> + '_ {
269        self.iter_errors()
270            .filter_map(|error| error.downcast_ref::<crate::Message>())
271            .map(|error| &error.values)
272            .filter(|values| !values.is_empty())
273    }
274
275    /// Clone all [`Self::metadata()`] dictionaries into one owned dictionary.
276    ///
277    /// Later values in logical breadth-first error traversal order replace earlier values with the same key.
278    /// Thus, more specific causes override their enclosing contexts. For independent causes, the later-visited
279    /// cause wins; merging does not retain which context supplied a value. Use [`Self::metadata()`] instead when
280    /// that distinction matters. An error without metadata yields an empty dictionary.
281    pub fn metadata_merged(&self) -> Metadata {
282        let mut merged = Metadata::new();
283        for values in self.metadata() {
284            merged.extend(values.iter().map(|(key, value)| (key.clone(), value.clone())));
285        }
286        merged
287    }
288
289    /// Return the error that is most likely the root cause, based on [`Frame::probable_cause()`].
290    ///
291    /// Return the stored error if there is no unique causal child. Nested [`crate::Error`] graphs participate alongside
292    /// native sources and explicit children, matching [`crate::Error::probable_cause()`] without consuming this exception.
293    pub fn probable_cause(&self) -> &(dyn Error + 'static) {
294        self.frame.probable_cause().unwrap_or_else(|| self.frame.error())
295    }
296
297    /// Find the first diagnostic error that downcasts to `T` in logical breadth-first order.
298    /// Classification-only markers are omitted, as in [`Self::iter_errors()`].
299    ///
300    /// Nested [`crate::Error`] values are inspected recursively, matching [`crate::Error::downcast_any_ref()`].
301    pub fn downcast_any_ref<T: Error + 'static>(&self) -> Option<&T> {
302        self.iter_errors().find_map(|error| error.downcast_ref())
303    }
304}
305
306impl<E> Deref for Exn<E>
307where
308    E: Error + Send + Sync + 'static,
309{
310    type Target = E;
311
312    fn deref(&self) -> &Self::Target {
313        self.error()
314    }
315}
316
317impl<E: Error + Send + Sync + 'static> fmt::Debug for Exn<E> {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        write_frame_recursive(f, self.frame(), ErrorMode::Display)
320    }
321}
322
323impl fmt::Debug for Frame {
324    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325        write_frame_recursive(f, self, ErrorMode::Display)
326    }
327}
328
329#[derive(Copy, Clone)]
330pub(crate) enum ErrorMode {
331    Display,
332    Debug,
333}
334
335impl ErrorMode {
336    pub(crate) fn fmt(self, error: &(dyn Error + 'static), f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        if let Some(io) = error.downcast_ref::<std::io::Error>()
338            && io.get_ref().is_some()
339        {
340            // The traversal reports the payload separately, so neither Display nor Debug may expand it here.
341            return write!(f, "I/O error ({:?})", io.kind());
342        }
343        // The outer alternate flag controls report layout, not the formatting of individual diagnostics.
344        match self {
345            ErrorMode::Display => write!(f, "{error}"),
346            ErrorMode::Debug => write!(f, "{error:?}"),
347        }
348    }
349}
350
351fn write_frame_recursive(f: &mut fmt::Formatter<'_>, frame: &Frame, err_mode: ErrorMode) -> fmt::Result {
352    if crate::error::is_transparent_marker(frame.error()) {
353        let children = ErrorNode::Frame(frame).children();
354        if !children.is_empty() {
355            for (index, child) in children.into_iter().enumerate() {
356                if index != 0 {
357                    writeln!(f)?;
358                }
359                write_error_node_recursive(f, child, &mut Vec::new(), &mut 0, err_mode, true)?;
360            }
361            return Ok(());
362        }
363    }
364    write_error_node_recursive(f, ErrorNode::Frame(frame), &mut Vec::new(), &mut 0, err_mode, true)
365}
366
367fn write_error_node_recursive(
368    f: &mut fmt::Formatter<'_>,
369    node: ErrorNode<'_>,
370    siblings_follow: &mut Vec<bool>,
371    number: &mut usize,
372    err_mode: ErrorMode,
373    linearize: bool,
374) -> fmt::Result {
375    let children = node.children();
376    // Nested boundaries already expose a flattened sequence rather than a new fork.
377    let is_chain = children.len() == 1
378        || (!children.is_empty()
379            && children
380                .iter()
381                .all(|child| matches!(child, ErrorNode::FlatSource { .. })));
382    let continues_chain = linearize && is_chain;
383    if !siblings_follow.is_empty() {
384        f.write_str("\n    ")?;
385        // The main chain has no connector; only nested levels use its existing indentation columns.
386        if let Some((has_sibling, ancestors)) = siblings_follow[1..].split_last() {
387            for has_sibling in ancestors {
388                f.write_str(if *has_sibling { "│ " } else { "  " })?;
389            }
390            f.write_str(if *has_sibling || continues_chain {
391                "├─"
392            } else {
393                "└─"
394            })?;
395        }
396        write!(f, "{number}: ")?;
397    }
398    err_mode.fmt(node.root_error(), f)?;
399    if !f.alternate() {
400        write_location(f, node.location())?;
401    }
402
403    if children.is_empty() {
404        return Ok(());
405    }
406    if siblings_follow.is_empty() {
407        f.write_str("\n\nCaused by:")?;
408    }
409    if continues_chain {
410        let has_sibling = siblings_follow.last().copied().unwrap_or(false);
411        let child_count = children.len();
412        for (index, child) in children.into_iter().enumerate() {
413            if siblings_follow.is_empty() {
414                siblings_follow.push(false);
415            } else {
416                *number += 1;
417            }
418            if let Some(last) = siblings_follow.last_mut() {
419                *last = has_sibling || index + 1 < child_count;
420            }
421            write_error_node_recursive(f, child, siblings_follow, number, err_mode, true)?;
422        }
423        if let Some(last) = siblings_follow.last_mut() {
424            *last = has_sibling;
425        }
426        return Ok(());
427    }
428    let child_count = children.len();
429    for (mut number, child) in children.into_iter().enumerate() {
430        siblings_follow.push(number + 1 < child_count);
431        write_error_node_recursive(f, child, siblings_follow, &mut number, err_mode, is_chain)?;
432        siblings_follow.pop();
433    }
434
435    Ok(())
436}
437
438impl<E: Error + Send + Sync + 'static> fmt::Display for Exn<E> {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        fmt::Display::fmt(&self.frame, f)
441    }
442}
443
444impl<E: Error + Send + Sync + 'static> PartialEq<str> for Exn<E> {
445    fn eq(&self, other: &str) -> bool {
446        crate::root_error_eq(self.frame().error(), other)
447    }
448}
449
450impl<E: Error + Send + Sync + 'static> PartialEq<&str> for Exn<E> {
451    fn eq(&self, other: &&str) -> bool {
452        <Self as PartialEq<str>>::eq(self, other)
453    }
454}
455
456impl<E: Error + Send + Sync + 'static> PartialEq<String> for Exn<E> {
457    fn eq(&self, other: &String) -> bool {
458        <Self as PartialEq<str>>::eq(self, other)
459    }
460}
461
462impl fmt::Display for Frame {
463    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
464        if f.alternate() {
465            // Keep individual Debug labels compact while reporting all causes.
466            write_frame_recursive(f, self, ErrorMode::Debug)
467        } else {
468            if crate::error::is_transparent_marker(self.error())
469                && let Some(diagnostic) = self.iter_errors_with_locations().next()
470            {
471                return fmt::Display::fmt(diagnostic.error(), f);
472            }
473            fmt::Display::fmt(self.error(), f)
474        }
475    }
476}
477
478/// A frame in the exception tree.
479pub struct Frame {
480    source: FrameSource,
481}
482
483/// The owning frame contents, with single-diagnostic formatting for standard source-chain consumers.
484/// Keeping this separate from `Frame` preserves its existing full-tree formatting without another allocation.
485pub(crate) struct FrameSource {
486    error: Box<dyn Error + Send + Sync + 'static>,
487    location: &'static Location<'static>,
488    children: Vec<Frame>,
489}
490
491impl fmt::Display for FrameSource {
492    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493        // Do not forward alternate formatting: a nested public Error could otherwise print its entire subtree.
494        write!(f, "{}", self.error())
495    }
496}
497
498impl fmt::Debug for FrameSource {
499    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500        fmt::Debug::fmt(self.error(), f)
501    }
502}
503
504impl Error for FrameSource {
505    fn source(&self) -> Option<&(dyn Error + 'static)> {
506        crate::error::native_source(self.error()).or_else(|| {
507            self.children.first().map(|frame| {
508                if frame.children().is_empty() {
509                    frame.error() as &(dyn Error + 'static)
510                } else {
511                    &frame.source as &(dyn Error + 'static)
512                }
513            })
514        })
515    }
516}
517
518impl FrameSource {
519    pub(crate) fn error(&self) -> &(dyn Error + Send + Sync + 'static) {
520        let mut error = &*self.error;
521        loop {
522            if let Some(erased) = error.downcast_ref::<Untyped>() {
523                error = &*erased.0;
524            } else if let Some(shared) = error.downcast_ref::<ErrorHandle>() {
525                error = shared.owned_error();
526            } else {
527                return error;
528            }
529        }
530    }
531
532    pub(crate) fn location(&self) -> &'static Location<'static> {
533        self.location
534    }
535
536    pub(crate) fn children(&self) -> &[Frame] {
537        &self.children
538    }
539}
540
541impl Frame {
542    /// Return the error as a reference to [`Error`].
543    ///
544    /// If the error was [erased](crate::Exn::erased), this is the original error,
545    /// so it can still be downcast to its actual type.
546    pub fn error(&self) -> &(dyn Error + Send + Sync + 'static) {
547        self.source.error()
548    }
549
550    /// Return the source code location where this exception frame was created.
551    ///
552    /// The file path is the compiler-provided path, before diagnostic formatting shortens it.
553    pub fn location(&self) -> &'static Location<'static> {
554        self.source.location()
555    }
556
557    /// Return explicitly raised child frames.
558    ///
559    /// Native [`Error::source()`] values are borrowed from [`Self::error()`] and traversed lazily, so they aren't owned
560    /// `Frame` children.
561    pub fn children(&self) -> &[Frame] {
562        self.source.children()
563    }
564
565    pub(crate) fn source_frame(&self) -> &FrameSource {
566        &self.source
567    }
568}
569
570/// A borrowed node that lets one traversal visit both explicit exception frames and native [`Error::source()`] chains.
571///
572/// Explicitly raised errors are stored as [`Frame`] values, whereas native sources remain owned by their errors and
573/// must be borrowed when traversed. `Source` represents such a borrowed native error and carries forward the location
574/// of its owning frame for internal formatting without turning the source into a frame or losing its concrete type.
575#[derive(Clone, Copy)]
576pub(crate) enum ErrorNode<'a> {
577    Frame(&'a Frame),
578    Source {
579        error: &'a (dyn Error + 'static),
580        location: &'static Location<'static>,
581    },
582    /// A source from a nested boundary's already flattened iterator; its descendants are emitted separately.
583    FlatSource {
584        error: &'a (dyn Error + 'static),
585        location: &'static Location<'static>,
586    },
587}
588
589impl<'a> ErrorNode<'a> {
590    pub(crate) fn error(self) -> &'a (dyn Error + 'static) {
591        match self {
592            ErrorNode::Frame(frame) => frame.error(),
593            ErrorNode::Source { error, .. } | ErrorNode::FlatSource { error, .. } => error,
594        }
595    }
596
597    fn root_error(self) -> &'a (dyn Error + 'static) {
598        let mut error = self.error();
599        while let Some(nested) = error.downcast_ref::<crate::Error>() {
600            error = nested.error();
601        }
602        error
603    }
604
605    /// Return the frame location used when formatting this node.
606    ///
607    /// A frame returns its own captured location. A native source inherits the location of the frame whose error owns its
608    /// source chain, providing formatting context even though no location was captured for the source itself.
609    pub(crate) fn location(self) -> &'static Location<'static> {
610        match self {
611            ErrorNode::Frame(frame) => frame.location(),
612            ErrorNode::Source { location, .. } | ErrorNode::FlatSource { location, .. } => location,
613        }
614    }
615
616    /// Return this node's diagnostic children in traversal order, promoting descendants of classification markers.
617    ///
618    /// A direct native [`Error::source()`] or I/O payload is first and inherits this node's formatting location.
619    /// For a frame, explicitly raised child frames follow it in insertion order. The compatibility `source()` of a nested [`crate::Error`] is
620    /// skipped in favor of its complete flattened diagnostic graph; following the compatibility source here would
621    /// expose only one path and duplicate that expansion.
622    pub(crate) fn children(self) -> Vec<ErrorNode<'a>> {
623        if matches!(self, ErrorNode::FlatSource { .. }) {
624            return Vec::new();
625        }
626        let error = self.error();
627        let location = self.location();
628        let mut children = Vec::new();
629        if let Some(nested) = error.downcast_ref::<crate::Error>() {
630            let root_error = self.root_error();
631            let mut skipped_root = false;
632            for source in nested
633                .iter_errors_with_locations()
634                .filter(|source| !source.error().is::<crate::Error>())
635            {
636                // Nested boundaries can have children before the innermost root in breadth-first order.
637                if !skipped_root && std::ptr::eq(source.error(), root_error) {
638                    skipped_root = true;
639                    continue;
640                }
641                children.push(ErrorNode::FlatSource {
642                    error: source.error(),
643                    location: source.location().unwrap_or(location),
644                });
645            }
646        } else if let Some(error) = crate::error::native_source(error) {
647            children.push(ErrorNode::Source { error, location });
648        }
649        if let ErrorNode::Frame(frame) = self {
650            children.extend(frame.children().iter().map(ErrorNode::Frame));
651        }
652        let mut diagnostics = Vec::new();
653        for child in children {
654            if crate::error::is_transparent_marker(child.error()) {
655                diagnostics.extend(child.children());
656            } else {
657                diagnostics.push(child);
658            }
659        }
660        diagnostics
661    }
662}
663
664/// Navigation
665impl Frame {
666    /// Follow the unique causal child until reaching a leaf or a branch.
667    ///
668    /// Native [`Error::source()`] values, I/O payloads, nested [`crate::Error`] graphs, and explicitly raised frames all
669    /// participate.
670    ///
671    /// An *aggregate* is the error at a branch that groups two or more diagnostic causes. This is a role in the error
672    /// tree, not a special concrete error type. For example, [`Exn::raise_all`] can attach multiple failed operations
673    /// to a shared `"batch failed"` [`crate::Message`]. That shared message is the aggregate, so selection stops there
674    /// rather than arbitrarily choosing one operation's error:
675    ///
676    /// ```text
677    /// outer context
678    /// └─ batch failed  (aggregate, selected)
679    ///    ├─ first operation failed
680    ///    └─ second operation failed
681    /// ```
682    ///
683    /// All [`crate::ClassificationMarker`] values are ignored. Their frames, including nested boundaries,
684    /// are transparent: their real descendants count as children of the nearest non-marker parent instead.
685    /// Return `None` if selection stays at this frame, allowing callers to fall back to [`Self::error()`], even for a
686    /// classification-only root.
687    pub fn probable_cause(&self) -> Option<&(dyn Error + 'static)> {
688        self.probable_cause_inner()
689    }
690
691    /// Iterate over all explicitly created frames in breadth-first order. The first frame is this instance, followed by
692    /// all explicitly raised children. Native [`Error::source()`] values are not frames.
693    pub fn iter_frames(&self) -> impl Iterator<Item = &Frame> + '_ {
694        let mut queue = std::collections::VecDeque::new();
695        queue.push_back(self);
696        BreadthFirstFrames { queue }
697    }
698}
699
700/// Breadth-first iterator over explicitly created `Frame`s.
701pub struct BreadthFirstFrames<'a> {
702    queue: std::collections::VecDeque<&'a Frame>,
703}
704
705impl<'a> Iterator for BreadthFirstFrames<'a> {
706    type Item = &'a Frame;
707
708    fn next(&mut self) -> Option<Self::Item> {
709        let frame = self.queue.pop_front()?;
710        for child in frame.children() {
711            self.queue.push_back(child);
712        }
713        Some(frame)
714    }
715}
716
717impl<E> From<Exn<E>> for Box<Frame>
718where
719    E: Error + Send + Sync + 'static,
720{
721    fn from(err: Exn<E>) -> Self {
722        err.frame
723    }
724}
725
726impl<E> From<Exn<E>> for Box<dyn Error + Send + Sync + 'static>
727where
728    E: Error + Send + Sync + 'static,
729{
730    fn from(err: Exn<E>) -> Self {
731        Box::new(err.into_error())
732    }
733}
734
735#[cfg(feature = "anyhow")]
736impl<E> From<Exn<E>> for anyhow::Error
737where
738    E: Error + Send + Sync + 'static,
739{
740    fn from(err: Exn<E>) -> Self {
741        anyhow::Error::from(err.into_chain())
742    }
743}
744
745impl<E> From<Exn<E>> for Frame
746where
747    E: Error + Send + Sync + 'static,
748{
749    fn from(err: Exn<E>) -> Self {
750        *err.frame
751    }
752}
753
754impl From<Frame> for Exn {
755    fn from(frame: Frame) -> Self {
756        Exn::from_boxed_frame(Box::new(frame))
757    }
758}
759
760impl Exn {
761    pub(crate) fn from_boxed_frame(mut frame: Box<Frame>) -> Self {
762        if !frame.source.error.is::<Untyped>() {
763            frame.source.error = Box::new(Untyped(frame.source.error));
764        }
765        Exn {
766            frame,
767            phantom: Default::default(),
768        }
769    }
770}
771
772#[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))]
773impl Exn {
774    pub(crate) fn from_chain(chain: ChainedError) -> Self {
775        let mut frames = Vec::new();
776        let mut next = Some(chain);
777        while let Some(node) = next {
778            next = node.source.map(|source| *source);
779            let frame = (!node.err.is_native_source()).then(|| Frame {
780                source: FrameSource {
781                    error: node.err.into_owned_error(),
782                    location: node.location,
783                    children: Vec::new(),
784                },
785            });
786            frames.push((frame, node.logical_parent));
787        }
788        // Native sources remain owned by their explicit frame; only those frames are rebuilt.
789        while let Some((frame, parent)) = frames.pop() {
790            let Some(mut frame) = frame else { continue };
791            frame.source.children.reverse();
792            match parent {
793                Some(parent) => frames[parent]
794                    .0
795                    .as_mut()
796                    .expect("an explicit frame has an explicit parent")
797                    .source
798                    .children
799                    .push(frame),
800                None => return frame.into(),
801            }
802        }
803        unreachable!("an error chain always contains its root frame")
804    }
805}
806
807/// A marker to show that type information is not available,
808/// while storing all extractable information about the erased type.
809/// It's the default type for [Exn].
810pub struct Untyped(Box<dyn Error + Send + Sync + 'static>);
811
812impl Untyped {
813    pub(crate) fn from_boxed(error: Box<dyn Error + Send + Sync + 'static>) -> Self {
814        Untyped(error)
815    }
816}
817
818impl fmt::Display for Untyped {
819    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
820        fmt::Display::fmt(&self.0, f)
821    }
822}
823
824impl fmt::Debug for Untyped {
825    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
826        fmt::Debug::fmt(&self.0, f)
827    }
828}
829
830impl Error for Untyped {
831    fn source(&self) -> Option<&(dyn Error + 'static)> {
832        self.0.source()
833    }
834}
835
836impl<E> From<Exn<E>> for ChainedError
837where
838    E: std::error::Error + Send + Sync + 'static,
839{
840    fn from(err: Exn<E>) -> Self {
841        let flattened = flatten_error_nodes(*err.frame);
842        let mut source = None;
843        for node in flattened.into_iter().rev() {
844            source = Some(Box::new(ChainedError {
845                err: node.error,
846                location: node.location,
847                logical_parent: node.logical_parent,
848                source,
849            }));
850        }
851        *source.expect("an Exn always contains its root error")
852    }
853}
854
855struct OwnedErrorNode {
856    error: ErrorHandle,
857    location: &'static Location<'static>,
858    logical_parent: Option<usize>,
859}
860
861/// Consume an exception-frame tree and flatten its errors into logical breadth-first order for [`ChainedError`].
862///
863/// Each frame's direct native [`Error::source()`] is queued before its explicitly raised child frames, and subsequent
864/// native sources continue as children of the preceding source. Every output node retains an owning [`ErrorHandle`], the
865/// frame location used for formatting, and the output index of its logical parent so the tree relationships can later be
866/// reconstructed. Native sources inherit their owning frame's location.
867///
868/// A nested [`crate::Error`] is retained as one node without following its compatibility `source()` chain. Its internal
869/// graph is expanded separately by the [`crate::Error`] traversal APIs, avoiding a partial and duplicated representation.
870fn flatten_error_nodes(root: Frame) -> Vec<OwnedErrorNode> {
871    enum Pending {
872        Frame {
873            frame: Frame,
874            logical_parent: Option<usize>,
875        },
876        Source {
877            error: ErrorHandle,
878            location: &'static Location<'static>,
879            logical_parent: usize,
880        },
881    }
882
883    let mut queue = VecDeque::from([Pending::Frame {
884        frame: root,
885        logical_parent: None,
886    }]);
887    let mut out = Vec::new();
888    while let Some(node) = queue.pop_front() {
889        let node_index = out.len();
890        match node {
891            Pending::Frame {
892                frame:
893                    Frame {
894                        source:
895                            FrameSource {
896                                error,
897                                location,
898                                children,
899                            },
900                    },
901                logical_parent,
902            } => {
903                let error = ErrorHandle::new(unerase(error));
904                if let Some(source) = error.source() {
905                    queue.push_back(Pending::Source {
906                        error: source,
907                        location,
908                        logical_parent: node_index,
909                    });
910                }
911                queue.extend(children.into_iter().map(|frame| Pending::Frame {
912                    frame,
913                    logical_parent: Some(node_index),
914                }));
915                out.push(OwnedErrorNode {
916                    error,
917                    location,
918                    logical_parent,
919                });
920            }
921            Pending::Source {
922                error,
923                location,
924                logical_parent,
925            } => {
926                if let Some(source) = error.source() {
927                    queue.push_back(Pending::Source {
928                        error: source,
929                        location,
930                        logical_parent: node_index,
931                    });
932                }
933                out.push(OwnedErrorNode {
934                    error,
935                    location,
936                    logical_parent: Some(logical_parent),
937                });
938            }
939        }
940    }
941    out
942}
943
944/// Remove all type-erasure markers before storing an error in a [`ChainedError`].
945///
946/// [`Untyped::source()`] deliberately forwards to the wrapped error's source to keep
947/// the marker transparent. Storing the marker itself in the chain would therefore
948/// hide a wrapped leaf error from source traversal and classification. Unwrapping it
949/// here retains the original runtime type without changing those source semantics.
950fn unerase(mut error: Box<dyn Error + Send + Sync + 'static>) -> Box<dyn Error + Send + Sync + 'static> {
951    loop {
952        match error.downcast::<Untyped>() {
953            Ok(untyped) => error = untyped.0,
954            Err(typed) => return typed,
955        }
956    }
957}