Skip to content

Supertrait Auto-impl#3851

Open
dingxiangfei2009 wants to merge 13 commits intorust-lang:masterfrom
dingxiangfei2009:supertrait-auto-impls
Open

Supertrait Auto-impl#3851
dingxiangfei2009 wants to merge 13 commits intorust-lang:masterfrom
dingxiangfei2009:supertrait-auto-impls

Conversation

@dingxiangfei2009
Copy link

@dingxiangfei2009 dingxiangfei2009 commented Aug 26, 2025

We would like to propose auto impl syntax for supertraits for a few language enhancement, to ultimately facilitate easier trait evolution and refactoring and easier trait authoring when trait hierarchy is in consideration.

24/09/2025: revision published with typo fixes and dropping the unsafe qualifier for probe_extern_impl attribute.

Past edit history 21/09/2025: revision published with the following changes. - Address [comments from @cramertj](https://github.com//pull/3851#discussion_r2301552062) by including the worked examples mentioned in the [design meeting](https://hackmd.io/@1n2nRkvSTd-QgK8cCPds1w/HkBmeCE7xx). A new proposal is included so that an unsafe attribute may be used to allow compiler to probe further and decide if `auto impl` default implementation would be conflicting. This might need consultation with the types team as its effectiveness and stability depends very much on the capabilities of the trait solver. - Address [comments from @ElchananHaas](https://github.com//pull/3851#discussion_r2306004253) and [comments from @N4tus](https://github.com//pull/3851#issuecomment-3239334843) on several deficiencies in the worked examples, such as missing superbounds and complexity in the motivating examples. - Address [a comment from @programmerjake](https://github.com//pull/3851#discussion_r2302020617) by removing the template text.

Rendered

@ehuss ehuss added T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC. labels Aug 26, 2025
@traviscross traviscross added the I-lang-radar Items that are on lang's radar and will need eventual work or consideration. label Aug 27, 2025
@N4tus
Copy link

N4tus commented Aug 30, 2025

The explanation states that the traits need to have a super/sub trait relation. But some examples use traits e.g. MouseEventHandler and Double, that are not in a relation to the trait they provide an auto impl for.

dingxiangfei2009 and others added 2 commits September 21, 2025 17:00
Signed-off-by: Xiangfei Ding <dingxiangfei2009@protonmail.ch>
@fmease
Copy link
Member

fmease commented Sep 22, 2025

@dingxiangfei2009 Please don't force-push in this repo, that's heavily discouraged. From the README.md:

Specifically, do not squash or rebase commits after they are visible on the pull request.

@dingxiangfei2009
Copy link
Author

Quick question for @fmease, should I rename the RFC file from serial 0000-... to 3851-... in this text by ourselves? Or will there be a bot to rename the file?

@tomassedovic
Copy link
Contributor

@dingxiangfei2009 the name won't get updated automatically. We've merged the 2025H2 RFC, forgot to update and had to open a separate PR to fix the name: #3860.

It's possible to declare that an auto implementation is unsafe.
```rs
trait MySubTrait: MyTrait {
unsafe auto impl MyTrait;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, does this create an unsafe requirement or discharge an existing unsafe requirement? you seem to have it do both which is generally not how Rust uses unsafe anymore, now that we have unsafe_op_in_unsafe_fn enabled by default

Copy link
Author

@dingxiangfei2009 dingxiangfei2009 Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that this can be confusing.

  • Inside a trait Subtrait: .. { .. } block, unsafe auto impl creates an unsafe requirement.
  • Inside a impl Subtrait { .. } block, unsafe auto impl discharges the corresponding unsafe requirement.

Should we adjust the "keyword" orders, so that the distinction can be more obvious?

Copy link
Member

@programmerjake programmerjake Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that seems less useful than what I would have expected needing unsafe for -- something like:

/// safety: `returns_even` must always return an even integer
// unsafe means we create a requirement that impls must fulfill
pub unsafe Supertrait {
    fn returns_even(&self) -> u8;
}

pub trait Subtrait: Supertrait {
    /// safety: returns_even always returns an even integer
    // unsafe here means we're fulfilling the requirement since this is an impl of Supertrait
    unsafe auto impl Supertrait {
        fn returns_even(&self) -> u8 {
            self.get_int().wrapping_mul(2)
        }
    };
    fn get_int(&self) -> u8;
}

impl Subtrait for u8 {
    fn get_int(&self) -> u8 {
        *self
    }
}

pub struct Even(u8);

impl Subtrait for Even {
    // no unsafe needed here because this isn't fulfilling or creating any safety requirements,
    // the unsafe impl Supertrait below does that,
    // all we're doing here is saying this impl doesn't also impl Supertrait
    extern impl Supertrait;
    fn get_int(&self) -> u8 {
        self.0 / 2
    }
}

// safety: returns_even always returns an even integer because Even contains an even integer
// unsafe here means we're fulfilling the requirement since this is an impl of Supertrait
unsafe impl Supertrait for Even {
    fn returns_even(&self) -> u8 {
        self.0
    }
}

Copy link
Member

@cramertj cramertj Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to @programmerjake 's expectations-- I would've expected unsafe auto impl to discharge an unsafe requirement-- that is, guarante that the provided impl of the supertrait fulfills the requirements of the unsafe trait SuperTrait. This is the usual meaning of unsafe impl in Rust.

I wouldn't have expected there to be an option to explicitly specify an auto impl as unsafe to apply.

Copy link
Author

@dingxiangfei2009 dingxiangfei2009 Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. So to not overload the meaning of unsafe, I will stick with the stable Rust's interpretation of unsafe.

On the contrary, is it more acceptable if we use attributes as signals? It could be an attribute like #[require(unusal_override)] on the def-site auto impl Trait { .. } and an attribute like #[allow(unusual_override)] on the overriding auto impl in the downstream implementor. I am happy to take suggestions, including using the existing linting machinery.

As an example,

auto impl Subtrait: Supertrait {
    #[require(unusual_override)]
    auto impl Supertrait {
         type Target = Self;
    }
}

impl Subtrait for MyType {
    #[allow(unusual_override)] // <-- this is required
    auto impl Supertrait {
        type Target = Ref<MyType>;
    }
}




<details>
Copy link
Member

@programmerjake programmerjake Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another good hypothetical example which I think Rust should implement:
splitting ToOwned so you can use Cow as a reference-or-owned type even if you have an unclonable type:

pub trait AsOwned {
    type Owned: Borrow<Self>;
}

pub trait ToOwned: AsOwned {
    auto impl AsOwned;
    fn to_owned(&self) -> Self::Owned;
    fn clone_into(&self, target: &mut Self::Owned) {
        *target = self.to_owned();
    }
}

that way Cow can be:

pub enum Cow<'a, B: AsOwned + ?Sized> {
    Borrowed(&'a B),
    Owned(B::Owned),
}

impl<B: AsOwned + ?Sized> Deref for Cow<'_, B> {
    type Target = B;
    fn deref(&self) -> &Self::Target {
        match self {
            Self::Borrowed(v) => v,
            Self::Owned(v) => v.borrow(),
        }
    }
}

impl<B: ToOwned + ?Sized> Cow<'_, B> {
    pub fn into_owned(self) -> B::Owned {
        match self {
            Self::Borrowed(v) => v.to_owned(),
            Self::Owned(v) => v,
        }
    }
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll note that @scottmcm came up with a much better idea that doesn't need to split ToOwned -- essentially adding another type parameter to Cow for the owned type too. That said, the split could still be useful when you don't want to write out the owned type and use AsOwned::Owned but your borrowed type can't implement ToOwned.

}
```

However, this practice will not be encouraged eventually under provision of this RFC. For this reason, we also propose a future-compatibility lint, which will be escalated on a future Edition boundary to denial. The lint shall highlight the existing `auto impl` block in the subtrait definition and suggest an explicit `extern impl` statement in the subtrait implementation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious what the motivation is for making this a warning, and I'm not sure how we'd ever translate this into a hard error. Crates may want to add new auto impls for existing supertrait relationships. If we're able to detect and allow this via #[probe_extern_impl], I'm not convinced that this shouldn't be the default behavior.

That is, in order to support #[probe_extern_impl], we have to solve the impl detection problem. If we've successfully solved that problem, I think I would prefer that this be the default behavior: it's more ergonomic, and it avoids introducing unnecessary warnings into existing working code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @cramertj I thought I pressed comment button but it is still here as draft. Oh well...

The shortest path to success here is to invoke the trait solving machinery to tell us if an external candidate impl exists. However, it puts us under the mercy of the power of the trait solver and the effort to formulate its capability, what is expected to work and what is not, in this or a future RFC is insurmountable. Making it a default behaviour would probably force the compiler to work a lot harder.

I propose that we could just start an experiment early, to see if we can make it work; or whether we need to resort to a simpler test due to potential performance regression and any others to follow.

// and we deduce based on some applicability criterion ...
impl AutoMyTrait for Foo {} // <-- generates MyTrait

// Suppose this item is added at one point of time
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thankfully, the addition of a blanket impl is already a breaking change. Given that, we can check when deciding to implement a trait whether there exists another potentially-overlapping impl. Even if the blanket impl does not overlap, we should treat it as potentially-overlapping.

I believe this analysis can also be done with only a conservative "is it possible that these Self types may overlap"-style check, just as coherence does today. For example:

trait MyTrait: SuperTrait {
    auto impl SuperTrait;
};

struct MyType;
impl MyTrait for MyType {}

// Turns off `auto impl` for *all* types, regardless of whether the bound applies.
//
// Note that, because blanket impls are only allowed in the crate that defines the trait,
// this blanket impl must be in the same crate defining `SuperTrait`, so it is visible to
// the definition of `MyTrait`.
//
// This means that we could even make it a hard error to use `auto impl SuperTrait` for
// any `SuperTrait` that has a blanket impl for an uncovered type parameter `T`.
impl<T: ...> SuperTrait for T { ... }

// Turns off `auto impl` for any type that can may unify with `&mut T`.
impl<T: ...> SuperTrait for &mut T { ... }

// Turns off `auto impl` for any type that may unify with `MyType<T>`.
//
// Note that this is not *purely* a syntactic analysis, but requires a coherence-like check:
// If `MyType` is a normal ADT generic over `T`, this is only other `MyType` values, but
// if `MyType` is e.g. `type MyType<T> = T;`, then this is all types.
impl<T: ...> SuperTrait for MyType<T> { ... }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cramertj what is supposed to happen under your proposal in the following example?

trait SuperTrait {}
trait Foo: SuperTrait {
    auto impl SuperTrait;
}

trait Bar: SuperTrait {
    auto impl SuperTrait;
}

struct MyType<T>(T);
// should add `impl SuperTrait for MyType<u32>` if no other impl for `MyType<_>` exists
impl Foo for MyType<u32> {} 
// should add `impl SuperTrait for MyType<i32>` if no other impl for `MyType<_>` exists
impl Bar for MyType<i32> {}

Copy link
Member

@cramertj cramertj Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lcnr My expectation would be that the compiler would that MyType<i32> and MyType<u32> will never unify, so the parent auto impls will be generated for both, and the result will "just work." What am I missing?

This comment was marked as resolved.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This came out of a meeting with @joshtriplett and @tmandry. Implementing "do these impls overlap" via proper coherence checking is significantly harder than doing some self-type based fast reject.

Right now builtin auto-trait impls are disabled if there exists any impl with the outermost rigid constructor (e.g we disable all auto trait impls for MyType<T> if there's an impl for MyType<i32> cc rust-lang/rust#93367). Doing something similar here is kinda trivial from a type system pov

The more powerful "whether we emit an impl depends on whether coherence thinks impls overlap" is a lot harder as suddenly the while trait system needs to reason about "impls may or may not exist at this point". Supporting that requires a new TypingMode and refactorings to the way we iterate trait impls in the solver and so on. That has a significantly higher type system cost.

I personally would kinda prefer not doing proper trait solving:tm: to figure out whether to emit impls, but hmm. I guess I've become more open to make the type system harder more involved over time it's nice because the typing mode used to figure out whether to add these impls won't be soundness critical, so it's just annoying to impl

```

### Example: relaxed bounds via new supertraits
A common use case of supertraits is weaken bounds involved in associated items. There are occassions that a weakend supertrait could be useful. Suppose that we have a factory trait in the following example. In this example, the `async fn make` factory method could be weakened so that the future returned could be used in the context where the future is not required to be of `Send`. This has been enabled through the use of [the `trait_variant` crate](https://docs.rs/trait-variant/latest/trait_variant/).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It could be useful to specify concretely that this is a reference to the issues encountered by the tower::Service API: https://docs.rs/tower/latest/tower/trait.Service.html

@obi1kenobi
Copy link
Member

This is a very interesting proposal!

As maintainer of cargo-semver-checks, I'm thinking about the SemVer implications here. Due to the significant number of edge cases (associated types, associated functions, auto impl, extern impl, etc.), I'm quite sure I would fail if I attempted to enumerate all the possible SemVer hazards myself. At the same time, I think it's very important that SemVer hazards be considered explicitly — especially for a complex language feature like this.

Would you mind adding a ## SemVer hazards section to the doc and enumerating all the new ways to cause a major breaking change introduced here?

@dingxiangfei2009
Copy link
Author

@obi1kenobi Yes, I remember that. I will tag you in the "changelog" when I add the SemVer hazard section for your reviews.


It is still up for discussion.

## What is the SemVer implication?
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@obi1kenobi what do you think? Should we add any further consideration?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, the information provided here is not sufficient to evaluate this proposal's SemVer impact, and it is also not sufficient to write a SemVer reference entry nor to write an exhaustive set of lints in cargo-semver-checks for this feature.

We need to have more details, consider more edge cases, and ideally set up more structure in the section.

Example edge cases we should consider in detail:

  • The text considers introducing auto impl into an existing trait Trait. What are the implications if we remove auto impl from trait Trait, essentially undoing that addition?
  • When we auto impl a supertrait and supply default implementations for the supertrait's methods, presumably deleting such a default implementation is SemVer major? For example, this might still compile if the supertrait also had a default implementation, and our auto impl used to override that default impl with a new default impl.
  • Is adding or removing unsafe around unsafe auto impl or similar able to cause compilation failures in downstream code?
  • Is switching between extern impl and auto impl ever possibly breaking toward users of the type ultimately implementing such traits, or is this merely a syntactic difference?
  • Can adding or removing an item in a supertrait cause a downstream trait that uses auto impl to be broken?
  • Can renaming an item in a supertrait cause a downstream trait that uses auto impl to be broken? For example, if the new item name is the same / ambiguous with an existing item in the downstream trait?

Based on my skim through the doc, it would appear that at least some of these cases are at least SemVer hazards if not outright SemVer-major. It would be useful to have them enumerated in this section, so they can be considered specifically from a SemVer angle. This is because most of our RFCs consider language design in the point-in-time setting ("I gave the compiler some code and it did X") while SemVer is about language design across time ("my code used to compile fine, then I updated a dependency and now it doesn't") and thus requires a different mindset.


If the super trait is `unsafe`, then the `auto impl` declaration must also be `unsafe`.

## Naming ambiguity
Copy link
Contributor

@pthariensflame pthariensflame Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section discusses the behavior of a name clash between supertrait and subtrait, but what about between two supertraits? For example:

trait A {
  fn x();
}

trait B {
  fn x();
}

trait C: A + B {
  auto impl A;
  auto impl B;
}

impl C {
  fn x() {} // ?????
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add additional rule to cover this case.

The x item there should lead to ambiguity error and will be rejected.

- Clarify the rule on possible nesting
- Clarify the semver hazards
- Clarify the name resolution ambiguity from two supertraits

Signed-off-by: Xiangfei Ding <dingxiangfei2009@protonmail.ch>
@dingxiangfei2009
Copy link
Author

November update

  • I added a rule to clarify that nesting auto impls could happen and how the nesting would work.
  • I added a section on SemVer and explored the consequence from possible edits to traits using this feature. cc @obi1kenobi
  • I clarified the name resolution ambiguity when the name can be resolved to two supertrait items.

Signed-off-by: Xiangfei Ding <dingxiangfei2009@protonmail.ch>
Signed-off-by: Xiangfei Ding <dingxiangfei2009@protonmail.ch>
@dingxiangfei2009
Copy link
Author

Update:

  • I forgot to mention that we would like to support higher-kinded superbounds, too. The text is now updated with the extension. Extensions in the text would be rolled out gradually as the feature is developed.

Signed-off-by: Xiangfei Ding <dingxiangfei2009@protonmail.ch>
Copy link
Member

@obi1kenobi obi1kenobi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for not being able to take another look at this sooner. I went through the RFC from start to finish, and left my thoughts inline.

Unfortunately, I think the SemVer hazards section needs a lot more work.

Among other concerns, if I understand the semantics correctly, I believe this proposal takes several previously SemVer-minor changes and upgrades them to SemVer-major in an unexpected way. The implications of that are quite serious! We need to figure out if that's a deal-breaker, something that can be avoided with more design work, or an acceptable and regrettable casualty of shipping this desirable feature. In any case, I think the RFC needs to tackle that head-on.


### Example: relaxed bounds via new supertraits

A common use case of supertraits is weaken bounds involved in associated items. There are occassions that a weakend supertrait could be useful. Suppose that we have a factory trait in the following example. In this example, the `async fn make` factory method could be weakened so that the future returned could be used in the context where the future is not required to be of `Send`. This has been enabled through the use of [the `trait_variant` crate](https://docs.rs/trait-variant/latest/trait_variant/). The [`tower::Service`](https://docs.rs/tower/latest/tower/trait.Service.html) trait would benefit greatly from this proposal by having also the `!Send` bound for local service without major refactoring.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo fix:

Suggested change
A common use case of supertraits is weaken bounds involved in associated items. There are occassions that a weakend supertrait could be useful. Suppose that we have a factory trait in the following example. In this example, the `async fn make` factory method could be weakened so that the future returned could be used in the context where the future is not required to be of `Send`. This has been enabled through the use of [the `trait_variant` crate](https://docs.rs/trait-variant/latest/trait_variant/). The [`tower::Service`](https://docs.rs/tower/latest/tower/trait.Service.html) trait would benefit greatly from this proposal by having also the `!Send` bound for local service without major refactoring.
A common use case of supertraits is weaken bounds involved in associated items. There are occassions that a weakened supertrait could be useful. Suppose that we have a factory trait in the following example. In this example, the `async fn make` factory method could be weakened so that the future returned could be used in the context where the future is not required to be of `Send`. This has been enabled through the use of [the `trait_variant` crate](https://docs.rs/trait-variant/latest/trait_variant/). The [`tower::Service`](https://docs.rs/tower/latest/tower/trait.Service.html) trait would benefit greatly from this proposal by having also the `!Send` bound for local service without major refactoring.

fn cmp(&self, other: &Rhs) -> Ordering;
// ...
}
// This is one of the more probably implementation:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line seems to have a typo too; I'm not certain what it's referring to

Comment on lines +14 to +17
trait Supertrait2 {
type Type1;
type Type2;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To improve clarity, might I recommend moving this trait next to trait Subtrait2 instead?

Conceptually, we have two back-to-back examples, so to me it's an easier read if their setup isn't interleaved.

Comment on lines +26 to +29
auto impl Supertrait2 {
type Type1 = Self;
type Type2 = ();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking my understanding here:

  • auto impl OtherTrait; is this position always valid, and forces the downstream impl to include the items of OtherTrait
  • auto impl OtherTrait { ... } can supply implementations / values of a subset (or all) items in OtherTrait, with anything that isn't specified being left to the downstream impl to set instead

Is that correct?

It might be useful to include a high-level summary like this near the top of this doc, for purposes of bootstrapping some intuition for the reader. Otherwise we have to extrapolate from examples, which is tricky and doesn't guarantee we arrive at the same conclusions.

}
```

There are now two choices for type `X` in the downstream crate.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a forced choice, meaning that X downstream is broken unless it chooses one of these (new) choices?

AFAICT this is a major breaking change for the stdlib, because extern impl PartialOrd; appears to be required to support separate impl PartialOrd for X and impl Ord for X. If that's the case, it might be good to mention that how this would be addressed will be explained later, ideally in a named section that can have its anchor linked here.


### Addition and removal of `unsafe` qualifier on the `auto impl` directives

This is a SemVer hazard and mandates a major change. The implementors should inspect their implementation against the trait safety specification and add or remove safety comments accordingly. It is possible that the semantics of the API would change as the safety obligation can propagate through the API across multiple crate boundaries.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this section need updating w.r.t. the question about whether unsafe introduces or discharges safety obligations?

Comment on lines 1067 to 1069
### Change in the proper defintion of super- and sub-traits

This is a SemVer hazard and mandates a major change. The justification follows API change in trait irregardless of super- or sub-trait relationship. This scenario encompasses any changes in types, function signature, bounds, names.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on what "proper definition" means here?

This section is also insufficiently detailed in order to be converted into SemVer lints.


This is a SemVer hazard and mandates a major change. The justification follows API change in trait irregardless of super- or sub-trait relationship. This scenario encompasses any changes in types, function signature, bounds, names.

---
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there are many more SemVer edge cases to consider.

Here's one:

// in crate `upstream1`
pub trait Supertrait {
    type Item;
}

// in crate `upstream2`, which depends on `upstream1`
pub trait Subtrait {
    auto impl Supertrait;
}

Say crate downstream depends on upstream2 and has an impl upstream2::Subtrait for one of its types.

Imagine upstream2 adds a default value for the associated type:

// in crate `upstream2`, which depends on `upstream1`
pub trait Subtrait {
    auto impl Supertrait {
        type Item = u8;
    }
}

This seems non-breaking, right? Just a default — take it or leave it.

But imagine if separately (either before, concurrently, or after — it's a different crate so we don't get to mandate ordering!) upstream1 also added a default — a different one!

// in crate `upstream1`
pub trait Supertrait {
    type Item = i32;
}

Now it seems that the following code is

Crate downstream may have:

  • observed the new upstream1 version first
  • added code that depends on Supertrait::Item = u8 from upstream1 as a default

For example:

impl upstream2::Subtrait for MyType {
    auto impl Supertrait {}  // use the default `Item = u8`
}

Now downstream upgrades to the new version of upstream2, and MyType::Item is now i32 — a major breaking change!

Is this correct?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imagine upstream2 adds a default value for the associated type:

It is breaking change. Changes to a blanket impl is a breaking change, no?

Copy link
Author

@dingxiangfei2009 dingxiangfei2009 Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// in crate upstream2, which depends on upstream1

Also this will not compile. Supertrait has an item Item but your auto impl Supertrait block is empty.

Retract. I have not noticed that you are adding the default item to auto impl in trait block. There are a few errors in the example, but I let me give some edits so that we get going in the clean state.

// in crate `upstream1`
pub trait Supertrait {
    type Item;
}

// in crate `upstream2`, which depends on `upstream1`
pub trait Subtrait: Supertrait {
    auto impl Supertrait;
}

What happens now is that there is no "blanket implementation" to generate, precisely because it is empty. It only means that downstream implementation of Subtrait demands an implementation either through auto impl or extern impl in impl.

Now the next version we add a default item Item, this must generate a "blanket implementation." Therefore, it is still considered a breaking change because it introduces one "blanket implementation" here, that the downstream crate must make a choice because of one important reason:

The upstream crate may have introduced a safety requirement, with which the new upstream `auto impl` complies with, but with which the downstream `auto impl` does not necessarily comply.

In previous drafts, there were proposed mechanisms to enforce it but it was dropped. I think that from Semver hazard point of view, this should be considered.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is that part breaking? The assoc type was always there and used to be required. Any existing code must have specified it explicitly.

When the default is added, those existing uses are unchanged. Merely new uses are supported that don't set the assoc type.

I'd love a counterexample if you believe this is incorrect, either in the auto impl case or in the blanket impl case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// in crate upstream2, which depends on upstream1

Also this will not compile. Supertrait has an item Item but your auto impl Supertrait block is empty.

Could you point me to where the RFC specified this requirement? I must have missed it.

This is why your help is key in figuring out the edge cases. This is a complex feature, and you understand it 10x better than anyone else.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I was editing the response but I did not finish it on time. You may see the response when you refresh the page now.

that part breaking

I would like to point out that default associated types are unstable at the moment. See rust-lang/rust#29661. However, let us keep this rolling to see how far this goes.

I think we need to get the record straight. A lot of crates are flying around. Start with upstream1@1.0 and upstream2@1.0.

// in crate `upstream1@1.0`
pub trait Supertrait {
    type Item;
}

// in crate `upstream2@1.0`, which depends on `upstream1@1.0`
pub trait Subtrait: Supertrait {
    auto impl Supertrait;
}

What does a valid downstream@1.0 crate look like? It will necessarily look like this.

impl MyType for Subtriat {
    auto impl Supertrait {
        type Item = Something; // This item is definitely required
    }
}

Let us upgrade the upstream crates.

// in crate `upstream1@2.0`
pub trait Supertrait {
    type Item = u32;
}

// in crate `upstream2@2.0`, which depends on `upstream1@2.0`
pub trait Subtrait: Supertrait {
    auto impl Supertrait {
        type Item = ();
    }
}

Will there breaking change to downstream code at version 1.0 when both upstream crates are upgraded? I do not see one really.

This is a SemVer hazard and mandates a major change. The justification follows API change in trait irregardless of super- or sub-trait relationship. This scenario encompasses any changes in types, function signature, bounds, names.

---

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More broadly, I'd like the SemVer section to explicitly consider the SemVer outcomes of a subtrait that uses auto impl of a supertrait in cases like:

  • adding or removing items in the supertrait
  • adding or removing defaults for items in the supertrait (for assoc fns/methods, consts, assoc types)
  • adding or removing where bounds to items in the supertrait
  • adding or removing supertraits of the supertrait
  • adding or removing auto impl inside a supertrait X of a trait that itself uses auto impl X
  • the supertrait newly becoming dyn-safe, or newly becoming non-dyn-safe
  • if the supertrait supplies an opaque type for an assoc type default (type Item = impl Debug), whether repeating type Item = impl Debug in the auto impl results in the same opaque type or not
  • whether auto impl can be used if the supertrait is sealed — and whether there's a difference here between cases where the subtrait with the auto impl is in the same crate as the supertrait or in a downstream crate
  • if the supertrait is #[doc(hidden)]-sealed (i.e. "cannot be implemented downstream without referencing non-public #[doc(hidden)] APIs), whether then
    • auto impl of that trait in a subtrait constitutes a public API violation (unless e.g. the subtrait is #[doc(hidden)] itself, or some more complex rule), or
    • the subtrait is automatically considered non-public API as if it were #[doc(hidden)], which has more SemVer implications on its own

This is a very complex new language feature, and there may be additional cases on top of what I've named above. This is why your help is crucial here! I probably can't come up with all the edge cases as well as you can, and nailing down all the edge cases is critical for us to set up users of this feature for success — both by helping users write precisely the code they intended, and by building lints to protect them from accidental breakage.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please refer to #3851 (comment).

I think in your cargo-semver checker, the implementation should basically treat auto impl as if there is a blanket implementation. I believe you can find all the answer to the listed questions and possible others by applying this rule. Let me know if you find interesting edge cases!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reject this line of argument in the strongest terms.

Quite honestly, I find your reply insulting. Especially so given the amount of time and energy I've already invested in both cargo-semver-checks and in this RFC.

I've demonstrated in other comments that this is absolutely not "as if" equivalent to blanket implementations. It is disingenuous to claim this is trivial to work out just by starting with the rules for blanket implementations.

Even if it were, I feel it is not acceptable for a proposer of a new language feature to demand someone else work out its edge cases.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unfortunate that our conversation arrived at this state. I would like to clarify that I do not intend to downplay the importance of cargo-semver-checks and I have greatly and respectfully reviewed your comments insofar.

However, I have a hard time to understand exactly how cargo-semver-checks would consider, whether SemVer hazard would take place under which rules. I have attended to your talks and I remember that there have been many many rules built into this tool. I would like to seek your understanding that without a clear deduction formulation of SemVer hazards, language feature designers could find themselves paralysed and confused. Please understand that, given that we are at an early stage of exploration, I would prefer the RFC text to stay focused on the main language design. I am at the same time sympathetic towards the ecosystem tool designer and, therefore, I offered an option to simplify the the SemVer deduction by reducing it and modelling it after blanket implementation analogy. That is my suggestion and I am eager to hear if this model fails at any other edge cases.

For the time being, I am sad to say that I have not enough expertise to expand the SemVer hazard section to the extend that meets the rigor that cargo-semver-checks requires, not until I become the expert in the tool itself. Until then, I would still need your help and I have always appreciated it. 🙇

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I absolutely agree that collaboration is required, and I'm happy to be part of the solution. I can help both directly with the work itself and also by supporting language feature designers in picking up these skills too.

I agree that the RFC should stay focused on the language design — and SemVer is a key part of that design. The edge cases need to be worked out one way or the other, and the RFC is (in my opinion) the best time and place to work them out — before there's any implementation or test cases that need to change too, and definitely before we discover ex post facto that something has been a SemVer violation all along.

An example of such an unfortunate ex post facto SemVer hazard is that removing #[non_exhaustive] from an enum is a major breaking change in Rust today. This was not intended and is not desirable! But here we are, and now a tremendous amount of work is going into figuring out how to proceed.

If you are convinced, I'd be more than happy to walk you through an algorithm for demonstrating breakage that's sufficient for both writing down clear SemVer rules and for writing cargo-semver-checks lints.


This is a SemVer hazard and mandates a major change. The justification follows API change in trait irregardless of super- or sub-trait relationship. This scenario encompasses any changes in types, function signature, bounds, names.

---
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more example:

Adding defaulted items to a trait is usually non-breaking:

pub trait Super {
    // Adding this item is fine.
    fn example() -> i64 { 0 }
}

But say a subtrait contains two auto impl supertraits:

pub trait Subtrait {
    auto impl upstream::Super;
    auto impl other::Other;
}

What happens if other::Other already had an example function? (Or, analogously, if instead of assoc fns we had assoc types or assoc consts, of course.)

If I've understood the "Naming ambiguity" portion of the proposal correctly, this is now an ambiguity conflict and hard compile-time error:

If the sub-trait definition contains two auto impl directives and a sub-trait implementation has an item with a name that can be resolved to an associated item in both of the auto impl supertraits, irrespective of the associated item kind, then it must also be rejected as ambiguity.

So now adding a trait item with a default is newly SemVer-major as a result of this feature, while previously it was totally SemVer-safe! Oops!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a missing condition in the rejection and I am going to update it.

Basically, this upgrade should be SemVer-safe.

From

pub trait TraitA {}
pub trait TraitB {}
pub trait Subtrait: TraitA + TraitB {
    auto impl TraitA;
    auto impl TraitB;
    fn foo() -> u8 { 0 }
}

To

pub trait TraitA {
    fn foo() -> u8 { 0 }
}
pub trait TraitB {
    fn foo() -> u8 { 0 }
}
pub trait Subtrait: TraitA + TraitB {
    auto impl TraitA;
    auto impl TraitB;
    fn foo() -> u8 { 0 }
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might be talking past each other here.

Imagine TraitA and TraitB are in two different upstream crates, and neither has foo. Imagine that Subtrait is in a downstream crate, and does not have a foo item of its own.

Presumably, TraitA adding foo with a default implementation will not break Subtrait, right? It's a defaulted item: those are not breaking per current SemVer rules, and the auto impl is presumably not forced to re-state the default.

Then subsequently, does TraitB adding foo with a default implementation break Subtrait? Because now Subtrait::foo is ambiguous.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this against the latest clarification in 0c2ac34 and I believe the answer to my question above is "yes".

Based on that, adding any trait items regardless of the presence of a default value is newly a SemVer-major breaking change in this proposal.

The SemVer reference says that adding a defaulted trait item is generally safe, and lists an existing ambiguity-related error that is not generally considered SemVer major. That carve-out does not apply in this case:

  • In the carve-out's case, there is a fully-qualified syntax that the caller could have used to avoid being broken. This hits the allowance in the API evolution RFC1105 for a breaking change to not be major. No equivalent exists here.
  • In the carve-out's case, using the ambiguous item in question is required to suffer breakage — it's that use that could rely on fully-qualified syntax to avoid breakage. This case is the inverse: items that are not referenced in the subtrait using auto impl trigger ambiguity errors, so there is no place for disambiguating syntax to be added.

This is a major concern for this RFC. We've implicitly formed a functionality hierarchy here, and we should have ways to allow traits to self-select into the right place in that hierarchy:

  • sealed traits may add any items without SemVer breakage
  • unsealed traits that cannot be auto impl'd may add any items with default values without SemVer breakage
  • unsealed traits that can be auto impl'd cannot add any items at all without immediate SemVer-major changes

I think traits should have to opt into being used in auto impl, or at minimum have a way to opt out + have an edition migration where prior editions' traits are opted out by default.

Copy link
Author

@dingxiangfei2009 dingxiangfei2009 Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the wording is not perfect, so let me attach a worked example in 25597e7.

So, if I run through the example, no matter how many items with default implementation (actually I think we should call them provided methods?) are added into TraitA or TraitB, they are not considered for name resolution- one just cannot provide another implementation in their own trait definition nowadays.

So the wording should have been, name ambiguity only arises when in impl Subtrait there is an item foo that matches a required item trait TraitA and another in trait TraitB, while in trait Subtrait definition foo is missing.

pub trait TraitA {
    fn foo();
}
pub trait TraitB {
    fn foo();
}
pub trait Subtrait: TraitA + TraitB {
    auto impl TraitA;
    auto impl TraitB;
}
impl Subtrait for MyType {
    fn foo() {} //~ ERROR This is true name ambiguity.
}

And under no circumstances Subtrait::foo can be resolved given the above example. I do not intend to make any $name in Subtrait::$name to be resolved to any item not truly belonging to Subtrait. This is one invariant I do not wish to break.

Comment on lines +639 to +662
Then you might have a helper for implementing `EventHandler` for a specific event type:

```rs
enum MouseEvent {
ClickEvent(ClickEvent),
MoveEvent(MoveEvent),
}

trait MouseEventHandler: EventHandler {
auto impl EventHandler {
type Event = MouseEvent;
fn handle_event(&mut self, event: MouseEvent) {
use MouseEvent::*;
match event {
ClickEvent(evt) => self.click_event(evt),
MoveEvent(evt) => self.move_event(evt),
}
}
}

fn click_event(&mut self, event: ClickEvent);
fn move_event(&mut self, event: MoveEvent);
}
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an important use-case of a feature like this, but it shows an important flaw in the concrete design here: The MouseEventHandler helper thing is itself now also a trait that the PrintHandler below does publicly implement. If you imagine such implementations helpers may even come from a different crate than EventHandler itself, and yet-another crate ultimately uses it downstream for defining PrintHandler, then any change switching out the convenience/helper used to power the EventHandler impl would in and my itself also be a breaking change.

If someone comes along and writes a new crate with an alternative trait NicerMouseEventHandler: EventHandler { auto impl EventHandler … } that turns out to be much more convenient so use than MouseEventHandler was, you can only switch to this semver-compatibly by keeping the original dependency on the crate that offered the MouseEventHandler, then keep implementing that trait, but switch to extern impl EventHandler; then add the trait NicerMouseEventHandler in addition. Which is of course nonsensical since keeping the old impl around kills any convenience benefits that the new NicerMouseEventHandler could offer.

I think this is a fairly serious flaw for a feature that aims to improve the overall situation of what’s possible in semver-comparible API changes.

In my opinion, ideally, it should somehow be made possible to implement something like a trait, but that isn’t necessarily an actual trait itself, but merely functions like a template for implementing one (or several) other traits more conveniently.

It’s unfortunate that this seems unnecessary for the primary motivating use-case of updating true hierarchies where all traits involved and all methods are supposed to be public and stably available anyway, not mere implementation details of a kind of trait-to-trait adapter; but I’m 100% certain that a supertrait auto-impl like this one will be used for that kind of thing, no matter what, so a more complete solution design seems somewhat necessary IMHO.

So [whereas the default behavior of Ord cannot change in this regard] people will write things like a convenience trait EqOrd that automatically implements Ord, PartialOrd, Eq and PartialEq for you based on a single cmp. Or convenient ways to implement entire Num trait hierarchies from the num crate. And almost certainly also even more complicated and deep trait hierarchies & collections than this.

And whenever the helper traits powering this convenience aren’t private (which is especially the case whenever they come from a different crate) then people will either ignore those additional appearing trait public impls and method APIs that this effectively creates, and thus paint themselves into much higher than necessary semver-brittleness corner; or they do notice them, and that doesn’t solve anything but now they can also be annoyed while looking at those unwanted traits popping up in their docs, as a cost for the added convenience. Or they’ll start exploring all kinds of sophisticated ways to fight bad language design and get the compiler to somehow do the thing they really want after all. (A bit like the current hacky patterns to achieve something like “sealed traits”.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your comments! Let me open my comment with response to this concern.

I think this is a fairly serious flaw for a feature that aims to improve the overall situation of what’s possible in semver-comparible API changes.

So I do not believe that improving the semver-compatibility is a goal of this proposal and I do not try to sell this proposal as a general solution to the problem.

One of the motivations of the proposal is to provide a more user friendly and flexible alternative to blanket implementations involving supertraits without the problems coming from them such as potential overlapping implementations and inability to override those blanket implementations. As illustration, let us use the EventHandler and see what auto impl offers and does not offer.

Without auto impl, the way to supply default implementation is to introduce a blanket implementation.

// CRATE A
trait MouseEventHandler: EventHandler {
    fn click_event(&mut self, event: ClickEvent);
    fn move_event(&mut self, event: MoveEvent);
}

impl<T: MouseEventHandler> EventHandler for T { // (*)
	type Event = MouseEvent;
	fn handle_event(&mut self, event: MouseEvent) {
		use MouseEvent::*;
		match event {
			ClickEvent(evt) => self.click_event(evt),
			MoveEvent(evt) => self.move_event(evt),
		}
	}
}

// CRATE B
trait NicerMouseEventHandler: MouseEventHandler { .. }

impl<T: MouseEventHandler> NicerMouseEventHandler for T { .. }

Now with auto impl as described in the RFC, for each impl MouseEventHandler for MyType for some MyType there will be an impl EventHandler for MyType that is templated after the auto impl block. The blanket implementation, as we know that comes with quite some drawbacks, is not necessary.

What I advocate strongly is that auto impl should still be treated conceptually as a blanket implementation with some modification. To think about it, we should ask ourselves, when the blanket implementation (*) in Crate A changes, by means of changes to the associated type or changes to implementation details of handle_event even, do we introduce SemVer breaking change? Please correct me if I am wrong, I think it is definitely a SemVer breaking change hazard. It could be overarching to say that any implementations with generics, blanket implementations included, will induce SemVer hazards when they are changed even in implementation details, but it rule sounds reasonable to me. cc @obi1kenobi

And I don't think this view should be altered when auto impl is morally a blanket implementation, so questions about SemVer hazards should be resolved by treating them as blanket implementations and answered in that domain.

If you imagine such implementations helpers may even come from a different crate than EventHandler itself, and yet-another crate ultimately uses it downstream for defining PrintHandler, then any change switching out the convenience/helper used to power the EventHandler impl would in and my itself also be a breaking change.

So, yes, following this train of thought these are all breaking changes.

In my opinion, ideally, it should somehow be made possible to implement something like a trait, but that isn’t necessarily an actual trait itself, but merely functions like a template for implementing one (or several) other traits more conveniently.

It’s unfortunate that this seems unnecessary for the primary motivating use-case of updating true hierarchies where all traits involved and all methods are supposed to be public and stably available anyway, not mere implementation details of a kind of trait-to-trait adapter; but I’m 100% certain that a supertrait auto-impl like this one will be used for that kind of thing, no matter what, so a more complete solution design seems somewhat necessary IMHO.

I still do not quite get what you are getting to and I am curious about your ideas. Would you mind elaborating? A few sketches will help.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I do not believe that improving the semver-compatibility is a goal of this proposal and I do not try to sell this proposal as a general solution to the problem.

This raises the obvious question: should it be a goal?

I believe we should explore what it would take to get to "yes".

Blanket impls are full of surprising SemVer hazards today. This proposal in its current state would make things even more challenging. It would be the wrong tradeoff, in my opinion, to address one problem by introducing a harder one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still do not quite get what you are getting to and I am curious about your ideas. Would you mind elaborating? A few sketches will help.

Sure, let me elaborate: Let’s say we have this kind of boiler-platy list of implementations:

struct Foo(u8);
impl Ord for Foo {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.0.cmp(&other.0)
    }
}
impl PartialOrd for Foo {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}
impl Eq for Foo {}
impl PartialEq for Foo {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}
impl std::hash::Hash for Foo {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.0.hash(state);
    }
}

Currently, one could simplify this with macros. Probably truly proc-macros would be the most convenient for users, but let's leave it as a simple macro_rules macro for the purposes of this demo, e.g.:

// a general helper crate for Ord and Hash impls

mod convenient_impl_helpers {

    #[macro_export]
    macro_rules! eq_ord_by_key {
        ($(@[$($TyArgs:tt)*] $(where [$($Bounds:tt)*])?)? $T:ty, |$x:ident| $key:expr) => {
            impl$(<$($TyArgs)*>)? Ord for $T $($(
                where $($Bounds)*
            )?)?
            {
                fn cmp(&self, other: &Self) -> std::cmp::Ordering {
                    let $x = self;
                    let key1 = &$key;
                    let $x = other;
                    let key2 = &$key;
                    Ord::cmp(key1, key2)
                }
            }
            impl$(<$($TyArgs)*>)? PartialOrd for $T $($(
                where $($Bounds)*
            )?)?
            {
                fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
                    Some(self.cmp(other))
                }
            }

            impl$(<$($TyArgs)*>)? Eq for $T $($(
                where $($Bounds)*
            )?)? {}
            impl$(<$($TyArgs)*>)? PartialEq for $T $($(
                where $($Bounds)*
            )?)?
            {
                fn eq(&self, other: &Self) -> bool {
                    let $x = self;
                    let key1 = &$key;
                    let $x = other;
                    let key2 = &$key;
                    *key1 == *key2
                }
            }
        }
    }

    #[macro_export]
    macro_rules! hash_by_key {
        ($(@[$($TyArgs:tt)*] $(where [$($Bounds:tt)*])?)? $T:ty, |$x:ident| $key:expr) => {
            impl$(<$($TyArgs)*>)? std::hash::Hash for $T $($(
                where $($Bounds)*
            )?)?
            {
                fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
                    let $x = self;
                    let key = &$key;
                    std::hash::Hash::hash(key, state);
                }
            }
        }
    }
}
// let's use that nice helper crate someone else (ideally) has already written now!

struct Foo2(u8);

eq_ord_by_key!(Foo2, |x| x.0);
hash_by_key!(Foo2, |x| x.0);

(playground)

Macros are kind of annoying to use though. Special syntax and visibility rules for macro_rules, or crate separation for proc macros, macro_rules is also annoying with generic parameters (since < > are not true parentheses)… of course these points aren’t completely on-topic and most are fixable in the long run… last but not least though, a macro isn’t type-checked! Its like a C++ template but even worse; you don’t get any errors until you actually use it, not only for type / resolution errors but also basic syntax errors.

So generally traits are Rust’s solution for nicer metaprogramming like this, with maximal type-checking at definition time, no weird syntax etc… Hence naturally this RFC’s features would definitely be used by people to improve on macros such as the one presented above. So now, your helper crate author can rewrite their macro into (this much nicer code):

// a general helper crate for Ord and Hash impls
mod convenient_impl_helpers {

    pub trait EqOrdByKey: Ord {
        fn key(&self) -> &impl Ord;
        auto impl Ord {
            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
                self.key().cmp(other.key())
            }
        }
        auto impl PartialOrd {
            fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
                Some(self.cmp(other))
            }
        }
        auto impl Eq {}
        auto impl PartialEq {
            fn eq(&self, other: &Self) -> bool {
                self.key() == other.key()
            }
        }
    }

    pub trait HashByKey: Hash {
        fn key(&self) -> &impl Hash;
        auto impl std::hash::Hash {
            fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
                self.key().hash(state);
            }
        }
    }
}

and as the user we can write the following to use it:

struct Foo3(u8);

impl EqOrdByKey for Foo3 {
    fn key(&self) -> &impl Ord {
        &self.0
    }
}
impl HashByKey for Foo3 {
    fn key(&self) -> &impl Hash {
        &self.0
    }
}

To demonstrate the semver point in this example: Let’s say we now realize, Foo3 should also offer a Borrow<u8> implementation, so let’s just add it:

impl Borrow<u8> for Foo3 {
    fn borrow(&self) -> &u8 {
        &self.0
    }
}

and by now it’s starting to become boilerplaty again.

We find out there’s different helper crate that’s offering exactly what we need for handling our use-case more cleanly. WIth the different helper crate, we can simply write:

struct Foo3(u8);
impl BorrowOrdHash<u8> for Foo3 {
    fn borrow(&self) -> &u8 {
        &self.0
    }
}

and it’ll handle the rest for us!

Implementation of that “different helper crate”…
// a different helper crate

mod borrow_delegate_convenience {
    pub trait BorrowOrdHash<T: Ord + Hash>: Borrow<T> + Ord + Hash {
        fn borrow(&self) -> &T;
        auto impl Ord {
            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
                self.borrow().cmp(other.borrow())
            }
        }
        auto impl PartialOrd {
            fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
                Some(self.cmp(other))
            }
        }
        auto impl Eq {}
        auto impl PartialEq {
            fn eq(&self, other: &Self) -> bool {
                self.borrow() == other.borrow()
            }
        }
        auto impl std::hash::Hash {
            fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
                self.borrow().hash(state);
            }
        }
        auto impl std::borrow::Borrow<T> {
            fn borrow(&self) -> &T {
                BorrowOrdHash::borrow(self)
            }
        }
    }
}

This is all very nice and clean and convenient and people will use it this way. But compared to the macro-based approach, it results in publicly visible, yet unwanted, implementations of these traits like EqOrdByKey, HashByKey or BorrowOrdHash; and switching to the more convenient not-a-macro-based solution from borrow_delegate_convenience additionally comprised a breaking API change on Foo3 that’s annoyingly also only due to the API of those publicly visible implementation of EqOrdByKey, HashByKey that we didn’t really want in the first place.

The idea I mentioned of having “something like a trait” is to make these helper things, EqOrdByKey, HashByKey, BorrowOrdHash, into things that are like partially like a trait in that you can write an impl for them; but explicitly they’re not like a trait in that you can not:

  • call any of their methods yourself – the methods (key, or borrow) only serve as input for the impl, not as additional API that is supposed to be made available
  • use their bound in any constraints: i.e. you cannot write T: EqOrdByKey anywhere – this way implementations of these not-a-trait things can be removed without any breakage.

As one possible way to implement this let me just introduce a new keyword, call these things something like template trait and keep the rest of the syntax like in this RFC, and we can just change it into:

pub template trait BorrowOrdHash<T: Ord + Hash>: Borrow<T> + Ord + Hash {
    fn borrow(&self) -> &T;
    auto impl Ord {
        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
            self.borrow().cmp(other.borrow())
        }
    }
    auto impl PartialOrd {
        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
            Some(self.cmp(other))
        }
    }
    auto impl Eq {}
    auto impl PartialEq {
        fn eq(&self, other: &Self) -> bool {
            self.borrow() == other.borrow()
        }
    }
    auto impl std::hash::Hash {
        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
            self.borrow().hash(state);
        }
    }
    auto impl std::borrow::Borrow<T> {
        fn borrow(&self) -> &T {
            BorrowOrdHash::borrow(self)
        }
    }
}

and we’d use it still like before

struct Foo4(u8);
impl BorrowOrdHash<u8> for Foo3 {
    fn borrow(&self) -> &u8 {
        &self.0
    }
}

but Foo4 does not visibly implement BorrowOrdHash now because BorrowOrdHash isn’t actually a trait; just Borrow<u8> and Ord and Hash.

This kind of feature is of course not really needed for the concrete motivation of refactoring something like std::fmt::Read – or, one other thing I have in mind is refactoring (eventually) Iterator into a special case of a generic “lending iterator” kind of trait.


Here’s a fun (or “interesting”?) follow-up, especially given your RFC already mentioned blanket impls for comparison: this kind of feature for refactoring traits into a hierarchy may potentially even go hand-in-hand with a blanket implementation but the other way around. That is: Assume besides Iterator { type Item; } we gain LendingIterator { type LendingItem<'lt>; }, then any lending iterator where the LendingItem type does not depend on 'lt can in principle always be used as an Iterator, and this fact/implication can become a blanket impl, like

impl<T, Itm> Iterator for T
where
    for<'lt> T: LendingIterator<LendingItem<'lt> = Itm>,
{
    type Item = Itm;
    // methods left out for brevity
}

though this also only works if the RFC is somehow extended… significantly… essentially:

  • first, we need to allow something like the template trait mentioned above
    template trait IteratorTempl {
        type Item;
        // methods left out for brevity
        auto impl LendingIterator {
            type LendingItem<'_lt> = Item;
            // methods left out for brevity
        }
    }
    and then whoever writes impl IteratorTempl for MyType actually implements LendingIterator through the template; then the blanket impl in turn gives you a MyType: Iterator impl.
    • for backwards compatibility then, next, we need a way to allow IteratorTempl to be re-exported under the same name Iterator as the Iterator trait itself. (Maybe literalls pub use LendingIterator as Iterator into the same module as the real trait Iterator). To avoid ambiguity, maybe Iterator can become a language-supported sealed trait; then it’s going to be clear that you would never write a downstream impl for that, and we could say there is never ambiguity between a sealed trait and a template trait because they are used downstream for mutually-exclusive purposes (the latter only for writing impls, the former for everything else).
    • to minimize confusion, this construct of fusing together a sealed trait and a template trait into a single public-API name could come with a compiler-enforced limitation to cases where the template trait does result (either directly or through blanket impls) in an actual implementation of the crate whose name it shares. Though this would preclude the possibility for opt-out of some of the auto impls. Maybe, as a minimal change, the syntax solution could be to make the auto keyword optional, and allow the extern opt-out only when auto was present. Alternatively the final keyword, like for final methods (RFC 3678), could be considered.

In case you wonder how I’m coming up with these thoughts on-the-fly: I don’t. It’s rather that I have already myself, occasionally, spent some time thinking thought this general problem space for a decent while already… let me check… it apparently it has been ~4 years already 😲!


If you’ll allow me, let me share just two more thoughts I found worth mentioning (and by all means, please feel free to move the conversation elsewhere; separate review thread; or Zulip; etc; if you want to continue any of the more tangential discussion points):

[Click to expand.]
  • AFAICT, the RFC limits the auto impl connection to cases where the Self type is shared between the outer and the auto impl’d trait, while allowing more freedom with other trait parameters. As someone who came into Rust from Haskell, which has no concept of Self and just treats all parameters to type classes the same, I’m always in the camp of viewing “generic traits” rather just as “relations of more than 1 argument”, and would love to minimize the special treatment of Self parameters in traits
    • this is however a thing that can be left out of an MVP, as it doesn’t really suffer the property of “annoying limitation that leads people to write more brittle APIs”, but it’s only an “annoying limitation” which can be lifted at a later point. It may be worth considering though as future possibility and with this possibility in mind, possibly the syntax should be auto impl Trait for Self { … }, not just auto impl Trait { … } [thogh I’m not sure yet what I’d prefer; maybe keep the latter as sugar, anyway?]
  • another possible concern [and touching on sort-of a counter-point to the above] is user error messages from concrete auto trait powered impls that turn out illegal for coherence-rules kind of reasons; there are a few error cases with this that only appear once the auto impl is actually used.
    • Of course the possibility for overlapping impl error messages that stem from the auto impl, not the top-level one; those are nothing new though, you can also get these from blanket impls.

    • A unique/new concern however is from orphan rules. To show a nontrivial example: if you have this:

      // methods left out for brevity
      trait Foo<A, B> {
          auto impl Bar<B, A> {
              // ...
          }
      }
      trait Bar<B, A> {}

      then a downstream user might write an impl as follows

      struct MyStruct {}
      impl<T> Foo<MyStruct, T> for String {}

      this would then run into orphan rules violation not for Foo but one for Bar (due to the order of the parameters) from the auto impl (to see the kind of error message, see this playground)
      This isn’t a huge concern, but it’s at least worth considering. If some of these possible “template instantiation”-time-ish errors are deemed potentially-too-confusing1, we could consider whether it’s possible and desirable to defined additional limitations on the auto impls that somehow prevent such orphan-rules issues.

Footnotes

  1. In particular, I could imagine that leaving this unaccounted for could allow users to refactor traits in ways that are breaking changes even when they weren’t supposed to be breaking, and where catching the breaking use-case is sufficiently nontrivial that it’s easily overlooked by people not 100% fluent with the last details of Rust’s orphan/coherence rules.

Copy link
Member

@obi1kenobi obi1kenobi Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea I mentioned of having “something like a trait” is to make these helper things, EqOrdByKey, HashByKey, BorrowOrdHash, into things that are like partially like a trait in that you can write an impl for them; but explicitly they’re not like a trait in that you can not:

  • call any of their methods yourself – the methods (key, or borrow) only serve as input for the impl, not as additional API that is supposed to be made available
  • use their bound in any constraints: i.e. you cannot write T: EqOrdByKey anywhere – this way implementations of these not-a-trait things can be removed without any breakage.

I also came to this conclusion. The ability to name (in bounds etc.) the intermediate helper trait featuring auto impl seems to open quite the SemVer can of worms.

A unique/new concern however is from orphan rules.

Oh, this is an excellent point. This is also a major SemVer hazard that IMHO needs to be discussed in the RFC, at least as a "this is an open question requiring more consideration prior to stabilization".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replying to @steffahn ...

On template trait

I like the template trait idea honestly. I have one question. In the BorrowOrdHash we have apparently the item borrow syntatically associated to it. BorrowOrdHash is not a trait, so borrow is an inherent method to the Self type? What is the implication to the method resolution?

On the overall topic, I wonder this can be solved by existing unstable features like rust-lang/rust#41517.

It is also a matter of perspective. To me trait is a contract and a set of invariants. Following this strict interpretation, which I recommend every user to follow, no two traits are SemVer compatible as soon as there is a mismatch in the crate identity or version, no matter how void the trait content would be. Moving from EqOrdByKey + HashByKey to BorrowOrdHash is a breaking change and I will not have a second thought when I bump my crate version.

However, this is a common pattern in ecosystem to have a bag of trait bounds and give it a name. What they want is have a way to write a trait bound succinctly instead of writing 42 trait bounds connect with + in their where clauses everywhere. For that we already have answer, which is trait alias. If one really wants a bag of traits, we should make it work.

On that note, template trait can fill a gap between trait alias + bag of impls and auto impl, on the point of introducing SemVer hazard. I would say SemVer hazard is not necessarily a bad thing, it is good by a lot. It is rather a bad idea that any type that implements this bag of traits Eq/Ord/... could be used to call my API. That would mean that I loose my control over what types are safe to use for my library, because the user could call my library without generating the right impls through (proc-)macros or writing the right impls because they never read trait documentation.

So in my opinion when a trait is really so void of trait characteristics and regresses to just a bag of trait impls without strings attached, I would recommend to make trait alias work, which I am interested in also because of its important niche, and let our users use it. To throw right tools at the problem basically.


On orphan rules

auto impl should not allow any changes to trait solving or trait selection at this point, this is agreed upon. On the point of orphan rules, auto impl does not intend to make impl<T> Foo<MyStruct, T> for String {} compile-what a blanket implementation cannot do, should auto impl will not enable either. If we really need to be careful, I think this is about good diagnostic to explain why orphan rule is violated at which impl.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

I-lang-radar Items that are on lang's radar and will need eventual work or consideration. T-lang Relevant to the language team, which will review and decide on the RFC. T-types Relevant to the types team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.