Skip to content

super trait bounds can result in unconstrained regions #136547

Open

Description

@lcnr
trait Super {
    type SAssoc;
}
trait Trait<'a>: Super<SAssoc = <Self as Trait<'a>>::TAssoc> {
    type TAssoc;
}
fn hr_where_bound<T: for<'a> Trait<'a>>() {}

T: for<'a> Trait<'a>> currently elaborates to T: for<'a> Super<SAssoc = <T as Trait<'a>>::TAssoc>. THe projection clause uses 'a in the term without constraining it in the alias. Manually writing such a where-clause results in an error: https://rust.godbolt.org/z/d8G6GvMbe

error[E0582]: binding for associated type `SAssoc` references lifetime `'a`, which does not appear in the trait input types
  --> <source>:10:22
   |
10 |     T: for<'a> Super<SAssoc = <T as Trait<'a>>::TAssoc>,
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

One can still implement and use Trait as long as TAssoc does not mention 'a. People use such super trait bounds in the wild, as discovered by a crater run in #136303.

Supporting unconstrained regions is difficult as it causes normalization of <T as SUper>::SAssoc to use a different unconstrained region variable each time. This can prevent us from proving type outlive bounds, as they rely on region equality when applying assumptions:

fn test<'a, T: 'a>() {}
fn unconstrained_lt1<'arg, T: for<'a> Trait<'a>>(x: &'arg <T as Super>::SAssoc) {
    test::<'arg, <T as Super>::SAssoc>();
    //~^ ERROR `<T as Trait<'_>>::TAssoc` does not live long enough
}


// Has an implied `<T as Super>::SAssoc: 'arg` bound.
struct Struct<'arg, T: Super>(&'arg <T as Super>::SAssoc);
fn unconstrained_lt2<'arg, T: for<'a> Trait<'a>>(x: &'arg <T as Super>::SAssoc) {
    let _ = Struct::<T>(x);
    //~^ ERROR `<T as Trait<'_>>::TAssoc` does not live long enough
}

I've discussed this issue with @compiler-errors and believe that we need to keep supporting such code as it's 'reasonable' and forbidding it results in significant crater breakage.

Our current idea is to encode "One can still implement and use Trait as long as TAssoc does not mention 'a" in the type system by adding support for bivariant region parameters on associated types. We would then change 'a to be bivariant in <T as Trait<'a>>::TAssoc and ignore it when checking outlives bounds. We'd still get unconstrained regions this way, but they should no longer cause issues.

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Feb 4, 2025
  2. added
    C-enhancementCategory: An issue proposing an enhancement or a PR with one.
    T-typesRelevant to the types team, which will review and decide on the PR/issue.
    A-associated-itemsArea: Associated items (types, constants & functions)
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Feb 4, 2025
  3. pinned this issue on Feb 4, 2025
  4. unpinned this issue on Feb 4, 2025
  5. aliemjay commented on Feb 5, 2025

    @aliemjay
    Contributor

    I’ve encountered this issue before with implied bounds and was fairly certain that I had opened a ticket for it, but I couldn’t find one when checked.

    Our current idea is to encode "One can still implement and use Trait as long as TAssoc does not mention 'a" in the type system by adding support for bivariant region parameters on associated types. We would then change 'a to be bivariant in <T as Trait<'a>>::TAssoc and ignore it when checking outlives bounds.

    The issue is independent of TAssoc:

    trait Super { type SAssoc; }
    trait Trait<'a>: Super<SAssoc = &'a ()> {}
    fn hr_where_bound<T: for<'a> Trait<'a>>() {}

    How the proposed solution deals with the code above?

  6. lcnr commented on Feb 5, 2025

    @lcnr
    ContributorAuthor

    How the proposed solution deals with the code above?

    Good point. There are two ways to implement trait here

    impl<'a> Super for &'a () {
        type SAssoc = &'a ();
    }
    impl<'a> Trait<'a> for &'a () {}
    
    impl Super for () {
        type SAssoc = &'static ();
    }
    impl Trait<'static> for () {}

    With both of these impls T: for<'a> Trait<'a> is unsatisfiable. We can probably not do anything here and just accept the broken type outlives constraints. I don't think such unconstrained regions are unsound, they are just difficult to support correctly. The reason this is a problem right now is that it makes it significantly more difficult to compute implied bounds outside of mir borrowck, as we rely on the normalized signature being used by the implied bounds computation being the same as the normalized signature used in borrowck.

    We could check that elaboration does not result in unconstrained regions outside of aliases? Checking that elaboration does not result in unconstrained regions being mentioned in the term at all also breaks the following code as the foo where-clauses elaborates to for<'a> T: FnOnce<I::Item<'a>> -> <Self as FnOnceDup<I::Item<'a>>>::Output:

    trait LendingIterator {
        type Item<'a>;
    }
    
    trait FnOnceDup<Arg>: FnOnce(Arg) -> <Self as FnOnceDup<Arg>>::Output {
        type Output;
    }
    
    fn foo<T>()
    where
        for<'a> T: FnOnceDup<I::Item<'a>>,

    That doesn't seem too helpful and kinda hacky however 🤔

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)C-enhancementCategory: An issue proposing an enhancement or a PR with one.T-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