Skip to content

GEN-192: Use Pin API for error-stack #5008

@Bennett-Petzold

Description

@Bennett-Petzold

Related Problem

Nesting enums is an effective strategy for providing errors from a library, allowing the downstream user to use arbitrarily granular handling. e.g.

struct Err1 {}

enum Err2 {
  Downstream(Err1),
  Originating
}

However, this does not seem possible in Report. I believe the way to emulate this for functions producing reports is as follows:

struct Err1 {}

enum Err2 {
  Downstream,
  Originating
}

fn foo() {
  let orig_report = Report::new(Err1);
  let top_report = orig_report.change_context(Err2::Downstream);
  
  match top_report.current_context() {
    Downstream => {
      let orig_err = top_report.downcast_ref::<Err1>().unwrap();
      ...
    },
    Originating => { ... }
  }
}

This requires the library author to document the downstream error and the user to read that documentation. Standard error types have no such limitation.

Proposed Solution

Frames are currently stored as Box<dyn FrameImpl> with mutable handle methods. Make this type Pin<Box<dyn FrameImpl>> and replace mutable access methods with https://doc.rust-lang.org/std/pin/struct.Pin.html#method.set wrappers. Report can then be self-referential with its errors, enabling Deref access through some const * wrapper (ReportCell) and a special construction method (change_context_ref) that provides a reference to the current context.

struct Err1 {}

enum Err2 {
  Downstream(ReportCell<Err1>),
  Originating
}

fn foo() {
  let orig_report = Report::new(Err1);
  let top_report = orig_report.change_context_ref(|err_ref| Err2::Downstream(err_ref));
  
  match top_report.current_context() {
    Downstream(orig_err_ref) => {
      let orig_err: &Err1 = *orig_err_ref;
      ...
    },
    Originating => { ... }
  }
}

For soundness when a Context uses borrowed data during drop, Report would need to be changed from Box<Vec<Frame>> to Box<Vec<ManuallyDrop<Frame>>> and given a custom Drop implementation that enforces top of stack to bottom of stack drop ordering.

This scheme would allow for both enum matching and mutation (via replacement) for purposes outlined in #4610 (comment). However, it would introduce unsafe into the codebase for its implementation.

Alternatives

It's possible that I'm simply wrong about enum handling (If that's the case, great!). I've also experimented with mitigating this by:

  1. Wrapping Report to create the guarantees and transforms I need -- ends up creating a complex error type exclusive to a single library.
  2. Writing docs to indicate next-on-stack error type for error enums.
  3. Avoiding returning Report<E> from most functions, wrapping it at the top. Returning plain E allows for pattern matching, but this puts all backtrace line numbers at the topmost error. It also avoids most the benefits of this library.

Additional context

If this seems viable, maybe as a breaking change or as an alternate form of Report under a feature flag, I'm happy to work on a PR.

Metadata

Metadata

Assignees

Labels

area/libsRelates to first-party libraries/crates/packages (area)area/libs > error-stackAffects the `error-stack` crate (library)category/enhancementNew feature or requestlang/rustPull requests that update Rust code

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions