Orphan rules wrongfully? rejecting generic impl of fundamental trait with local marker

my understanding of fundamental traits are that a downstream crate may impl it for another foreign type as long as it itself has a generic argument that is set to a local type.

Upstream Crate

#![feature(fundamental)]

#[fundamental]
pub trait FundamentalTrait<T>{}

Downstream Crate

enum LocalMarker{}
trait Realisation: FundamentalTrait<LocalMarker> + ToString{}
impl<T: Realisation> FundamentalTrait<LocalMarker> for T{}

impl Realisation for u8{}
impl Realisation for u16{}

With other words in my understanding this should be valid code, but it is not:

type parameter T must be covered by another type when it appears before the first local type (LocalMarker) [E0210] Note: implementing a foreign trait is only possible if at least one of the types for which it is implemented is local, and no uncovered type parameters appear before that first local type Note: in this case, 'before' refers to the following order: impl<..> ForeignTrait<T1, ..., Tn> for T0, where T0 is the first and Tn is the last

Am I correct that this is a bug in the compiler?

Doing it manually for each type instead of a generic impl works correctly, however i realised that using a generic impl gives better compiler errors in a downdownstream crate

No, that’s not right. That’s a misinterpretation, somewhat transferring the concept of #[fundamental]-marked type to the trait case, even though IMO the two features have not so much too do with each other at all.

Instead, what the page you linked already says in an answer:

traits can also be marked with #[fundamental]. This has a dual meaning to fundamental types. If any type doesn't currently implement a fundamental trait, it can be assumed that that type won't implement it in the future (again, barring a breaking change).

so, no compiler-bug, just a misunderstanding on your end of what it is that the feature does.


What #[fundamental] on a trait allows is get rid of some instances of “upstream crate might add such-and-such impl in the future” error cases. I might add a concrete example to this reply shortly. Here’s some example code


trait MyTrait {}
impl<T: Iterator> MyTrait for T {}

trait MyTrait2 {}
impl<T: FnOnce()> MyTrait2 for T {}

//impl MyTrait for u8 {} // not okay; compilation error if uncommented
// error message contants the following:
// note: upstream crates may add a new impl of trait `std::iter::Iterator` for type `u8` in future versions

// okay, because #[fundamental] on FnOnce, which guarantees that types
// such as `u8` that don't currently implement FnOnce will never implement
// FnOnce in the future
impl MyTrait2 for u8 {}
3 Likes

I believe what it boils down to is that the orphan check doesn't care about fundamental traits, but the overlap check does.

yeah i realise now what i actually misread.

this still seems relevant tho as it does have the effect i describe:
as the trait is fundamental and the crate does not include any as following:

impl<T, U> FundamentalTrait<U> for T{}

or

impl<U> FundamentalTrait<U> for AnyConcreteType{}

this in principle allows downstream crates to do

impl FundamentalTrait<LocalMarker> for AnyConcreteType{}

and as this is indeed AnyConcreteType, a generic impl would have the same effects, or am i missing some coherence effect somewhere?

I don't think the fundamental-ness plays a role in these discussions anymore. Blanket impls, including ones “just” of the form

impl<U> FundamentalTrait<U> for AnyConcreteType{}

do in fact constitute technically-breaking changes (when added later) for ordinary traits, too.


The general issue with downstream

impl<T> Trait<LocalType> for T {}

is that it overlaps with

impl<T> Trait<T> for OtherLocalType {}

in other independent downstream crates. The orphan rules give an order/hierarchy to Self and generic argument, and arbitrarily choose to allow the latter and disallow the former, in order to avoid this conflict.

Your example further limits T to T: Realisation (so OtherLocalType from a completely independent downstream crate wouldn’t implement 'Realisation; unless a blanket impl in our same crate that defines Realisation` does this) but the trait solver and its rules aren’t … let’s say “smart enough” … to handle such reasoning, and really, formulating good rules for supporting this seems non-trivial.

What I don't quite understand however is, that the code in the OG post works if all of it is in the same crate.

Please mind tho that trait LocalMarker{} is not pub it acts as a private::Sealed here

In that case, the trait is local to that impl.

Feel free to check out the reference for a write-down of the full rule.

I assume you meant struct LocalMarker {}. Visibility isn’t really a huge hindrance to trait implementations. If you write a generic impl<T> …something…, then all types allowed for T also include types that are not visible at the place the impl was given.[1]


  1. Now, I’ve never thought about the question if there is possible consistent rule-sets that give different treatment where you know that the places where the type (like LocalMarker) and the places where the impl are used/usable are kind-of completely disjoint, but intuitively that’s either impossible, or at least complicated & highly non-trivial language-design that was never explored, and might be undesirable for being too complicated (e.g. complicated to learn, too). ↩︎

2 Likes

I concede. I am sure there were good reasons why this was exactly decided as it is now for now.

Anyway as concrete impl are still possible my abi still works, and i have been doing all this marker type bending just because we do not have associated traits/metatraits yet.

trait MetaTrait<trait T>: T{}

anyway thanks for taking you time and writing out answers

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.