Skip to content
2 changes: 1 addition & 1 deletion gix-error/src/exn/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub trait ErrorExt: std::error::Error + Send + Sync + 'static {
I: IntoIterator,
I::Item: Into<Exn<T>>,
{
Exn::from_iter(sources, self)
Exn::raise_all(sources, self)
}
}

Expand Down
62 changes: 30 additions & 32 deletions gix-error/src/exn/impls.rs
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I know gix-error has to wrap once more the inner error into Untyped because downcast must have the type param matched. In this case, it seems to be inevitable.

Copy link
Member

@Byron Byron Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory, the into_inner() and into_box() can be removed as there is always gix_error::Error to hold these types.

Let's leave it as is for now, and I will consider removing them to avoid that double-box.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

use std::error::Error;
use std::fmt;
use std::fmt::Formatter;
use std::marker::PhantomData;
use std::ops::Deref;
use std::panic::Location;
Expand Down Expand Up @@ -69,10 +68,8 @@ impl<E: Error + Send + Sync + 'static> Exn<E> {
}

/// Create a new exception with the given error and children.
///
/// It's no error if `children` is empty.
#[track_caller]
pub fn from_iter<T, I>(children: I, err: E) -> Self
pub fn raise_all<T, I>(children: I, err: E) -> Self
where
T: Error + Send + Sync + 'static,
I: IntoIterator,
Expand All @@ -94,6 +91,14 @@ impl<E: Error + Send + Sync + 'static> Exn<E> {
new_exn
}

/// Use the current exception as the head of a chain, adding `err` to its children.
#[track_caller]
pub fn chain<T: Error + Send + Sync + 'static>(mut self, err: impl Into<Exn<T>>) -> Exn<E> {
let err = err.into();
self.frame.children.push(*err.frame);
self
}

/// Use the current exception the head of a chain, adding `errors` to its children.
#[track_caller]
pub fn chain_iter<T, I>(mut self, errors: I) -> Exn<E>
Copy link
Contributor Author

@tisonkun tisonkun Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps change this to chain_all as well.

P.S. I'll start a PR on exn to add these two chain methods. In the initial use case of exn in ScopeDB, we can always know all the children when constructing the chain/tree, so we doubt whether to make children mutable after the chain has been established. But reading gix's use cases, it seems chains that adding children afterwards can be helpful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps change this to chain_all as well.

That sounds great, please feel free to do this here, otherwise I will do it in a follow-up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

Expand All @@ -116,14 +121,6 @@ impl<E: Error + Send + Sync + 'static> Exn<E> {
self.frame.children.drain(..).map(Exn::from)
}

/// Use the current exception the head of a chain, adding all `err` to its children.
#[track_caller]
pub fn chain<T: Error + Send + Sync + 'static>(mut self, err: impl Into<Exn<T>>) -> Exn<E> {
let err = err.into();
self.frame.children.push(*err.frame);
self
}

/// Erase the type of this instance and turn it into a bare `Exn`.
pub fn erased(self) -> Exn {
let untyped_frame = {
Expand All @@ -148,7 +145,7 @@ impl<E: Error + Send + Sync + 'static> Exn<E> {
}

/// Return the current exception.
pub fn as_error(&self) -> &E {
pub fn error(&self) -> &E {
self.frame
.error
.downcast_ref()
Expand All @@ -175,19 +172,21 @@ impl<E: Error + Send + Sync + 'static> Exn<E> {
}

/// Turn ourselves into a top-level [Error] that implements [`std::error::Error`].
///
/// [Error]: crate::Error
pub fn into_error(self) -> crate::Error {
self.into()
}

/// Return the underlying exception frame.
pub fn as_frame(&self) -> &Frame {
pub fn frame(&self) -> &Frame {
&self.frame
}

/// Iterate over all frames in breadth-first order. The first frame is this instance,
/// followed by all of its children.
pub fn iter(&self) -> impl Iterator<Item = &Frame> {
self.as_frame().iter()
self.frame().iter()
}

/// Iterate over all frames and find one that downcasts into error of type `T`.
Expand All @@ -204,18 +203,18 @@ where
type Target = E;

fn deref(&self) -> &Self::Target {
self.as_error()
self.error()
}
}

impl<E: Error + Send + Sync + 'static> fmt::Debug for Exn<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write_frame_recursive(f, self.as_frame(), "", ErrorMode::Display, TreeMode::Linearize)
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_frame_recursive(f, self.frame(), "", ErrorMode::Display, TreeMode::Linearize)
}
}

impl fmt::Debug for Frame {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_frame_recursive(f, self, "", ErrorMode::Display, TreeMode::Linearize)
}
}
Expand All @@ -233,7 +232,7 @@ enum TreeMode {
}

fn write_frame_recursive(
f: &mut Formatter<'_>,
f: &mut fmt::Formatter<'_>,
frame: &Frame,
prefix: &str,
err_mode: ErrorMode,
Expand All @@ -257,9 +256,8 @@ fn write_frame_recursive(
write!(f, "\n{prefix}└─ ")?;

let child_child_len = child.children().len();
let may_linerarize_chain =
matches!(tree_mode, TreeMode::Linearize) && children_len == 1 && child_child_len == 1;
if may_linerarize_chain {
let may_linearize_chain = matches!(tree_mode, TreeMode::Linearize) && children_len == 1 && child_child_len == 1;
if may_linearize_chain {
write_frame_recursive(f, child, prefix, err_mode, tree_mode)?;
} else if cidx < children_len - 1 {
write_frame_recursive(f, child, &format!("{prefix}| "), err_mode, tree_mode)?;
Expand All @@ -271,14 +269,14 @@ fn write_frame_recursive(
Ok(())
}

fn write_location(f: &mut Formatter<'_>, exn: &Frame) -> fmt::Result {
fn write_location(f: &mut fmt::Formatter<'_>, exn: &Frame) -> fmt::Result {
let location = exn.location();
write!(f, ", at {}:{}:{}", location.file(), location.line(), location.column())
}

impl<E: Error + Send + Sync + 'static> fmt::Display for Exn<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
std::fmt::Display::fmt(&self.frame, f)
fmt::Display::fmt(&self.frame, f)
}
}

Expand Down Expand Up @@ -451,14 +449,14 @@ impl From<Frame> for Exn {
pub struct Untyped(Box<dyn Error + Send + Sync + 'static>);

impl fmt::Display for Untyped {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
std::fmt::Display::fmt(&self.0, f)
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}

impl fmt::Debug for Untyped {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
std::fmt::Debug::fmt(&self.0, f)
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}

Expand All @@ -468,14 +466,14 @@ impl Error for Untyped {}
pub struct Something;

impl fmt::Display for Something {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Something went wrong")
}
}

impl fmt::Debug for Something {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
std::fmt::Display::fmt(&self, f)
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self, f)
}
}

Expand Down
4 changes: 2 additions & 2 deletions gix-error/src/exn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ pub use impls::{Frame, Something, Untyped};

mod macros;

/// An exception type that can hold an [error tree](Exn::from_iter) and the call site.
/// An exception type that can hold an [error tree](Exn::raise_all) and the call site.
///
/// While an error chain, a list, is automatically created when [raise](Exn::raise)
/// and friends are invoked, one can also use [`Exn::from_iter`] to create an error
/// and friends are invoked, one can also use [`Exn::raise_all`] to create an error
/// that has multiple causes.
///
/// # Warning: `source()` information is stringified and type-erased
Expand Down
2 changes: 1 addition & 1 deletion gix-error/tests/error/exn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ fn error_tree() {
|
└─ E7, at gix-error/tests/error/main.rs:22:30
");
insta::assert_debug_snapshot!(err.as_frame().iter().map(ToString::to_string).collect::<Vec<_>>(), @r#"
insta::assert_debug_snapshot!(err.frame().iter().map(ToString::to_string).collect::<Vec<_>>(), @r#"
[
"E6",
"E5",
Expand Down
4 changes: 2 additions & 2 deletions gix-error/tests/error/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ mod utils {
let e11 = Error("E11").raise();
let e12 = e11.raise(Error("E12"));

let e5 = Exn::from_iter([e3, e10, e12], Error("E5"));
let e5 = Exn::raise_all([e3, e10, e12], Error("E5"));

let e2 = Error("E2").raise();
let e4 = e2.raise(Error("E4"));

let e7 = Error("E7").raise();
let e8 = e7.raise(Error("E8"));

Exn::from_iter([e5, e4, e8], Error("E6"))
Exn::raise_all([e5, e4, e8], Error("E6"))
}

#[derive(Debug)]
Expand Down
4 changes: 2 additions & 2 deletions gix/src/revision/spec/parse/delegate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ impl<'repo> Delegate<'repo> {
match (ambiguous_errors.pop(), ambiguous_errors.pop()) {
(Some(one), None) => Some(one),
(Some(one), Some(two)) => {
Some(Exn::from_iter([one, two], message!("Both objects were ambiguous")).erased())
Some(Exn::raise_all([one, two], message!("Both objects were ambiguous")).erased())
}
_ => (!delayed_errors.is_empty()).then(|| {
if delayed_errors.len() == 1 {
delayed_errors.pop().expect("it's exactly one")
} else {
Exn::from_iter(delayed_errors, message!("one or more delayed errors")).erased()
Exn::raise_all(delayed_errors, message!("one or more delayed errors")).erased()
}
}),
}
Expand Down