Skip to content

RPIT(IT)s "leak" internal implementation details of lifetime capturing via boundedness #140517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Y-jiji opened this issue Apr 30, 2025 · 13 comments
Labels
A-impl-trait Area: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch. A-lifetimes Area: Lifetimes / regions C-bug Category: This is a bug. S-has-mcve Status: A Minimal Complete and Verifiable Example has been found for this issue T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue.

Comments

@Y-jiji
Copy link

Y-jiji commented Apr 30, 2025

I tried this code:

trait A {
    type B<'a>;
}

struct Foo(u64);

impl A for Foo {
    type B<'a> = Bar<'a>;
}

struct Bar<'a>(&'a Foo);

trait X<Y: A> {
    fn method<'a, 'b>(&'a self, bar: Y::B<'b>) -> impl Iterator<Item=usize>;
}

struct Eric;

impl X<Foo> for Eric {
    fn method<'a, 'b>(&'a self, _bar: <Foo as A>::B<'b>) -> impl Iterator<Item=usize> {
        vec![1, 2].into_iter()
    }
}

struct Jack;

impl X<Foo> for Jack {
    fn method<'a, 'b>(&'a self, _bar: Bar<'b>) -> impl Iterator<Item=usize> {
        vec![1, 2].into_iter()
    }
}

fn main() {}

I expected to see this happen: implementation of X<Foo> on Jack compiles without error

  • As you can see, Bar<'a> and <Foo as A>::B<'a> are exactly the same type.
  • Therefore, implementation of X<Foo> on Jack and Eric should have identical behavior.
  • Implementation of X<Foo> on Eric compiles.

Instead, this happened: implementation of X<Foo> on Jack gives the following error

error[E0195]: lifetime parameters or bounds on method `method` do not match the trait declaration
  --> gat-borrow-checking-bug.rs:28:14
   |
14 |     fn method<'a, 'b>(&'a self, bar: Y::B<'b>) -> impl Iterator<Item=usize>;
   |              -------- lifetimes in impl do not match this method in trait
...
28 |     fn method<'a, 'b>(&'a self, _bar: Bar<'b>) -> impl Iterator<Item=usize> {
   |              ^^^^^^^^ lifetimes do not match method in trait

error: aborting due to 1 previous error

Meta

rustc --version --verbose:

rustc 1.85.1 (4eb161250 2025-03-15)

Using the nightly version, it still doesn't work

rust version 1.88.0-nightly (74509131e 2025-04-29)
@Y-jiji Y-jiji added the C-bug Category: This is a bug. label Apr 30, 2025
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Apr 30, 2025
@Y-jiji Y-jiji changed the title Unexpected different behaviour between GAT and direct type parameter GAT code doesn't compile when plug in the type directly Apr 30, 2025
@Y-jiji

This comment has been minimized.

@rustbot

This comment has been minimized.

@Y-jiji

This comment has been minimized.

@rustbot rustbot added the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label Apr 30, 2025
@Y-jiji

This comment has been minimized.

@rustbot rustbot added the T-types Relevant to the types team, which will review and decide on the PR/issue. label Apr 30, 2025
@fmease fmease added the A-GATs Area: Generic associated types (GATs) label Apr 30, 2025
@jieyouxu jieyouxu removed the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label May 1, 2025
@Y-jiji
Copy link
Author

Y-jiji commented May 5, 2025

@fmease @jieyouxu I'm willing to take some effort to fix this, but I'm absolutely a newbie in rust compiler. Can I get some help to get started?

@fmease

This comment has been minimized.

@fmease fmease linked a pull request May 5, 2025 that will close this issue
@fmease fmease added A-lifetimes Area: Lifetimes / regions A-impl-trait Area: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch. labels May 5, 2025
@fmease
Copy link
Member

fmease commented May 5, 2025

You've encountered a detail of the type system: "Boundedness".

The 'b defined on assoc fn method in the last trait impl is late-bound but the 'b in the trait decl is early-bound and thus there's a mismatch. If you add where 'b: (sic!) or where 'b: 'b to the bottom-most method, then your linked code will compile because the where-clause forces that 'b to be early-bound thus matching the trait definition.

@fmease
Copy link
Member

fmease commented May 5, 2025

I guess you could call this an implementation detail of RPIT(IT)s leaking through (existential impl-Trait in return position). If an RPIT captures an in-scope lifetime, it forces it to be early-bound.

So if you have:

fn f0<'a>() {} // 'a is late-bound
fn f1<'a>() where 'a: {} // 'a is early-bound

fn f2<'a>() -> impl Sized {} // 'a is late-bound in Rust <2024 and early-bound in Rust >=2024
fn f3<'a>() -> impl Sized + use<> {} // 'a is late-bound
fn f4<'a>() -> impl Sized + use<'a> {} // 'a is early-bound

@fmease
Copy link
Member

fmease commented May 5, 2025

I g2g can't finish my explanation rn. Later then.

@fmease
Copy link
Member

fmease commented May 6, 2025

Okay, but there are indeed super fishy things going on...

  1. In trait Trait { fn f<'a>() -> impl Sized; }, 'a is early-bound (as alluded to above).
  2. However, in trait Trait { fn f<'a>(_: &'a ()) -> impl Sized; }, 'a is suddenly late-bound! What?
  3. Moreover, in trait Trait { fn f<'a>(_: <&'a () as Identity>::Output) -> impl Sized; }, 'a is suddenly early-bound!

This is what this issue essentially boils down to!

Doesn't need RPITIT, RPIT also exhibits this behavior:

Code (Rust 2024, w.l.o.g.) Boundedness of 'a
fn f<'a>() -> impl Sized {} early
fn f<'a>(_: &'a ()) -> impl Sized {}'a late
fn f<'a>(_: <&'a () as Identity>::Output) -> impl Sized {} early
fn f<'a>(_: HideIdentity<&'a ()>) -> impl Sized {}1 late

Footnotes

  1. With struct HideIdentity<T>(<T as Identity>::Output);. Meaning this doesn't directly have to do with variance, it seems.

@fmease fmease added S-has-mcve Status: A Minimal Complete and Verifiable Example has been found for this issue and removed A-GATs Area: Generic associated types (GATs) labels May 6, 2025
@fmease
Copy link
Member

fmease commented May 6, 2025

cc @compiler-errors

@fmease fmease changed the title GAT code doesn't compile when plug in the type directly RPIT(IT)s "leak" internal implementation details of lifetime capturing via boundedness May 6, 2025
@fmease
Copy link
Member

fmease commented May 6, 2025

Let's look what #![rustc_variance_of_opaques] has to say for the last four examples:

Code Variances of the opaque
fn f<'a>() -> impl Sized {} ['a: *, 'a: o]
fn f<'a>(_: &'a ()) -> impl Sized {}'a ['a: o]
fn f<'a>(_: <&'a () as Identity>::Output) -> impl Sized {} ['a: *, 'a: o]
fn f<'a>(_: HideIdentity<&'a ()>) -> impl Sized {}[^1] ['a: o]

but that doesn't tell us anything we don't already know.

@fmease
Copy link
Member

fmease commented May 6, 2025

Like, I obviously know of lifetime duplication for RPITITs (RPITs, too??) but that's not necessarily related.

The rules observed above seem super "arbitrary" just looking at these four examples. I obviously haven't looked into the compiler impl yet, I first need to find the relevant place. So I don't yet know if this is intentional, WONTFIX or 'worth fixing'.

Edit: Aye, I'm pretty sure what we observe here gets fully determined in fn is_late_bound_map. Likely relating to constrainedness.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-impl-trait Area: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch. A-lifetimes Area: Lifetimes / regions C-bug Category: This is a bug. S-has-mcve Status: A Minimal Complete and Verifiable Example has been found for this issue T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

4 participants