Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a pessimistic Backtrace type #88

Open
shepmaster opened this issue May 29, 2019 · 3 comments
Open

Add a pessimistic Backtrace type #88

shepmaster opened this issue May 29, 2019 · 3 comments
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed

Comments

@shepmaster
Copy link
Owner

It's hard to tell if a foreign error type has a backtrace or not. If it does, we don't want to create our own, otherwise we do.

It could be interesting to try and have some type that checks if the underlying error has a backtrace. If so, it does nothing. Otherwise, it generates one at that point.

@shepmaster shepmaster added enhancement New feature or request help wanted Extra attention is needed good first issue Good for newcomers labels May 29, 2019
@Lej77
Copy link

Lej77 commented May 31, 2019

I guess this would be accomplished with the derive macro. I have written a macro that can be used on a type and if the type implements a certain trait then it can do something with that trait.

Here is all the code I wrote when testing out how this could work. It became quite a lot of code so here is a simplified version that includes as little unnecessary code as possible.

Edit: Here is an even simpler version of the code. It's macro doesn't create a new scope via a module because of the limitations that I discuss in the Creating a new scope with a module section. Instead it tries to name the affected method with a unique name __snafu_get_backtrace.

Code explanation

The code works using "resolver" structs that has a method that takes an error and returns its backtrace. The structs contain a PhantomData that is used to store the error type. This means that the structs' impl blocks can have where clauses based on the error type. This is used to only implement the method that gets the backtrace if the error type actually has a backtrace.

The code uses the std::ops::Deref trait to allow for a fallback method. If the method that gets the backtrace isn't implemented for the resolver then it derefs to a struct that has a method with the same signature but that just returns None.

The macro has the constraint that it requires that no conflicting traits are in scope. Conflicting traits are traits that are implemented for the resolver and that provides a method with the same name that is used for the method that gets the backtrace from an error. I can see two ways to work around that issue:

  1. Use a module in the macro to get a new scope without any user defined traits. This is the solution that my example code uses.
  2. Use a method name that is really weird so that it is very unlikely that the user will have a trait in scope that has the same method name.

Creating a new scope with a module

After some more consideration the first solution (using a module to get a new scope) seems to have some issues. It looks like if a struct is defined inside a function then it can't be referenced from anywhere else. If this is true then it is not possible to write a macro that works for every struct using the first method.

Example code:

fn module_scope() {
    struct Test;
    mod inner {
        // Won't compile:
        // fn take(_: super::Test) { }

        // Won't compile:
        // fn take(_: super::module_scope::Test) { }

        // Won't compile:
        // fn take(_: Test) { }
    }
}

Edit: Workaround that allows creating a new scope with a module

After some more thought and experimentation it is possible to refer directly to the user provided type from inside a new module, this would make the macro work in all situations.

This is some code that demonstrates how to safely reference a type inside the new module:

fn module_scope() {
    struct Test;
    impl Test {
        fn test(&self) -> usize {151}
    }

    impl inner::GetTarget for () {
        type Target = Test;
    }
    mod inner {
        pub(super) trait GetTarget {
            type Target;
        }
        pub(super) fn take(x: <() as GetTarget>::Target) {
            // User traits aren't in scope but its still possible to refer directly to the user provided type:
  
            assert_eq!(x.test(), 151);
        }
    }
  inner::take(Test);
}

With this method its possible to make a macro that gets a backtrace if a type implements snafu::ErrorCompat and otherwise returns None. Here is a playground that demonstrates how this can be done.

@shepmaster
Copy link
Owner Author

Wow, that's a lot of hard work (and code)! Maybe I was a bit optimistic that this would be an "easy" addition!

I was thinking something along the lines of this.

TL;DR, we do need some help from the derive macro to appropriately call PessimisticBacktrace::new with a reference to the error and to call pbwo.0.as_ref().or_else(|| wo.backtrace() at the appropriate time. This is ugly because it makes that type "special ", although we could introduce traits to make it a bit more general if it was useful.

What do you think?

@Lej77
Copy link

Lej77 commented Jun 10, 2019

Your code assumes that the source error implements the ErrorCompat trait while my code doesn't. This means that the macro could only generate your code if the ErrorCompat trait is implemented for the source error otherwise it would cause a compile time error. If this trait is not already required (which I don't think it is) then the user would need to annotate their source error with an attribute or something to tell the macro to generate your code.

One way to be able to always generate the source backtrace checking code is if specialization is used to allow a blanket implementation of ErrorCompat for errors that don't manually implement it. Since specialization is unstable this isn't currently possible (at lest on stable rust).

My code is a hack for specialization that allows the code to work even if the source error doesn't implement ErrorCompat. This means that the code could always be generated without the user annotating the source error with an attribute or something like that.

Edit: After some more thought I understand what you really mean now. You want the user to specify the PessimisticBacktrace type in their struct while I was thinking that it should just always happen by default when the Backtrace type is used. My method would require that the Backtrace type can be created empty or can be copied from another Backtrace. Don't know which approach would make the most sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants