-
Notifications
You must be signed in to change notification settings - Fork 13k
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
Fix dyn incompleteness with multiple supertraits with different substitutions #133397
base: master
Are you sure you want to change the base?
Fix dyn incompleteness with multiple supertraits with different substitutions #133397
Conversation
@bors try |
…ss, r=<try> Fix dyn incompleteness with multiple supertraits with different substitutions So much to write about this. Fixes rust-lang#133361 r? `@ghost`
This comment has been minimized.
This comment has been minimized.
☀️ Try build successful - checks-actions |
@craterbot check |
👌 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
🚧 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
🎉 Experiment
|
88fa8f4
to
22cb94a
Compare
@bors try for later: |
…ss, r=<try> Fix dyn incompleteness with multiple supertraits with different substitutions So much to write about this. Fixes rust-lang#133361 r? `@ghost`
This comment has been minimized.
This comment has been minimized.
☀️ Try build successful - checks-actions |
👌 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
🚧 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
@craterbot cancel |
🗑️ Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
Rework dyn trait lowering to stop being so intertwined with trait alias expansion This PR reworks the trait object lowering code to stop handling trait aliases so funky, and removes the `TraitAliasExpander` in favor of a much simpler design. This refactoring is important for making the code that I'm writing in rust-lang/rust#133397 understandable and easy to maintain, so the diagnostics regressions are IMO inevitable. In the old trait object lowering code, we used to be a bit sloppy with the lists of traits in their unexpanded and expanded forms. This PR largely rewrites this logic to expand the trait aliases *once* and handle them more responsibly throughout afterwards. Please review this with whitespace disabled. r? lcnr
1ea7bfd
to
6991acf
Compare
This comment has been minimized.
This comment has been minimized.
6991acf
to
0b5d11c
Compare
This comment has been minimized.
This comment has been minimized.
0b5d11c
to
8225eb5
Compare
This comment has been minimized.
This comment has been minimized.
OK, this is ready for an initial review and then FCP after. |
8225eb5
to
e67f999
Compare
e67f999
to
38a3ff4
Compare
@@ -88,7 +89,6 @@ const BASE_SYSROOT_SUITE: &[TestCase] = &[ | |||
), | |||
TestCase::build_bin_and_run("aot.float-minmax-pass", "example/float-minmax-pass.rs", &[]), | |||
TestCase::build_bin_and_run("aot.issue-72793", "example/issue-72793.rs", &[]), | |||
TestCase::build_bin("aot.issue-59326", "example/issue-59326.rs"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @bjorn3: I removed this test since it no longer is expected to pass (well, it fails with a deny lint, I guess I could allow it too).
If you're happy with me removing it tho, should I also remove fn build_bin
? Or leave it as allow(unused)
since this is the only used callsite?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, let me just restore the test and allow the lint.
38a3ff4
to
5d8e6a4
Compare
This comment has been minimized.
This comment has been minimized.
5d8e6a4
to
e34c743
Compare
This comment has been minimized.
This comment has been minimized.
e34c743
to
178d48d
Compare
This comment has been minimized.
This comment has been minimized.
…trait's existential predicates
178d48d
to
b1af9a5
Compare
The job Click to see the possible cause of the failure (guessed by this bot)
|
@@ -5120,6 +5122,20 @@ declare_lint! { | |||
crate_level_only | |||
} | |||
|
|||
declare_lint! { | |||
/// Hi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right I need lint docs.
Background
The way that we handle a dyn trait type's projection bounds is very structural today. A dyn trait is represented as a list of
PolyExistentialPredicate
s, which in most cases will be a principal trait (likeIterator
) and a list of projections (likeItem = u32
). Importantly, the list of projections comes from user-written associated type bounds on the type and from elaborating the projections from the principal's supertraits.For example, given a set of traits like:
For the type
dyn Bar<i32, u32>
, the list of projections will be something like[Foo<i32>::Assoc = i32, Foo<u32>::Assoc = u32]
. We deduplicate these projections when they're identical, so fordyn Bar<(), ()>
would be something like[Foo<()>::Assoc = ()]
.Shortcomings: inference
We face problems when we begin to mix this structural notion of projection bounds with inference and associated type normalization. For example, let's try calling a generic function that takes
dyn Bar<A, B>
with a value of typedyn Bar<(), ()>
:What's going on here? Well, when calling
call_bar
, the generic signature&dyn Bar<?A, ?B>
does not unify with&dyn Bar<(), ()>
because the list of projections differ --[Foo<?A>::Assoc = ?A, Foo<?B>::Assoc = ?B]
vs[Foo<()>::Assoc = ()]
.A simple solution to this may be to unify the principal traits first, then attempt to deduplicate them after inference. In this case, if we constrain
?A = ?B = ()
, then we would be able to deduplicate those projections in the first list.However, this idea is still pretty fragile, and it's not a complete solution.
Shortcomings: normalization
Consider a slightly modified example:
This fails in the new solver. In this example, we try to unify
dyn Bar<(), ()>
anddyn Bar<(), <() as Mirror>::Assoc>
. We are faced with the same problem even though there are no inference variables, and making this work relies on eagerly and deeply normalizing all projections so that they can be structurally deduplicated.This is incompatible with how we handle associated types in the new trait solver, and while we could perhaps support it with some major gymnastics in the new solver, it suggests more fundamental shortcomings with how we deal with projection bounds in the new solver.
Shortcomings: redundant projections
Consider a final example:
In this case, we have a user-written associated type bound (
Assoc = _
) which overlaps the bound that comes from the supertrait projection ofBar
(namely,Foo<Assoc = ()>
). In a similar way to the two examples above, this causes us to have a projection list mismatch that the compiler is not able to deduplicate.Solution
The general strategy that this PR takes is to change the way we construct the
PolyExistentialPredicate
s of thedyn Trait
type. Specifically, we do not eagerly elaborate into it any associated type bounds that are implied by the supertraits of the dyn type's principal trait, and instead adjust projection and other code to compute these supertrait projections on-demand as needed from the principal trait.Concretely, consider the following code:
When users write
dyn Trait
, the projections on that type used to be[Super::Assoc = ()]
, but now are an empty list. When projecting<dyn Trait as Super>::Assoc
, we used to look through that list for a match, and now we look through the list and also re-elaborate the principal traitTrait
to figure out matches for projectingAssoc
.Specifically, I had to change these places in the compiler:
The weirdest of these changes is well-formedness. This is because we want to keep supporting, for example, code that looks like:
Since previously, due to eagerly elaborating supertrait projections into the type
dyn Foo<'a, T>
meant that it actually acted as if it weredyn Foo<'a, T, Assoc = &'a T>
, and the implied bounds code could deduce that for the type to be well-formed, then&'a T
must also be well-formed, and thus we must also have thatT: 'a
. I don't think it's that big of a deal to keep supporting this code, since I'm mainly concerned with fixing this class ofdyn Trait
bugs and not reworking the way thatdyn Trait
interacts with implied bounds.Caveat: Associated type bound shadowing
Since this PR changes the code to no longer explicitly treat associated type bounds that are elaborated from supertrait bounds the same as user-written associated type bounds, we must choose what to do when users write associated type bounds that overlap with ones from supertraits. Specifically, there are three distinct classes of associated type bounds.
Given code like:
dyn Trait<Assoc = ()>
. This associated type bound matches the supertrait associated type bound exactly.dyn Trait<Assoc = <() as Mirror>::Assoc>
. While this associated type bound is redundant above, we can't detect that it's redundant because normalizing while loweringdyn Trait
types leads to trait cycles.dyn Trait<Assoc = i32>
. This bound is conflicting, and there are no types which may be coerced to this trait alias.We have several choices for how to handle these.
Option A: Never deduplicate redundant associated type bounds
Always treat super-written associated type bounds as user-written predicates. This means that
dyn Trait
anddyn Trait<Assoc = ()>
are distinct types.I did not even begin to consider this solution since it seems to me to be unnecessarily breaking. I expect it to break thousands of crates in crater.
Option B: Deduplicate only syntactically equal associated type bounds
Filter out syntactically equal bounds (i.e. bounds from (1.) above), but otherwise treat associated type bounds as user-written projections (i.e. (2.) and (3.) above). While the fallout from this is much less than option A, we run the risk of introducing a new set of type errors on previously valid code:
which would lead to a new type error since the first type (
&dyn IteratorOfUnit<Item = <() as Mirror>::Assoc>
) has a projection list of[Iterator::Item = <() as Mirror>::Assoc]
, and the second type has an empty projection list.(side-note: You may ask why we can't just normalize here to filter out cases where we can trivially normalize -- normalizing in astconv/hir-lowering is very likely to result in cycle errors since normalization requires calling the same queries that we're normalizing within, so I think using normalization here would be incredibly fragile).
I analyzed this solution and there's some moderate breakage, specifically ~10 crates that all rely on an old version of
ra_ap_hir_ty
which had redundant type bounds on adyn Trait
(which I fixed in rust-lang/rust-analyzer#18577):... which results in a type mismatch because
dyn TypeFolder
anddyn TypeFolder<Error = Self::Error>
are distinct even thoughSelf::Error
normalizes toInfallible
, which is what is implied by the supertrait bounds ofTypeFolder
.Option C: Always prefer supertrait associated type bounds (the choice this PR chooses)
In the implementation proposed by this PR, we always prefer supertrait bounds over associated type bounds written on the type. That is, we never consider user-written associated type bounds when they are provided by a supertrait, and instead drop them on the floor, and lint if it doesn't agree with the bound from the supertrait.
That means any type
dyn Trait<Assoc = ...>
is equal todyn Trait
, since we ignore theAssoc = ...
bound since it's implied by a supertrait. I think that the behavioral changes associated with this PR don't matter much in practice -- if the associated type is in group (2.) and semantically equal (i.e. equal modulo normalization) then we're not changing behavior here, and if the associated type is not equal (group (3.)), then the dyn trait type was never possible to create in the first place, so I don't expect much code to be exercising that anyways.As a consequence, this code goes from fail to pass (for now, until we make a lint discussed below into a hard error):
However, we probably want to strongly discourage or outright ban associated type bounds which may be shadowed in this way. To begin the transition to being much stricter about these associated type bounds, I've added two lints to detect these cases.
Lints
To help users detect cases where behavior may be changed, I've added two lints:
DYN_ASSOC_REDUNDANT
- Warn, which covers the group (1.) of syntactically equal bounds.DYN_ASSOC_SHADOWED
- Deny (although I could be convinced to upgrade this one to warn for a release cycle), which covers the last two groups (2.) and (3.) of conflicting bounds.I don't expect
DYN_ASSOC_REDUNDANT
to ever become a hard error, butDYN_ASSOC_SHADOWED
probably should become a hard error soon since it'll ensure that the code sample above (which went from fail -> pass) goes back to being an error as it should.Downsides
The only surprising downside of this is the change in semantics around user-written associated type bounds which shadow ones implied by supertraits. I am pretty well convinced that this is unavoidable, and I chose the behavior to avoid the fallout from crater that we observed.
Conclusion
I could be convinced with going with option (B.), but I'd probably want to keep the
DYN_ASSOC_SHADOWED
lint as Deny so that we can move to eventually reporting these mismatches as soon as possible in ty lowering, rather than delaying them until a type mismatch occurs.