Deriving has this problem where it can’t generate the desired impls, such as in the following example:
struct Private<T>(T);
#[derive(Clone)]
pub struct Foo<T>(Rc<Private<T>>);
// should generate:
impl<T> Clone for Foo<T>
where Rc<Private<T>>: Clone {...}
// but instead generates the more restrictive:
impl<T> Clone for Foo<T>
where T: Clone {...}
Because the bound mentioning Private
would cause a “private type in public interface” error, deriving has no choice but to place bounds on type parameters directly, making Foo<NonClone>
not Clone
even though Rc
is always Clone
.
Similar situations arise with references and raw pointers.
Serde is generating the correct bounds but at the cost of warnings that will turn into errors in the future (unless the rules get relaxed in the future).
Armed with the knowledge that these bounds mentioning private types can be usually rewritten to no bounds (like above) or bounds on type parameters, I opened RFC PR 1671.
That RFC contains a complex where
clause elaboration rule which lets deriving work with the exact bounds without letting users observe bounds that depend on impl
s they can’t see.
However, @petrochenkov pointed out that the current check looks at bounds to handle cases like this:
pub trait Trait<T> {
type Assoc; // can be T in some impls
}
pub fn foo<T: Trait<Private>>() -> T::Assoc {...}
The current syntactic check needs to look at both the function signature (fn() -> T::Assoc
) and bound (T: Trait<Private>
) and only the latter suggests that the signature might be exposing a private type.
Note that this is not the case for deriving, which is collateral damage here.
If we expand associated types to the fully qualified form, we get:
pub fn foo<T: Trait<Private>>() -> <T as Trait<Private>>::Assoc {...}
The resulting return type is enough to ban this case.
Therefore, the complex RFC can be replaced with simply ignoring bounds and checking types only after expanding associated types to the fully qualified form (<X as Trait<Y>>::Assoc
).
The guarantee you then get is that no values of private types could be exported from the module (or taken as arguments by exported functions), deriving “just” works, but users can see the effects of private impl
s (although you can cause that right now with re-exporting a subset of a private module).
I am leaning towards the simpler solution but I wanted to get a few more opinions before I throw away the more complex RFC.
Do you think the simpler solution that doesn’t look at the bounds at all and lets deriving do its thing in peace is the right choice? Or does it allow too much code (i.e. code which is bounded on hidden impls) to compile?
EDIT: Added an explanation of the associated type issue that caused bounds to be checked.