Barriers for `derive`


#1

Sometimes I want a type T to implement some trait (e.g. PartialEq) but prevent other types containing T from deriving that trait.
This post is a question about how to achieve this better.

As a concrete examples, I’ll look at Span and Ident in the compiler, because I’ve recently noticed that careless deriving of PartialEq for structures containing spans and identifiers created a mine field of bugs in macro expansion. Constructions that are commonly used by people are now bug-free, but step to the left or step to the right and things stop working.

Spans can be reasonably compared themselves, but usually if they are used in larger structures they serve informational purpose, e.g. one token tree should be equal to another identical token tree regardless of where it’s located in the source file.
Identifiers are a bit different - they can be reasonably compared hygienically, but before the comparison you may want to tweak some contexts or you may want to compare unhygienically in some cases. Larger structures need to decide how exactly they want to compare identifiers.

So I want a way to create a barrier for derive(PartialEq) on structures containing Span or Ident.
If someone tries to derive, they should see a message “STOP! THINK!” and write implementation of PartialEq manually, or not write it at all and write something more flexible instead.


One possible solution is to capitulate and not to implement PartialEq and add a separate comparison function instead.
Then you can use something like Ident::equal(i1, i2) for comparisons and something like ComparableIdent wrapper for maps/sets (HashMap<ComparableIdent, Value>).
I tried this approach, it’s workable but not convenient and involves some amount of suffering.

Any better solution?
(Possibly including enhancements to derive.)


#2

The flip side to your solution is to implement PartialEq on the base type, and then use a non-PartialEq wrapper in contexts where you really don’t want it compared. I guess it depends which should be the default case.


#3

From a purely technical standpoint (and not whether we should support this), some form of annotation could work:

#[derive(PartialEq)]
#[derive_final] // Unbounded bikeshed on naming...
struct Foo {
    // Fields...
}

or just:

#[derive_final(PartialEq)]
struct Foo {
    // Fields...
}

This has a few problems tho.

  1. There is no way for custom derive macros to know whether #[derive_final] was done on the type of an field inside the currently-being-derived type.
    • This can be solved by passing such information to the derive macro, but then deriving becomes other than strictly textual (on the text of the type definition), and you will need parts or the whole text of other referenced types in the fields or variants.
    • This solution in turn creates other problems around compiler phases.
  2. This only gives a measure of protection for #[derive(..)] but no mechanism for protecting bad equivalent impls.

Another way to solve 1. is by adding eq_final:

pub trait PartialEq<Rhs: ?Sized = Self> {
    fn eq(&self, other: &Rhs) -> bool { self.eq_final(other) }
    fn eq_final(&self, other: &Rhs) -> bool;
}

And then we #[derive_final] with:

#[derive_final(PartialEq)]
struct Foo(u8);

// Generates:
impl PartialEq for Foo {
    fn eq(&self, other: &Rhs) -> bool {
         self.0.eq_final(&other.0)
    }
    fn eq_final(&self, other: &Rhs) -> bool {
         panic!("PartialEq for Foo is #[derive_final]")
    }
}

And when we add a normal derive on top of that, we get:

#[derive(PartialEq)]
struct Bar(Foo);

// Generates:
impl PartialEq for Foo {
    fn eq_final(&self, other: &Rhs) -> bool { self.0.eq_final(&other.0) }
}

Now, when you try to execute Bar::eq_final or Bar::eq, you will get a panic!. However, this mechanism is quite invasive and also uses dynamic correctness checks, which is ungreat.