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

Make it crystal clear what lint type_alias_bounds actually signifies #126575

Merged
merged 9 commits into from
Jul 26, 2024

Conversation

fmease
Copy link
Member

@fmease fmease commented Jun 17, 2024

This is part of my work on F-lazy_type_alias `#![feature(lazy_type_alias)]` (tracking issue).


To recap, the lint type_alias_bounds detects bounds on generic parameters and where clauses on (eager) type aliases. These bounds should've never been allowed because they are currently neither enforced1 at usage sites of type aliases nor thoroughly checked for correctness at definition sites due to the way type aliases are represented in the compiler. Allowing them was an oversight.

Explicitly label this as a known limitation of the type checker/system and establish the experimental feature lazy_type_alias as its eventual proper solution.

Where this becomes a bit tricky (for me as a rustc dev) are the "secondary effects" of these bounds whose existence I sadly can't deny. As a matter of fact, type alias bounds do play some small roles during type checking. However, after a lot of thinking over the last two weeks I've come to the conclusion (not without second-guessing myself though) that these use cases should not trump the fact that these bounds are currently inherently broken. Therefore the lint type_alias_bounds should and will continue to flag bounds that may have subordinate uses.

The two known secondary effects are:

  1. They may enable the use of "shorthand" associated type paths T::Assoc (as opposed to fully qualified paths <T as Trait>::Assoc) where T is a type param bounded by some trait Trait which defines that assoc ty.
  2. They may affect the default lifetime of trait object types passed as a type argument to the type alias. That concept is called (trait) object lifetime default.

The second one is negligible, no question asked. The first one however is actually "kinda nice" (for writability) and comes up in practice from time to time.

So why don't I just special-case trait bounds that "define" shorthand assoc type paths as originally planned in #125709?

  1. Starting to permit even a tiny subset of bounds would already be enough to send a signal to users that bounds in type aliases have been legitimized and that they can expect to see type alias bounds in the wild from now on (proliferation). This would be actively misleading and dangerous because those bounds don't behave at all like one would expect, they are not real2!
    1. Let's take type A<T: Trait> = T::Proj; for example. Everywhere else in the language T: Trait means T: Trait + Sized. For type aliases, that's not the case though: T: Trait and T: Trait + ?Sized for that matter do neither mean T: Trait + Sized nor T: Trait + ?Sized (for both!). Instead, whether T requires Sized or not entirely depends on the definition of Trait2. Namely, whether or not it is bounded by Sized.
    2. Given type A<T: Trait<AssocA = ()>> = T::AssocB;, while X: Trait gets checked given A<X> (by virtue of projection wfchecking post alias expansion2), the associated type constraint AssocA = () gets dropped entirely! While we could choose to warn on such cases, it would inevitably lead to a huge pile of special cases.
    3. While it's common knowledge that the body / aliased type / RHS of an (eager) type alias does not get checked for well-formedness, I'm not sure if people would realize that that extends to bounds as well. Namely, type A<T: Trait<[u8]>> = T::Proj; compiles even if Trait's generic parameter requires Sized. Of course, at usage sites [u8]: Sized would still end up getting checked2, so it's not a huge problem if you have full control over A. However, imagine that A was actually part of a public API and was never used inside the defining crate (not unreasonable). In such a scenario, downstream users would be presented with an impossible to use type alias! Remember, bounds may grow arbitrarily complex and nuanced in practice.
    4. Even if we allowed trait bounds that "define" shorthand assoc type paths, we would still need to continue to warn in cases where the assoc ty comes from a supertrait despite the fact that the shorthand syntax can be used: type A<T: Sub> = T::Assoc; does compile given trait Sub: Super {} and trait Super { type Assoc; }. However, A<X> does not enforce X: Sub, only X: Super2. All that to say, type alias bounds are simply not real and we shouldn't pretend they are!
    5. Summarizing the points above, we would be legitimizing bounds that are completely broken!
  2. It's infeasible to implement: Due to the lack of TypeckResults in ItemCtxt (and a way to propagate it to other parts of the compiler), the resolution of type-dependent paths in non-Body items (most notably type aliases) is not recoverable from the HIR alone which would be necessary because the information of whether an associated type path (projection) is a shorthand is only present pre&in-HIR and doesn't survive HIR ty lowering. Of course, I could rerun parts of HIR ty lowering inside the lint type_alias_bounds (namely, probe_single_ty_param_bound_for_assoc_ty which would need to be exposed or alternatively a stripped-down version of it). This likely has a performance impact and introduces complexity. In short, the "benefits" are not worth the costs.

  • 1st commit: Small tweak
  • 2nd commit: Refacoring
  • 3rd & 4th commit: Update a diagnostic to avoid suggesting type alias bounds
  • 5th commit: Flag type alias bounds even if the RHS contains inherent associated types.
    • I started to allow them at some point in the past which was not correct (see commit for details)
  • 6th commit: Allow type alias bounds if the RHS contains const projections and GCEs are enabled
    • (and add a FIXME(generic_const_exprs) to be revisited before (M)GCE's stabilization)
    • As a matter of fact type alias bounds are enforced in this case because the contained AnonConsts do get checked for well-formedness and crucially they inherit the generics and predicates of their parent item (here: the type alias)
  • Commits 7-9: Improve the lint type_alias_bounds itself

Fixes #125789 (sugg diag fix).
Fixes #125709 (wontfix, acknowledgement, sugg diag applic fix).
Fixes #104918 (sugg diag applic fix).
Fixes #100270 (wontfix, acknowledgement, sugg diag applic fix).
Fixes #94398 (true fix).

r? @compiler-errors @oli-obk

Footnotes

  1. From the perspective of the trait solver.

  2. Given type A<T: Trait> = T::Proj;, the reason why the trait bound "T: Trait" gets seemingly enforced at usage sites of the type alias A is simply because A<X> gets expanded to "<X as Trait>::Proj" very early on and it's the expansion that gets checked for well-formedness, not the type alias reference. 2 3 4 5

@fmease fmease added the L-type_alias_bounds Lint: type_alias_bounds label Jun 17, 2024
@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 Jun 17, 2024
.suggestion = the clause will not be checked when the type alias is used, and should be removed
lint_builtin_type_alias_bounds_enable_feat_help = add `#![feature(lazy_type_alias)]` to the crate attributes to enable the desired semantics
lint_builtin_type_alias_bounds_label = will not be checked at usage sites of the type alias
lint_builtin_type_alias_bounds_limitation_note = this is a known limitation of the type checker that may be lifted in a future edition.
Copy link
Member Author

Choose a reason for hiding this comment

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

Type checker / type system (implementation vs specification), it's not as clear-cut in Rust.

Copy link
Contributor

Choose a reason for hiding this comment

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

could just leave out the of the type checker part

// Bounds of the form `T: 'a` with `T` type param affect object lifetime defaults.
if let hir::WherePredicate::BoundPredicate(pred) = pred
&& pred.bounds.iter().any(|bound| matches!(bound, hir::GenericBound::Outlives(_)))
&& pred.bound_generic_params.is_empty() // indeed, even if absent from the RHS
Copy link
Member Author

Choose a reason for hiding this comment

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

This does look a bit wrong but as a matter of fact while T: 'a does
associate T with the object lifetime default 'a, for<'x> T: 'a
does not even though 'x does not occur inside 'a!

let ty = cx.tcx.type_of(item.owner_id).instantiate_identity();
if ty.has_type_flags(ty::TypeFlags::HAS_CT_PROJECTION)
&& cx.tcx.features().generic_const_exprs
{
return;
Copy link
Member Author

@fmease fmease Jun 17, 2024

Choose a reason for hiding this comment

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

That's just what I have concluded from extensively playing around without
actually reading through the source, so corrections and clarifications are
welcome.

Please check out tests/ui/const-generics/generic_const_exprs/type-alias-bounds.rs for an elaboration.

Copy link
Member

Choose a reason for hiding this comment

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

Your explanation in the test seems correct to me 👍 what a funny interaction. Does a similar thing happen with feature(type_alias_impl_trait) and type Foo<T: Trait> = Bar<T::Assoc, impl Sized>; 🤔

HAS_CT_PROJECTIONS is a bit sus since really the check you want is "does this have unevaluated const arg of DefKind::AnonConst" and we might wind up having unevaluated const args that arent anon consts one day. I don't think it really matters though since gce is kinda on rocky ground anyway so complicating this over some hypothetical potential future (that probably wont happen) seems not worth it to me.

👍 to adding this special case if you want it. It'd also just be fine to not have it, gce is unstable enough (and generally broken enough) that I expect we'll wind up fixing bounds on type aliases long before we stabilize gce lol.

Copy link
Member Author

@fmease fmease Jun 18, 2024

Choose a reason for hiding this comment

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

Does a similar thing happen with feature(type_alias_impl_trait) and type Foo<T: Trait> = Bar<T::Assoc, impl Sized>;

All TAITs are implicitly lazy — TyCtxt::type_alias_is_lazy is also true if the aliased type contains opaque types and therefore TAIT references get HIR-ty-lowered to ty::Weak.

the check you want is "does this have unevaluated const arg of DefKind::AnonConst"

So basically visiting the Ty and check if it has any UnevaluatedConsts where tcx.def_kind($.def.0) == DefKind::AnonConst? Sure I can do that.

Yeah, I figured that the way (M)GCEs are resolved and dealt with will likely change significantly. Hence the FIXME(generic_const_exprs): Revisit this before stabilization. I just really wanted to add a test for this relatively surprising behavior just so ppl working on (M)GCE have something tangible to work with. I don't particularly care right now how the final behavior will look like as long as various edge cases are considered :^)

Copy link
Member Author

@fmease fmease Jun 18, 2024

Choose a reason for hiding this comment

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

I just really wanted to add a test for this relatively surprising behavior

E.g., the fact that type A<T> = (T, I32<{ 0 }>); doesn't get checked for well-formedness under GCE but type A<T> = (T, I32<{{ 0 }}>); or type A<T> = (T, I32<{ 0; 0 }>); does :P Ofc, I know why that happens atm but I hope that that particular behavior won't stay as is ^^'

Copy link
Member

Choose a reason for hiding this comment

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

So basically visiting the Ty and check if it has any UnevaluatedConsts where tcx.def_kind($.def.0) == DefKind::AnonConst? Sure I can do that.

Yeah but it's really not a big deal, I'd be perfectly comfortable landing with the current handling of gce stuff.

but I hope that that particular behavior won't stay as is ^^'

It'll definitely get fixed when stabilizing lazy type aliases. It'll also be fixed by min_generic_const_exprs since that wont use anon consts

*[other] these bounds
}
lint_builtin_type_alias_bounds_qualify_assoc_tys_sugg = fully qualify this associated type
lint_builtin_type_alias_bounds_where_clause = where clauses on type aliases are not enforced
Copy link
Member Author

@fmease fmease Jun 17, 2024

Choose a reason for hiding this comment

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

Moving "in[/on] type aliases" before "not enforced" was not a stylistic decision.
The previous wording could be read as "not enforced within the body or definition of type aliases
which — while not entirely untrue (lack of item wfchecking) — would miss the point and might confuse
users who realize that trait bounds can indeed define/enable shorthand projections
(and are thus "enforced" for some unusual interpretation of the word like "to give strength or force to").

#[subdiagnostic]
pub sub: Option<SuggestChangingAssocTypes<'a, 'b>>,
}
let affect_object_lifetime_defaults = self
Copy link
Member Author

Choose a reason for hiding this comment

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

I went back and forth on whether to display any sort of note or span note (à la "however, these bounds do affect the lifetime default of trait objects passed to the type alias") or help message ("annotate trait object types passed to the type alias with an explicit lifetime") but nothing seemed right (and I couldn't get it to show below the removal suggestion due to a limitation of Diag infra)

// See also `tests/ui/const-generics/generic_const_exprs/type-alias-bounds.rs`.
let ty = cx.tcx.type_of(item.owner_id).instantiate_identity();
if ty.has_type_flags(ty::TypeFlags::HAS_CT_PROJECTION)
&& cx.tcx.features().generic_const_exprs
Copy link
Member Author

Choose a reason for hiding this comment

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

This should be correct in cross-crate scenarios, too. I've tested GCE const projections from external crates used inside of a type alias of the local non-GCE crate.

@bors

This comment was marked as resolved.

@fmease fmease force-pushed the update-lint-type_alias_bounds branch from d193e75 to 3a67897 Compare July 22, 2024 23:33
@rust-log-analyzer

This comment has been minimized.

@fmease fmease force-pushed the update-lint-type_alias_bounds branch from 3a67897 to 9fe101f Compare July 22, 2024 23:42
@fmease fmease added the rla-silenced Silences rust-log-analyzer postings to the PR it's added on. label Jul 22, 2024
@fmease fmease force-pushed the update-lint-type_alias_bounds branch from 9fe101f to 5859dff Compare July 22, 2024 23:50
@fmease fmease removed the rla-silenced Silences rust-log-analyzer postings to the PR it's added on. label Jul 23, 2024
.suggestion = the clause will not be checked when the type alias is used, and should be removed
lint_builtin_type_alias_bounds_enable_feat_help = add `#![feature(lazy_type_alias)]` to the crate attributes to enable the desired semantics
lint_builtin_type_alias_bounds_label = will not be checked at usage sites of the type alias
lint_builtin_type_alias_bounds_limitation_note = this is a known limitation of the type checker that may be lifted in a future edition.
Copy link
Member

Choose a reason for hiding this comment

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

nit .

Copy link
Member

Choose a reason for hiding this comment

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

oh, well actually this has a newline... hm.... i don't know how to best render this then

@compiler-errors
Copy link
Member

Thanks for making this so clear. Sorry for taking so long to approve. Can you double check the messages don't have a . at the end, then r=me?

@compiler-errors
Copy link
Member

In that case, whatevs

@bors r+

@bors
Copy link
Contributor

bors commented Jul 26, 2024

📌 Commit 5859dff has been approved by compiler-errors

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 26, 2024
bors added a commit to rust-lang-ci/rust that referenced this pull request Jul 26, 2024
Rollup of 7 pull requests

Successful merges:

 - rust-lang#126575 (Make it crystal clear what lint `type_alias_bounds` actually signifies)
 - rust-lang#127017 (Extend rules of dead code analysis for impls for adts to impls for types refer to adts)
 - rust-lang#127523 (Migrate `dump-ice-to-disk` and `panic-abort-eh_frame` `run-make` tests to rmake)
 - rust-lang#127557 (Add a label to point to the lacking macro name definition)
 - rust-lang#127989 (Migrate `interdependent-c-libraries`, `compiler-rt-works-on-mingw` and `incr-foreign-head-span` `run-make` tests to rmake)
 - rust-lang#128099 (migrate tests/run-make/extern-flag-disambiguates to rmake)
 - rust-lang#128170 (Make Clone::clone a lang item)

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors merged commit ceae371 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#126575 - fmease:update-lint-type_alias_bounds, r=compiler-errors

Make it crystal clear what lint `type_alias_bounds` actually signifies

This is part of my work on https://github.com/rust-lang/rust/labels/F-lazy_type_alias ([tracking issue](rust-lang#112792)).

---

To recap, the lint `type_alias_bounds` detects bounds on generic parameters and where clauses on (eager) type aliases. These bounds should've never been allowed because they are currently neither enforced[^1] at usage sites of type aliases nor thoroughly checked for correctness at definition sites due to the way type aliases are represented in the compiler. Allowing them was an oversight.

Explicitly label this as a known limitation of the type checker/system and establish the experimental feature `lazy_type_alias` as its eventual proper solution.

Where this becomes a bit tricky (for me as a rustc dev) are the "secondary effects" of these bounds whose existence I sadly can't deny. As a matter of fact, type alias bounds do play some small roles during type checking. However, after a lot of thinking over the last two weeks I've come to the conclusion (not without second-guessing myself though) that these use cases should not trump the fact that these bounds are currently *inherently broken*. Therefore the lint `type_alias_bounds` should and will continue to flag bounds that may have subordinate uses.

The two *known* secondary effects are:

1. They may enable the use of "shorthand" associated type paths `T::Assoc` (as opposed to fully qualified paths `<T as Trait>::Assoc`) where `T` is a type param bounded by some trait `Trait` which defines that assoc ty.
2. They may affect the default lifetime of trait object types passed as a type argument to the type alias. That concept is called (trait) object lifetime default.

The second one is negligible, no question asked. The first one however is actually "kinda nice" (for writability) and comes up in practice from time to time.

So why don't I just special-case trait bounds that "define" shorthand assoc type paths as originally planned in rust-lang#125709?

1. Starting to permit even a tiny subset of bounds would already be enough to send a signal to users that bounds in type aliases have been legitimized and that they can expect to see type alias bounds in the wild from now on (proliferation). This would be actively misleading and dangerous because those bounds don't behave at all like one would expect, they are *not real*[^2]!
   1. Let's take `type A<T: Trait> = T::Proj;` for example. Everywhere else in the language `T: Trait` means `T: Trait + Sized`. For type aliases, that's not the case though: `T: Trait` and `T: Trait + ?Sized` for that matter do neither mean `T: Trait + Sized` nor `T: Trait + ?Sized` (for both!). Instead, whether `T` requires `Sized` or not entirely depends on the definition of `Trait`[^2]. Namely, whether or not it is bounded by `Sized`.
   2. Given `type A<T: Trait<AssocA = ()>> = T::AssocB;`, while `X: Trait` gets checked given `A<X>` (by virtue of projection wfchecking post alias expansion[^2]), the associated type constraint `AssocA = ()` gets dropped entirely! While we could choose to warn on such cases, it would inevitably lead to a huge pile of special cases.
   3. While it's common knowledge that the body / aliased type / RHS of an (eager) type alias does not get checked for well-formedness, I'm not sure if people would realize that that extends to bounds as well. Namely, `type A<T: Trait<[u8]>> = T::Proj;` compiles even if `Trait`'s generic parameter requires `Sized`. Of course, at usage sites `[u8]: Sized` would still end up getting checked[^2], so it's not a huge problem if you have full control over `A`. However, imagine that `A` was actually part of a public API and was never used inside the defining crate (not unreasonable). In such a scenario, downstream users would be presented with an impossible to use type alias! Remember, bounds may grow arbitrarily complex and nuanced in practice.
   4. Even if we allowed trait bounds that "define" shorthand assoc type paths, we would still need to continue to warn in cases where the assoc ty comes from a supertrait despite the fact that the shorthand syntax can be used: `type A<T: Sub> = T::Assoc;` does compile given `trait Sub: Super {}` and `trait Super { type Assoc; }`. However, `A<X>` does not enforce `X: Sub`, only `X: Super`[^2]. All that to say, type alias bounds are simply not real and we shouldn't pretend they are!
   5. Summarizing the points above, we would be legitimizing bounds that are completely broken!
2. It's infeasible to implement: Due to the lack of `TypeckResults` in `ItemCtxt` (and a way to propagate it to other parts of the compiler), the resolution of type-dependent paths in non-`Body` items (most notably type aliases) is not recoverable from the HIR alone which would be necessary because the information of whether an associated type path (projection) is a shorthand is only present pre&in-HIR and doesn't survive HIR ty lowering. Of course, I could rerun parts of HIR ty lowering inside the lint `type_alias_bounds` (namely, `probe_single_ty_param_bound_for_assoc_ty` which would need to be exposed or alternatively a stripped-down version of it). This likely has a performance impact and introduces complexity. In short, the "benefits" are not worth the costs.

---

* 3rd commit: Update a diagnostic to avoid suggesting type alias bounds
* 4th commit: Flag type alias bounds even if the RHS contains inherent associated types.
  * I started to allow them at some point in the past which was not correct (see commit for details)
* 5th commit: Allow type alias bounds if the RHS contains const projections and GCEs are enabled
  * (and add a `FIXME(generic_const_exprs)` to be revisited before (M)GCE's stabilization)
  * As a matter of fact type alias bounds are enforced in this case because the contained AnonConsts do get checked for well-formedness and crucially they inherit the generics and predicates of their parent item (here: the type alias)
* Remaining commits: Improve the lint `type_alias_bounds` itself

---

Fixes rust-lang#125789 (sugg diag fix).
Fixes rust-lang#125709 (wontfix, acknowledgement, sugg diag applic fix).
Fixes rust-lang#104918 (sugg diag applic fix).
Fixes rust-lang#100270 (wontfix, acknowledgement, sugg diag applic fix).
Fixes rust-lang#94398 (true fix).

r? `@compiler-errors` `@oli-obk`

[^1]: From the perspective of the trait solver.
[^2]: Given `type A<T: Trait> = T::Proj;`, the reason why the trait bound "`T: Trait`" gets *seemingly* enforced at usage sites of the type alias `A` is simply because `A<X>` gets expanded to "`<X as Trait>::Proj`" very early on and it's the *expansion* that gets checked for well-formedness, not the type alias reference.
@fmease fmease deleted the update-lint-type_alias_bounds branch July 26, 2024 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
L-type_alias_bounds Lint: type_alias_bounds S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
7 participants