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