Skip to content

Inconsistent behavior when using associated type bounds in super trait with constraints #153002

Open

Description

@95th

I tried this code:

pub trait A {
    type T: A<T = Self::T>;
}

pub trait B: A<T: B> {}

and it gives this error (similar to #65913):

error[E0391]: cycle detected when computing the implied predicates of `B`
 --> src/lib.rs:5:19
  |
5 | pub trait B: A<T: B> {}
  |                   ^
  |
  = note: ...which immediately requires computing the implied predicates of `B` again
note: cycle used when computing normalized predicates of `B`
 --> src/lib.rs:5:1
  |
5 | pub trait B: A<T: B> {}
  | ^^^^^^^^^^^^^^^^^^^^
  = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

As per @Centril comment here: #65913 (comment)
if I try the equivalent version, it also fails with the same error:

pub trait B
where
    Self: A<T: B>
{}

but If I tried the further equivalent version, it works:

pub trait B
where
    Self: A,
    <Self as A>::T: B
{}

I expected to see the same error as the first two variants.

Instead, this happened: it compiles.

Similarly, this works:

pub trait B: A
where
    Self::T: B
{}

Rust playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=8b0c5f1f71410a0feba69462f4e6d915

Meta

rustc --version --verbose:

rustc 1.93.1 (01f6ddf75 2026-02-11)
binary: rustc
commit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf
commit-date: 2026-02-11
host: aarch64-apple-darwin
release: 1.93.1
LLVM version: 21.1.8
Backtrace

<backtrace>

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Feb 23, 2026
  2. theemathas commented on Feb 23, 2026

    @theemathas
    Contributor

    The two syntaxes seem to have different behavior elsewhere, too. For example:

    trait Bound {}
    
    trait Super {
        type Assoc;
    }
    
    trait Sub1: Super<Assoc: Bound> {}
    trait Sub2: Super where Self::Assoc: Bound {}
    
    // The compiles
    fn works<T: Sub1<Assoc = i32>>() {}
    // This errors
    fn fails<T: Sub2<Assoc = i32>>() {}
    error[E0277]: the trait bound `i32: Bound` is not satisfied
      --> src/lib.rs:13:13
       |
    13 | fn fails<T: Sub2<Assoc = i32>>() {}
       |             ^^^^^^^^^^^^^^^^^ the trait `Bound` is not implemented for `i32`
       |
    help: this trait has no implementations, consider adding one
      --> src/lib.rs:1:1
       |
     1 | trait Bound {}
       | ^^^^^^^^^^^
    note: required by a bound in `Sub2`
      --> src/lib.rs:8:38
       |
     8 | trait Sub2: Super where Self::Assoc: Bound {}
       |                                      ^^^^^ required by this bound in `Sub2`
    
    For more information about this error, try `rustc --explain E0277`.
    
  3. added
    A-associated-itemsArea: Associated items (types, constants & functions)
    T-langRelevant to the language team
    T-typesRelevant to the types team, which will review and decide on the PR/issue.
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Feb 23, 2026
  4. theemathas commented on Feb 23, 2026

    @theemathas
    Contributor

    Another weirdness:

    trait Bound {}
    
    trait Super {
        type Assoc;
    }
    
    trait Sub1: Super<Assoc: Bound> {}
    trait Sub2: Super where Self::Assoc: Bound {}
    
    // This compiles
    fn works<T: Sub1>() {}
    // This errors
    fn fails<T: Sub2>() {}
    error[E0277]: the trait bound `<T as Super>::Assoc: Bound` is not satisfied
      --> src/lib.rs:13:13
       |
    13 | fn fails<T: Sub2>() {}
       |             ^^^^ the trait `Bound` is not implemented for `<T as Super>::Assoc`
       |
    note: required by a bound in `Sub2`
      --> src/lib.rs:8:38
       |
     8 | trait Sub2: Super where Self::Assoc: Bound {}
       |                                      ^^^^^ required by this bound in `Sub2`
    help: consider further restricting the associated type
       |
    13 | fn fails<T: Sub2>() where <T as Super>::Assoc: Bound {}
       |                     ++++++++++++++++++++++++++++++++
    
    For more information about this error, try `rustc --explain E0277`.
    
  5. theemathas commented on Feb 23, 2026

    @theemathas
    Contributor

    Possibly related: #56003

  6. theemathas commented on Feb 23, 2026

    @theemathas
    Contributor

    Another weirdness:

    trait Bound {}
    trait Super {
        type Assoc;
    }
    trait Sub: Super where Self::Assoc: Bound {
        fn method(&self) {}
    }
    
    struct Thing;
    impl Bound for i32 {}
    impl Super for Thing {
        type Assoc = i32;
    }
    impl Sub for Thing {}
    
    fn make() -> impl Sub {
        Thing
    }
    
    fn works() {
        let _ = &make() as &dyn Sub<Assoc = _>;
    }
    
    fn fails() {
        make().method();
    }
    error[E0277]: the trait bound `<impl Sub as Super>::Assoc: Bound` is not satisfied
      --> src/lib.rs:25:12
       |
    25 |     make().method();
       |            ^^^^^^ the trait `Bound` is not implemented for `<impl Sub as Super>::Assoc`
       |
    help: the trait `Bound` is implemented for `i32`
      --> src/lib.rs:10:1
       |
    10 | impl Bound for i32 {}
       | ^^^^^^^^^^^^^^^^^^
    note: required by a bound in `Sub::method`
      --> src/lib.rs:5:37
       |
     5 | trait Sub: Super where Self::Assoc: Bound {
       |                                     ^^^^^ required by this bound in `Sub::method`
     6 |     fn method(&self) {}
       |        ------ required by a bound in this associated function
    
    For more information about this error, try `rustc --explain E0277`.
    
  7. RishabhRD commented on Feb 23, 2026

    @RishabhRD

    As there is inconsistent behavior, this is a good time to converge to a behavior which would be better for generic programming. We need to take best of both behaviors.

    Currently, inline constraints automatically infer the constraint for associated type.

    pub trait Base {
      type T
    }
    
    pub trait Refined: Base<T: Ord> {
    }
    
    fn f<C: Refined>(c: C) {
       // automatically derives C::T is Ord
    }

    However, with where version I explicitly need to mention it:

    pub trait Refined2: Base
        where Self::T: Ord
    {
    }
    
    fn g<C: Refined2>(c: C) 
         where C::T: Ord  // need to put it explicitly to compile for no reason.
    {
    }

    There is no reason, compiler can't infer the constraint automatically.

    On the other hand, where version can handle the recursive constraints but inline constraints can't:

    pub trait Refined3: Base
       where Self::T: Refined3
    {
    }
    
    pub trait Refined4: Base<T: Refined4> // doesn't compile :( with error saying cycle in constraints detected
    {}

    Handling these cases would be a big win for generic programming :)

  8. QuineDot commented on Feb 23, 2026

    @QuineDot
    trait Sub1: Super<Assoc: Bound> {}
    trait Sub2: Super where Self::Assoc: Bound {}
    
    // This compiles
    fn works<T: Sub1>() {}
    // This errors
    fn fails<T: Sub2>() {}

    This case is about supertrait elaboration / implied bounds, and these two cases should not have the same behavior (without opting in to it).

    Consider three scenarios:

    1. You have an elaborated bound (like Sub1)
    2. You have a non-elaborated bound (like Sub2)
    3. You don't have a bound

    It is generally a non-breaking change to go from 2 to 1 or from 2 to 3. But it is a breaking change to go from 1 to 2 or 3, and it is a breaking change to go from 3 to 1 or 2.

    So if you make Sub2 act like Sub1, you will be removing the ability of trait authors to loosen their bounds without breaking downstream. Or in other words, trait authors should have the ability to opt-in (or opt-out) of implied bounds.

    Associated type bounds were intentionally made implied; see the links in this stabilization PR.


    AFAIK the only way an author can choose between having bounds be implied or not today is to arrange to have the choice between using an associated type bound or using a bound on an associated type projection. It would be nice to broaden the ability to choose and to make it more explicit. But the choice shouldn't be taken away.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-associated-itemsArea: Associated items (types, constants & functions)A-trait-systemArea: Trait systemC-bugCategory: This is a bug.T-langRelevant to the language teamT-typesRelevant to the types team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions