Is the behavior of built-in derive macros documented?

Today I learned, that a built-in #[derive(…)], say e.g. for Clone, puts additional bounds beyond the ones I was previously aware of, which would have been: the bounds from the struct definition and the P: Clone for each type parameter P of the struct.

No, that’s not the full picture, I’ve noticed there’s (at least) one more rule: additionally, any associated types, are also bound. But only if you spell them a certain way! This results in fun results such as

// works; puts a I::Item: Clone bound (in addition to I: Clone)
#[derive(Clone)]
struct Foo<I: Iterator>(I::Item);
// of course, this would do the same for a `PhantomData<I::Item>`;
// it’s all syntactical ^^

// fails to compile
#[derive(Clone)]
struct Foo<I: Iterator>(<I as Iterator>::Item);
error[E0277]: the trait bound `<I as Iterator>::Item: Clone` is not satisfied
 --> src/lib.rs:3:25
  |
2 | #[derive(Clone)]
  |          ----- in this derive macro expansion
3 | struct Foo<I: Iterator>(<I as Iterator>::Item);
  |                         ^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `<I as Iterator>::Item`
  |
  = note: this error originates in the derive macro `Clone` (in Nightly builds, run with -Z macro-backtrace for more info)

I mean… all this is a bit quirky and maybe questionable (leaking implementation details, right?), but possibly it is simply intentional and well-defined behavior, but this shit must be properly documented, no!?

I have looked at std docs and the reference so far. I have not dug into any GitHub issues, though they wouldn’t be proper documentation anyways, they could at least serve as description for future documentation and/or track the issue of the lack of documentation.

6 Likes

First GitHub thing I could find about this, apparently the PR that implemented the behavior, sounds like the <I as Iterator>::Item syntax was supposed to work…

… the test case that PR comes with (which still exists on master) – of course – spectacularly fails to actually test the case of using <Type as Trait>::Type syntax, by needlessly integrating the combination of all kinds of cases into single combined case/struct. A struct containing both a B::Type field and a <B as DeclaredTrait>::Type field will gain the B::Type: Debug / B::Type: PartialEq bound in question, even if only one of the cases actually successfully triggers it.

I haven’t reviewed the PR’s implementation, or confirmed whether the behavior wasn’t changed (deliberately or not) at a later date… it is as it it is now in Rust 1.0 though. I’ll assume for now the option that possibly some subsequent change might have broken it – I couldn’t quite believe yet if the main PR description, prominently featuring <A as Trait>::Type syntax, shows a case that isn’t even properly addressed,

1 Like

Seems like there is an open issue. Also interesting, the relatively recent PR that fixes aligns rust-analyzer’s behavior with the compiler’s.

All in all, I would conclude that this certainly is not documented anywhere, otherwise e.g. rust-analyzer would’ve gotten it right to begin with. Thanks for reading this thread! :wave:

2 Likes

I feel you, but are you really that surprised? Almost every area I deep-dive on is underdocumented if not undocumented, with loads of unnoted corner-cases.

(Granted, std documentation is typically pretty good and more complete compared to the language.)

Maybe that's a case for internals? Maybe there's a good reason that this error stayed but maybe it's also just that somone needs to do the work.

I'm not surprised about the lack of documentation of the (seemingly even unintentional) corner cases, of course. But I would have bet that tbe official documentation at least contained the information that derived implementations unconditionally add bounds for the type parameters. That's quite basic stuff that almost everyone should know. I felt confused there wasn't even a partial documentation anywhere that so would have been able to point to somewhere and say "either it's unintentional, or so-and-so place should document it better".

Ah wait I just found the place(s) to point to… It’s documented on the traits! E.g. https://doc.rust-lang.org/std/clone/trait.Clone.html#derivable, stating “For a generic struct, #[derive] implements Clone conditionally by adding bound Clone on generic parameters.”


Anyways, certainly, the standard library generally being well-documented is an aspect, too.

Also, many quirks of the language are documented. I’m thinking of the “The evaluation order of operands swaps depending on the types of the operands: with primitive types the right-hand side will get evaluated first, while with non-primitive types the left-hand side will get evaluated first.” warning; or the in-depth explanation of temporary lifetimes, in the reference. Documenting something “weird” clearly acknowledges that it’s known behavior not intended to be changed… (though in this case, maybe we just want to change it…[1] if changing it outright is too breaking, then perhaps with an edition).

The thing that’s probably most underdocumented is the borrow checker, but at least one can argue for that, that it doesn’t matter as much, since it never changes the behavior of any code.


  1. I’m not sure whether it’s better to just eliminate this one case of creating bounds based on field types, or whether the <T as Tr>::Ty syntax should instead just be included ↩︎

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.