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

Implement RFC 2532 – Associated Type Defaults #61812

Merged
merged 31 commits into from
Feb 26, 2020
Merged

Implement RFC 2532 – Associated Type Defaults #61812

merged 31 commits into from
Feb 26, 2020

Conversation

jonas-schievink
Copy link
Contributor

@jonas-schievink jonas-schievink commented Jun 13, 2019

This is a partial implementation that is still missing the changes to object types, since I ran into some trouble while implementing that. I'm opening this part already to get feedback on the implementation and the unexpected test fallout (see my comments below). The remaining changes can be done in a later PR.

Blockers before this can land:

cc #29661
Fixes #53907
Fixes #54182
Fixes #62211
Fixes #41868
Fixes #63593
Fixes #47385
Fixes #43924
Fixes #32350
Fixes #26681
Fixes #67187

@rust-highfive

This comment has been minimized.

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jun 13, 2019
@@ -1104,27 +1104,41 @@ fn assemble_candidates_from_impls<'cx, 'gcx, 'tcx>(
// an error when we confirm the candidate
// (which will ultimately lead to `normalize_to_error`
// being invoked).
node_item.item.defaultness.has_value()
false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This looks very wrong to me, since it sounds like it'd allow assuming defaults inside the trait. For some reason that doesn't happen (the tests pass).

Copy link
Contributor

Choose a reason for hiding this comment

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

@jonas-schievink

Inside the trait, you won't be having a VtableImpl (which indicates a specific impl was selected), but a VtableParam (because of an "implicit where-clause").

Copy link
Contributor

@arielb1 arielb1 Jul 4, 2019

Choose a reason for hiding this comment

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

However, this does not correctly handle the case where an associated type default is overridden in a derived impl:

#![feature(associated_type_defaults, specialization)]

trait Foo {
    type Xyz = u32;
    fn foobar() -> Self::Xyz;
}

// 1)
impl<T> Foo for T {
    fn foobar() -> Self::Xyz { 0u32 }
}

// 2)
impl Foo for u32 {
    type Xyz = &'static u32;
}

fn main() {
    u32::foobar();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

However, according to the spec, the problem is somewhere else, because of

To permit this is not a problem because Foo for Vec<T> is not further specializable since Bar in the implementation has not been marked as default.

Copy link
Contributor

@Centril Centril Jul 6, 2019

Choose a reason for hiding this comment

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

However, according to the spec, the problem is somewhere else, because of

So the RFC says:

If an implementation does make the associated type available for further specialization, then other definitions in the implementation may not assume the given underlying specified type of the associated type and may only assume that it is Self::TheAssociatedType.

which the snippet above clearly violates... but the more interesting question is "how do we ban it?"...

...For the sake of separate compilation (i.e. imagine 2) is in a different crate), I think we need to say that 1) is legitimate but 2) is not. Why? Because it overrides type Xyz which should not be up for specialization since it was not marked as default in 1) (if it did then fn foobar could not assume type Xyz's definition). Moreover fn foobar here assumes type Xyz's definition.

Copy link
Contributor

@arielb1 arielb1 Jul 6, 2019

Choose a reason for hiding this comment

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

Sure. If we go by the RFC, the default associated type type Xyz = u32; should only be specializable by the first impl that matches, not by any specializing impls (this can be checked at 2) time because of the orphan rules), so 2) should give an E0520 (Xyz specializes an item from a parent impl, but that item is not marked default).

I'll note that this contradicts the way associated fns work:

#![feature(specialization)]

trait Foo {
    fn foobar() -> bool { false }
}

impl<T> Foo for T {
}

impl Foo for u32 {
    fn foobar() -> bool { true }
}

fn main() {
    println!("{:?}", u32::foobar()); // prints `true`
}

also, if we do that, we have to decide whether associated consts should work like associated types or like associated fns.

Copy link
Contributor

Choose a reason for hiding this comment

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

also, if we do that, we have to decide whether associated consts should work like associated types or like associated fns.

The RFC applies to associated fns as well so how they work will need to be changed as well but not necessarily in this PR. The RFC says:

This applies generally to any item inside a trait. [...]

which includes fns and consts also.

Copy link
Contributor

@arielb1 arielb1 Jul 6, 2019

Choose a reason for hiding this comment

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

This is an open question in the design of specialization (#31844):

Should default trait items be considered default (i.e. specializable)?

Specialization is unstable, so I don't think there's a real reason not to forbid respecialization of associated types and associated fns together. However, I might want to do this in an earlier PR, and assess crater impact.

That change that has a fair bit of breaking impact, so I'm for implementing it fairly quickly. However, I'll still be OK with opening an issue for the specialization rules change and linking it from the OP of #29661.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we continue on this question: can you have default provided trait items that can be re-overriden?

#![feature(specialization)]

trait Foo {
    default fn foobar() -> bool { false }
}

impl<T> Foo for T {
}

impl Foo for u32 {
    fn foobar() -> bool { true } // OK
}

fn main() {
    println!("{:?}", u32::foobar()); // prints `true`
}

I think either option would be worth writing clearly in a "changes to specialization" section in RFC 2532, along with examples.

Copy link
Contributor

@Centril Centril Jul 6, 2019

Choose a reason for hiding this comment

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

This is an open question in the design of specialization (#31844):

Should default trait items be considered default (i.e. specializable)?

This is a bit of an orthogonal question; the main question is instead whether re-overriding is allowed as you point out later. I think it flows from the reference of the RFC and the spirit of it that re-overriding is banned. Instead, the idea is that 1) in both of your snippets "lock in" what they do not explicitly allow for further specialization (with default $item in impls). Whether trait items must be prefixed with default to be specializable is not a particularly interesting question from this perspective and especially not from the POV of specialization groups as per in the "future possibilities" section of the RFC.

I think either option would be worth writing clearly in a "changes to specialization" section in RFC 2532, along with examples.

It's a good idea to write down interesting points and decisions somewhere be it in the RFC or in the tracking issue. I think we can link to this discussion in a bullet point in the tracking issue description and then consider amending the RFC with new information at a later point (when things have slowed down and the pieces are in place). These notes will be useful when I (or someone else, but most likely not...) writes the stabilization report.

let _: <Pub as PubTr>::AssocTy;
//~^ ERROR type `priv_parent_substs::Priv` is private
//~| ERROR type `priv_parent_substs::Priv` is private
let _: <Pub as PubTr>::AssocTy; // TODO no longer an error?!
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This also looks extremely wrong to me since the only difference to the code below is the omission of <_>, but I can't imagine how this patch caused this, so it might be preexisting?

@@ -184,19 +184,19 @@ error[E0223]: ambiguous associated type
--> $DIR/ufcs-partially-resolved.rs:36:12
|
LL | let _: <u8 as Tr>::Y::NN;
| ^^^^^^^^^^^^^^^^^ help: use fully-qualified syntax: `<<u8 as Tr>::Y as Trait>::NN`
| ^^^^^^^^^^^^^^^^^ help: use fully-qualified syntax: `<u16 as Trait>::NN`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These suggestions are now worse due to the projection succeeding

@rust-highfive

This comment has been minimized.

@Centril
Copy link
Contributor

Centril commented Jun 13, 2019

r? @nikomatsakis

@Centril

This comment has been minimized.

@Centril

This comment has been minimized.

@rust-highfive

This comment has been minimized.

@rust-highfive

This comment has been minimized.

src/test/ui/associated-types/defaults-specialization.rs Outdated Show resolved Hide resolved
| ^^ expected associated type, found u8
|
= note: expected type `fn() -> <A<T> as Tr>::Ty`
found type `fn() -> u8`
Copy link
Contributor

@Centril Centril Jun 14, 2019

Choose a reason for hiding this comment

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

I wonder if it would be feasible in a not-too-hacky a manner to tell the user that defaults are the cause of the failure (both when using specialization and not). Otherwise the user might be reasonably confused as to why they don't get to assume the default since they are unlikely to be aware of the type theoretical reasons.

Copy link
Contributor

Choose a reason for hiding this comment

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

This could also be left as future work; if you prefer that, please leave a checkbox in the tracking issue :)

Also cc @estebank

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I'll do this. This is challenging enough as is ;)

Copy link
Contributor

Choose a reason for hiding this comment

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

For that future work... One thing you might try in the unhappy path is to check whether there is a default that would have allowed it to succeed if not for our rules.


if let Some(default_ty) = default {
let default_ty = AstConv::ast_ty_to_ty(&ItemCtxt::new(tcx, def_id), default_ty);
let preds = AstConv::compute_bounds(
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a test to also make sure that bounds are respected when not written directly on the associated type itself?

trait Foo<T>
where
    // Same bound as `type Bar: Clone` but written indirectly:
    <Self as Foo<T>>::Bar: Clone,
{
    type Bar = Vec<T>;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Another case to consider is when you have multiple defaults:

trait Foo<T>
where
    <Self as Foo<T>>::Bar: Clone,
{
    type Bar = Vec<Self::Baz>;
    type Baz = T;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah this doesn't work at all right now, this check needs to be implemented differently.

This gets particularly tricky when you have a where-clause like where Self::Ty: Con<Self> on the trait. You can't really know if this holds for the defaulted type because Self isn't yet known.

Copy link
Contributor

Choose a reason for hiding this comment

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

In general, when faced with a situation "you can't really know yet" this should be treated as if it wasn't so. In other words, we want constructive proof that something holds. So in a situation with where Self::Ty: Con<Self>, because Self isn't yet known, the answer is to treat it as a type variable (which it is...) and then error.

I think the check should be something along the lines of substituting the associated types for the defaults and then try to prove the normalized where clauses. It seems to me that rejecting where Self::Ty: Con<Self>, should just "fall out".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just pushed a better implementation of the check and a comprehensive test

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks great!

Can you also add my last snippet to that so we check for scenarios involving more than one associated type?

Copy link
Contributor Author

@jonas-schievink jonas-schievink Jun 15, 2019

Choose a reason for hiding this comment

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

Done. Right now the behavior is to do a shallow replacement of associated types with their defaults, so <Self as Foo<T>>::Bar: Clone is turned into Vec<Self::Baz>, not Vec<T>. I guess this needs to be fixed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I think the shallow substitution is actually beneficial here, since it forces the trait to work with any combination of defaults and non-defaults: If we did the deep substitution and had a T: Clone bound, we'd prove that Vec<T>: Clone and the trait would be accepted. If a user then implements it and overrides only Baz, suddenly Vec<Self::Baz>: Clone might no longer hold, and the user would be forced to also override Bar. With the shallow substitution, the trait would have to add a Self::Baz: Clone bound instead/in addition to the T: Clone bound, which means that an implementation could replace either assoc. type without making others not work anymore.

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting; it seems to me a shallow substitution is strictly more conservative than the deeper one, right? So we can relax it later if we want.

I think it would be good to demonstrate the difference in a special test file and then also leave a description of the distinction (with examples) on the tracking issue so that these interesting aspects aren't lost in comments here.

@rust-highfive

This comment has been minimized.

@jonas-schievink

This comment has been minimized.

@rust-highfive

This comment has been minimized.

@rust-highfive

This comment has been minimized.

@jonas-schievink

This comment has been minimized.

@nikomatsakis
Copy link
Contributor

I read over the code here -- but then I was looking back over the list of tests that I assembled earlier and I felt like I didn't see (e.g.) tests that were looking at the interaction of dyn Trait and associated type defaults. Did I overlook that functionality, or is it not included in the PR?

@Centril
Copy link
Contributor

Centril commented Feb 24, 2020

I believe that part is not included per PR OP:

This is a partial implementation that is still missing the changes to object types, since I ran into some trouble while implementing that. [...]

@nikomatsakis
Copy link
Contributor

OK, great, I thought that might be the case, given the code diffs I saw.

@nikomatsakis
Copy link
Contributor

@bors r+

I also added the list of tests to the tracking issue #29661

@bors
Copy link
Contributor

bors commented Feb 25, 2020

📌 Commit 6cc268b has been approved by nikomatsakis

@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-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Feb 25, 2020
@nikomatsakis
Copy link
Contributor

@jonas-schievink sorry for taking so unforgiveably long here ❤️

@Centril
Copy link
Contributor

Centril commented Feb 25, 2020

I'm so excited to finally see this land! Thanks so much @jonas-schievink for your hard work and thanks @nikomatsakis for diligently assembling all those test cases.

@bors
Copy link
Contributor

bors commented Feb 26, 2020

⌛ Testing commit 6cc268b with merge 3a0d106...

@bors
Copy link
Contributor

bors commented Feb 26, 2020

☀️ Test successful - checks-azure
Approved by: nikomatsakis
Pushing 3a0d106 to master...

@bors bors added the merged-by-bors This PR was explicitly merged by bors. label Feb 26, 2020
@rust-highfive
Copy link
Collaborator

📣 Toolstate changed by #61812!

Tested on commit 3a0d106.
Direct link to PR: #61812

💔 clippy-driver on windows: test-pass → test-fail (cc @mcarton @oli-obk @Manishearth @flip1995 @yaahc @phansch @llogiq).
💔 clippy-driver on linux: test-pass → test-fail (cc @mcarton @oli-obk @Manishearth @flip1995 @yaahc @phansch @llogiq).

rust-highfive added a commit to rust-lang-nursery/rust-toolstate that referenced this pull request Feb 26, 2020
Tested on commit rust-lang/rust@3a0d106.
Direct link to PR: <rust-lang/rust#61812>

💔 clippy-driver on windows: test-pass → test-fail (cc @mcarton @oli-obk @Manishearth @flip1995 @yaahc @phansch @llogiq).
💔 clippy-driver on linux: test-pass → test-fail (cc @mcarton @oli-obk @Manishearth @flip1995 @yaahc @phansch @llogiq).
@bors bors merged commit 3a0d106 into rust-lang:master Feb 26, 2020
JohnTitor added a commit to JohnTitor/rust-clippy that referenced this pull request Feb 27, 2020
bors added a commit to rust-lang/rust-clippy that referenced this pull request Feb 27, 2020
@ehuss ehuss mentioned this pull request Mar 2, 2020
bors added a commit that referenced this pull request Mar 2, 2020
Update cargo, clippy

Closes #69601

## cargo

16 commits in e57bd02999c9f40d52116e0beca7d1dccb0643de..bda50510d1daf6e9c53ad6ccf603da6e0fa8103f
2020-02-21 20:20:10 +0000 to 2020-03-02 18:05:34 +0000
- Fix rare failure in collision_export test. (rust-lang/cargo#7956)
- Ignore broken Cargo.toml in git sources (rust-lang/cargo#7947)
- Add more fingerprint mtime debug logging. (rust-lang/cargo#7952)
- Fix plugin tests for latest nightly. (rust-lang/cargo#7955)
- Simplified usage code of SipHasher (rust-lang/cargo#7945)
- Add a special case for git config discovery inside tests (rust-lang/cargo#7944)
- Fixes issue rust-lang/cargo#7543 (rust-lang/cargo#7946)
- Filter out cfgs which should not be used during build (rust-lang/cargo#7943)
- Provide extra context on a query failure. (rust-lang/cargo#7934)
- Try to clarify `cargo metadata`'s relationship with the workspace. (rust-lang/cargo#7927)
- Update libgit2 dependency (rust-lang/cargo#7939)
- Fix link in comment (rust-lang/cargo#7936)
- Enable `cargo doc --open` tests on macos. (rust-lang/cargo#7932)
- build-std: remove sysroot probe (rust-lang/cargo#7931)
- Try to clarify how feature flags work on the "current" package. (rust-lang/cargo#7928)
- Add extra details in the new feature resolver doc comment. (rust-lang/cargo#7918)

## clippy

6 commits in fc5d0cc..8b7f7e6
2020-02-24 05:58:17 +0000 to 2020-03-02 20:00:31 +0000
- Rustup to #69469 (rust-lang/rust-clippy#5254)
- Some rustups (rust-lang/rust-clippy#5247)
- Update git2 to 0.12 (rust-lang/rust-clippy#5232)
- Rustup to #61812 (rust-lang/rust-clippy#5231)
- Add lint to improve floating-point expressions (rust-lang/rust-clippy#4897)
- Do not run deploy action on other repos (rust-lang/rust-clippy#5222)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment