Skip to content
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

Graciously handle Drop impls introducing more generic parameters than the ADT #127220

Merged
merged 2 commits into from
Jul 26, 2024

Conversation

BoxyUwU
Copy link
Member

@BoxyUwU BoxyUwU commented Jul 1, 2024

Follow up to #110577
Fixes #126378
Fixes #126889

Motivation

A current issue with the way we check drop impls do not specialize any of their generic parameters is that when the Drop impl introduces more generic parameters than are present on the ADT, we fail to prove any bounds involving those parameters. This can be demonstrated with the following code on stable which fails due to the fact that <T as Trait>::Assoc == U is not present in Foos ParamEnv even though arguably there is no reason it cannot compiler:

struct Foo<T: Trait>(T);

trait Trait {
    type Assoc;
}

impl<T: Trait<Assoc = U>, U: ?Sized> Drop for Foo<T> {
    //~^ ERROR: `Drop` impl requires `<T as Trait>::Assoc == U` but the struct ...
    fn drop(&mut self) {}
}

fn main() {}

I think the motivation for supporting this code is somewhat lacking, it might be useful in practice for deeply nested associated types where you might want to be able to write:
where T: Trait<Assoc: Other<AnotherAssoc: MoreTrait<YetAnotherAssoc: InnerTrait<Final = U>>>>
in order to be able to just use U in the function body instead of writing out the whole nested associated type. Regardless I don't think there is really any reason to not support this code and it is relatively easy to support it.

What I find slightly more compelling is the fact that when defining a const parameter const N: u8 we desugar that to having a where clause requiring the constant N is typed as u8 (ClauseKind::ConstArgHasType). As we always desugar const parameters to have these bounds, if we attempt to prove that some const parameter N is of type u8 and there is no bound on N in the enviroment that generally indicates usage of an incorrect ParamEnv (this has caught a bug already).

Given that, if we write the following code:

#![feature(associated_const_equality)]
struct Foo<T: Trait>(T);

trait Trait {
    const ASSOC: usize;
}

impl<T: Trait<ASSOC = N>, const N: usize> Drop for Foo<T> {
    fn drop(&mut self) {}
}

fn main() {}

The Drop impl would have this desugared where clause about N being of type usize, and if we were to try to prove that where clause in Foo's ParamEnv we would ICE as there would not be any ConstArgHasType in the environment (which generally indicates improper ParamEnv usage. As this is otherwise well formed code (the T: Trait<ASSOC = N> causes N to be constrained) we have to handle this somehow and I believe the only principled way to support this is the changes I have made to dropck.rs that would cause these code examples to compiler (Perhaps we could just throw out all ConstArgHasType where clauses from the predicates we prove but that makes me nervous even if it might actually be okay).

The changes

Currently the way dropck.rs works is that take the ParamEnv of the ADT and instantiate it with the generic arguments used on the self ty of the impl. We then instantiate the predicates of the drop impl with the identity params to the impl, e.g. in the original example <T as Trait>::Assoc == U stays as <T as Trait>::Assoc == U. We then attempt to prove all the where clauses in the instantiated env of the self type ADT.

This PR changes us to first instantiate the impl with infer vars, then we equate the self type (with infer vars as its generic arguments) with the self type as written by the user. This causes all generic parameters on the impl that are constrained via associated type/const equality bounds to be left as inference variables while all other parameters are still Ty/Const/Region

Finally when instantiating the predicates on the impl, instead of using the identity arguments, we use the list of inference variables of which some have been inferred to the impl parameters. In practice this means that we wind up proving <T as Trait>::Assoc == ?x which can succeed just fine. In the const generics example we would wind up trying to prove ConstArgHasType(?x: usize) instead of ConstArgHasType(N: usize) which avoids the ICE as it is expected to encounter goals of the form ?x: usize.

At a higher level the way I justify/think about this is that as we are proving goals in the environment of the ADT (Foo in the above examples), we do not expect to encounter generic parameters from a different environment so we must "deal with them" somehow. In this PR we handle them by replacing them with inference variables as they should either actually be unconstrained (and we will error later) or they are constrained to be equal to some associated type/const.

To go along with this it would be nice if we were not instantiating the adt's env with the generic arguments to the ADT in the Drop impl as it would make it clearer we are proving bounds in the adt's env instead of the Drop impl's. Instead we would map the predicates on the drop impl to be valid in the environment of the adt. In practice this causes diagnostic regressions as all of the generic parameters in errors refer to the ones defined on the adt; attempting to map these back to the ones on the impl, while possible, is involved as writing a TypeFolder over FulfillmentError is non trivial.

Edge cases

There are some subtle interactions here:

One is that we should not allow <T as Trait>::Assoc == U to be present on the Drop if U is constrained by the self type of the impl and the bound is not present in the ADT's environment. demonstrated with the following code:

trait Trait {
    type Assoc;
}

struct Foo<T: Trait, U: ?Sized>(T, U);

impl<T: Trait<Assoc = U>, U: ?Sized> Drop for Foo<T, U> {
    //~^ ERROR: `Drop` impl requires `<T as Trait>::Assoc == U`
    fn drop(&mut self) {}
}

fn main() {}

This is tested at tests/ui/dropck/constrained_by_assoc_type_equality_and_self_ty.rs.

Another weirdness is that we permit the following code to compile now:

struct Foo<T>(T);

impl<'a, T: 'a> Drop for Foo<T> {
    fn drop(&mut self) {}
}

This is caused by the fact that we permit unconstrained lifetime parameters in trait implementations as long as they are not used in associated types (so we do not wind up erroring on this code like we perhaps ought to), combined with the fact that as we are now proving T: '?x instead of T: 'a which allows proving the bound via '?x= 'empty wheras previously it would have failed.

This is tested as part of tests/ui/dropck/reject-specialized-drops-8142.rs.


r? @compiler-errors

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jul 1, 2024
@BoxyUwU BoxyUwU added T-types Relevant to the types team, which will review and decide on the PR/issue. and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jul 1, 2024
Copy link
Member

@compiler-errors compiler-errors left a comment

Choose a reason for hiding this comment

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

cool beans

@compiler-errors
Copy link
Member

compiler-errors commented Jul 1, 2024

This is a pretty simple extension to the new dropck algorithm that was introduced in #110577, however, it's probably worth FCPing because of a few subtleties pointed out in the PR description. This seems rock solid tho, thanks for the detailed PR description.

@rfcbot fcp merge

@rfcbot
Copy link

rfcbot commented Jul 1, 2024

Team member @compiler-errors has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Jul 1, 2024
@BoxyUwU
Copy link
Member Author

BoxyUwU commented Jul 1, 2024

WHOO WE DID IT !

image

@compiler-errors
Copy link
Member

@rfcbot review

@BoxyUwU BoxyUwU force-pushed the dropck_handle_extra_impl_params branch from f60ff37 to 6a00008 Compare July 2, 2024 01:30
@compiler-errors compiler-errors added S-waiting-on-fcp Status: PR is in FCP and is awaiting for FCP to complete. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 9, 2024
@compiler-errors
Copy link
Member

r=me on the code changes

@nikomatsakis
Copy link
Contributor

I'm having a hard time understanding what the change does. Is this a good summary, @BoxyUwU ?

  • Given a struct S<P> where WC_S with parameters P
  • and a impl<Q> Drop for S<R> where WC_I
  • try to prove forall<P> { WC_S => exists<Q> { S<P> = S<R>, WC_I } }

This would correspond to

  • using the ParamEnv from the struct (i.e., forall<P> { WC_S => ... })
  • instantiating the impl arguments Q with inference variables (exists<Q>)
  • proving the self type is equal (S<P> = S<R>) and impl where-clauses (WC_I)

@compiler-errors
Copy link
Member

@nikomatsakis: Yes exactly.

@nikomatsakis
Copy link
Contributor

@rfcbot reviewed

OK, that makes sense to me.

@rfcbot rfcbot added final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. and removed proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. labels Jul 15, 2024
@rfcbot
Copy link

rfcbot commented Jul 15, 2024

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot added finished-final-comment-period The final comment period is finished for this PR / Issue. and removed final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. labels Jul 25, 2024
@rfcbot
Copy link

rfcbot commented Jul 25, 2024

The final comment period, with a disposition to merge, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

This will be merged soon.

@rfcbot rfcbot added the to-announce Announce this issue on triage meeting label Jul 25, 2024
@compiler-errors
Copy link
Member

uwu

@bors r+ rollup

@bors
Copy link
Contributor

bors commented Jul 25, 2024

📌 Commit 6a00008 has been approved by compiler-errors

It is now in the queue for this repository.

@bors bors added the S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. label Jul 25, 2024
bors added a commit to rust-lang-ci/rust that referenced this pull request Jul 26, 2024
…iaskrgr

Rollup of 7 pull requests

Successful merges:

 - rust-lang#126090 (Fix supertrait associated type unsoundness)
 - rust-lang#127220 (Graciously handle `Drop` impls introducing more generic parameters than the ADT)
 - rust-lang#127950 (Use `#[rustfmt::skip]` on some `use` groups to prevent reordering.)
 - rust-lang#128085 (Various notes on match lowering)
 - rust-lang#128150 (Stop using `unsized_const_parameters` in core/std)
 - rust-lang#128194 (LLVM: LLVM-20.0 removes MMX types)
 - rust-lang#128211 (fix: compilation issue w/ refactored type)

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors merged commit 29314e4 into rust-lang:master Jul 26, 2024
6 checks passed
@rustbot rustbot added this to the 1.82.0 milestone Jul 26, 2024
rust-timer added a commit to rust-lang-ci/rust that referenced this pull request Jul 26, 2024
Rollup merge of rust-lang#127220 - BoxyUwU:dropck_handle_extra_impl_params, r=compiler-errors

Graciously handle `Drop` impls introducing more generic parameters than the ADT

Follow up to rust-lang#110577
Fixes rust-lang#126378
Fixes rust-lang#126889

## Motivation

A current issue with the way we check drop impls do not specialize any of their generic parameters is that when the `Drop` impl introduces *more* generic parameters than are present on the ADT, we fail to prove any bounds involving those parameters. This can be demonstrated with the following [code on stable](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=139b65e4294634d7286a3282bc61e628) which fails due to the fact that `<T as Trait>::Assoc == U` is not present in `Foo`s `ParamEnv` even though arguably there is no reason it cannot compiler:
```rust
struct Foo<T: Trait>(T);

trait Trait {
    type Assoc;
}

impl<T: Trait<Assoc = U>, U: ?Sized> Drop for Foo<T> {
    //~^ ERROR: `Drop` impl requires `<T as Trait>::Assoc == U` but the struct ...
    fn drop(&mut self) {}
}

fn main() {}
```

I think the motivation for supporting this code is somewhat lacking, it might be useful in practice for deeply nested associated types where you might want to be able to write:
`where T: Trait<Assoc: Other<AnotherAssoc: MoreTrait<YetAnotherAssoc: InnerTrait<Final = U>>>>`
in order to be able to just use `U` in the function body instead of writing out the whole nested associated type. Regardless I don't think there is really any reason to *not* support this code and it is relatively easy to support it.

What I find slightly more compelling is the fact that when defining a const parameter `const N: u8` we desugar that to having a where clause requiring the constant `N` is typed as `u8` (`ClauseKind::ConstArgHasType`). As we *always* desugar const parameters to have these bounds, if we attempt to prove that some const parameter `N` is of type `u8` and there is no bound on `N` in the enviroment that generally indicates usage of an incorrect `ParamEnv` (this has caught a bug already).

Given that, if we write the following code:
```rust
#![feature(associated_const_equality)]
struct Foo<T: Trait>(T);

trait Trait {
    const ASSOC: usize;
}

impl<T: Trait<ASSOC = N>, const N: usize> Drop for Foo<T> {
    fn drop(&mut self) {}
}

fn main() {}
```

The `Drop` impl would have this desugared where clause about `N` being of type `usize`, and if we were to try to prove that where clause in `Foo`'s `ParamEnv` we would ICE as there would not be any `ConstArgHasType` in the environment (which generally indicates improper `ParamEnv` usage. As this is otherwise well formed code (the `T: Trait<ASSOC = N>` causes `N` to be constrained) we have to handle this *somehow* and I believe the only principled way to support this is the changes I have made to `dropck.rs` that would cause these code examples to compiler (Perhaps we could just throw out all `ConstArgHasType` where clauses from the predicates we prove but that makes me nervous even if it might actually be okay).

## The changes

Currently the way `dropck.rs` works is that take the `ParamEnv` of the ADT and instantiate it with the generic arguments used on the self ty of the `impl`. We then instantiate the predicates of the drop impl with the identity params to the impl,  e.g. in the original example `<T as Trait>::Assoc == U` stays as `<T as Trait>::Assoc == U`. We then attempt to prove all the where clauses in the instantiated env of the self type ADT.

This PR changes us to first instantiate the impl with infer vars, then we equate the self type (with infer vars as its generic arguments) with the self type as written by the user. This causes all generic parameters on the impl that are constrained via associated type/const equality bounds to be left as inference variables while all other parameters are still `Ty`/`Const`/`Region`

Finally when instantiating the predicates on the impl, instead of using the identity arguments, we use the list of inference variables of which some have been inferred to the impl parameters. In practice this means that we wind up proving `<T as Trait>::Assoc == ?x` which can succeed just fine. In the const generics example we would wind up trying to prove `ConstArgHasType(?x: usize)` instead of `ConstArgHasType(N: usize)` which avoids the ICE as it is expected to encounter goals of the form `?x: usize`.

At a higher level the way I justify/think about this is that as we are proving goals in the environment of the ADT (`Foo` in the above examples), we do not expect to encounter generic parameters from a different environment so we must "deal with them" somehow. In this PR we handle them by replacing them with inference variables as they should either *actually* be unconstrained (and we will error later) or they are constrained to be equal to some associated type/const.

To go along with this it would be nice if we were not instantiating the adt's env with the generic arguments to the ADT in the `Drop` impl as it would make it clearer we are proving bounds in the adt's env instead of the `Drop` impl's. Instead we would map the predicates on the drop impl to be valid in the environment of the adt. In practice this causes diagnostic regressions as all of the generic parameters in errors refer to the ones defined on the adt; attempting to map these back to the ones on the impl, while possible, is involved as writing a `TypeFolder` over `FulfillmentError` is non trivial.

## Edge cases

There are some subtle interactions here:

One is that we should not allow `<T as Trait>::Assoc == U` to be present on the `Drop` if `U` is constrained by the self type of the impl and the bound is not present in the ADT's environment. demonstrated with the [following code](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=af839e2c3e43e03a624825c58af84dff):
```rust
trait Trait {
    type Assoc;
}

struct Foo<T: Trait, U: ?Sized>(T, U);

impl<T: Trait<Assoc = U>, U: ?Sized> Drop for Foo<T, U> {
    //~^ ERROR: `Drop` impl requires `<T as Trait>::Assoc == U`
    fn drop(&mut self) {}
}

fn main() {}
```
This is tested at `tests/ui/dropck/constrained_by_assoc_type_equality_and_self_ty.rs`.

Another weirdness is that we permit the following code to compile now:
```rust
struct Foo<T>(T);

impl<'a, T: 'a> Drop for Foo<T> {
    fn drop(&mut self) {}
}
```
This is caused by the fact that we permit unconstrained lifetime parameters in trait implementations as long as they are not used in associated types (so we do not wind up erroring on this code like we perhaps ought to), combined with the fact that as we are now proving `T: '?x` instead of `T: 'a` which allows proving the bound via `'?x= 'empty` wheras previously it would have failed.

This is tested as part of `tests/ui/dropck/reject-specialized-drops-8142.rs`.

---

r? `@compiler-errors`
@apiraino apiraino removed the to-announce Announce this issue on triage meeting label Aug 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this PR / Issue. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. S-waiting-on-fcp Status: PR is in FCP and is awaiting for FCP to complete. T-types Relevant to the types team, which will review and decide on the PR/issue.
Projects
None yet
7 participants