-
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
[Underspecified semantics] Type check defaults at declaration. #46785
[Underspecified semantics] Type check defaults at declaration. #46785
Conversation
r? @eddyb (rust_highfive has picked a reviewer for you, use r? to override) |
02d1cd9
to
cb9da17
Compare
src/librustc_typeck/check/wfcheck.rs
Outdated
|
||
fcx.impl_implied_bounds(item_def_id, item.span) | ||
}); | ||
} | ||
|
||
/// Checks where clauses and inline bounds. |
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.
Nit: maybe better to say "...and inline bounds that are declared on def_id
."
src/librustc_typeck/check/wfcheck.rs
Outdated
// Defaults must be well-formed. | ||
for d in defaults { | ||
fcx.register_wf_obligation(fcx.tcx.type_of(d), fcx.tcx.def_span(d), self.code.clone()); | ||
} |
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.
This part seems unambiguously good.
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.
Though we likely want to do a crater run and decide if we need forwards compat warnings.
src/librustc_typeck/check/wfcheck.rs
Outdated
} | ||
|
||
// Check that each default fulfills the bounds on it's parameter. | ||
// We go over each predicate and duplicate it, substituting defaults in the self type. |
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.
This part seems less obviously like something we want. It's a bit tricky, since of course there can be "interconnections" between defaults.
I guess that to start i'd like to state more clearly what property we are trying to enforce here. For example, are we checking that each default, considered individually, would not cause things to stop being well-formed?
Let's take struct Foo<T=u32, U=i32> where C
(where C
is some set of where-clauses) as our running example. We are already checking that WF(C)
, which basically says that forall T: C => WF(C)
.
I guess you are then trying to check that forall T: [i32/U] C => [i32/U] WF(C)
? That is, assuming that U
is i32
, but holding T
abstract, we check that the predicates C
are well-formed? (And then we do a similar thing for T
?)
(Does that make sense? Am I confused (or being confusing)?)
Interesting. That sounds like the correct thing to do. What I implemented checks only trait Trait {}
struct Foo<T, U>(T, U);
impl Trait for Foo<i32, i32> {}
struct Bogus<T = i32, U = i32>(Foo<T, U>) where Foo<T, U> : Trait; It does not detect that the defaults in |
@leodasvacas
OK, I agree that's not sufficient. Also, it seems like you wound up with quite a lot of code for doing that check, and -- as you said -- calls to skip-binder and so forth. That is kind of a bad sign. I feel like we should just be able to do some substitutions, kind of like what was written above. Am I overlooking something? |
19415dc
to
e09082c
Compare
Ah, |
The failure boils down to: trait Trait<T> {}
struct Foo<U, V=i32>(U, V)
where U: Trait<V>;
______________________^ the trait `Trait<i32>` is not implemented for `U` This wasn't happening before because I wasn't substituting the rhs of trait clauses. It seems I'm not substituting in enough places, what I should be checking is EDIT: I've been experimenting with "just substituting" in this branch and the exact same problem remains, even if I'm checking |
This now substitute defaults only in the LHS of trait predicates. The code is looking better. There is a |
ping @nikomatsakis, I think this may be ready for another look! |
@leodasvacas |
It's been over 6 days since we last heard from reviewer @nikomatsakis. Perhaps someone else from @rust-lang/compiler could take a look at this? |
4f33c30
to
b0973cc
Compare
@shepmaster I got it |
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.
Left some nits and also some comments. I need to dig a bit more but I am beginning to have second thoughts -- in particular, the check that says "all predicates must be WF with each default substituted independently" feels a bit strict to me.
Consider the example I gave: either default is fine, so long as one of the two parameters is overriden to String
. It is not ok to override neither. Should we really error out on this case?
It seems a bit inconsistent with some of the direcitons we've been going, which is too allow for more "unsatisfiable" things but make it an error only when they wind up being used.
I feel like we need to consult the feelings of the @rust-lang/lang team on this question, and perhaps would want an RFC to get broader feedback (e.g., @sgrif might have an opinion).
src/librustc_typeck/check/wfcheck.rs
Outdated
}, |def, _| { | ||
let identity_substs = fcx.tcx.mk_param_from_def(def); | ||
if def.index != param_def.index { | ||
identity_substs |
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.
This name is odd; normally a substs
is a set of substitutions, but here this is a single type. How about something like param_ty
or identity_ty
?
src/librustc_typeck/check/wfcheck.rs
Outdated
.filter(|def| def.has_default && | ||
def.index >= generics.parent_count() as u32); | ||
for param_def in defaulted_params { | ||
// Defaults must be well-formed. |
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.
Nit: I always think it helps to see concrete examples. So here we might extend the comment to say:
This parameter has a default value. Check that this default value is well-formed. This would rule out something like this (see test XXX.rs
):
struct Foo<T = Vec<[u32]>> { .. }
// Vec<[u32]> is not WF because `[u32]: Sized` does not hold.
src/librustc_typeck/check/wfcheck.rs
Outdated
for param_def in defaulted_params { | ||
// Defaults must be well-formed. | ||
let d = param_def.def_id; | ||
fcx.register_wf_obligation(fcx.tcx.type_of(d), fcx.tcx.def_span(d), self.code.clone()); |
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.
Nit: some whitespace after this line would make the code more readable, I think. In particular, this for
loop below is a kind of separate thing than the register_wf_obligation
call.
src/librustc_typeck/check/wfcheck.rs
Outdated
fcx.register_wf_obligation(fcx.tcx.type_of(d), fcx.tcx.def_span(d), self.code.clone()); | ||
// Check the clauses are well-formed when the param is substituted by it's default. | ||
// In trait definitions, the predicate `Self: Trait` is problematic. | ||
// Therefore we skip such predicates. This means we check less than we could. |
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.
Here too I'd like to see an example.
As an example, consider something like this:
struct Foo<T = u32, U = i32>
where T: Bar<U> { }
trait Bar<U> where Self: Baz<U> { }
trait Baz<U> { }
impl Baz<String> for u32 { }
impl Baz<i32> for String { }
Here, when processing Foo
, we would attempt to prove the following:
forall<T, U> { // i.e., with T, U in scope
(T: Bar<U>) => { // and with `T: Bar<U>` in our environment
WF(T: Bar<U>),
WF(u32: Bar<U>), // substituting the default for T
WF(T: Baz<i32>),
}
}
This would fail because
WF(u32: Bar<U>)
requires thatu32: Baz<U>
, which is not implemented.WF(T: Bar<i32>)
requires thatT: Baz<i32>
, which is not implemented.
Actually, before I go too much further, let me verify again to be sure I am remembering correctly... that is the intention of this PR, right?
src/librustc_typeck/check/wfcheck.rs
Outdated
let d = param_def.def_id; | ||
fcx.register_wf_obligation(fcx.tcx.type_of(d), fcx.tcx.def_span(d), self.code.clone()); | ||
// Check the clauses are well-formed when the param is substituted by it's default. | ||
// In trait definitions, the predicate `Self: Trait` is problematic. |
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.
Can you say a bit more here -- remind me -- what makes Self: Trait
problematic exactly?
src/librustc_typeck/check/wfcheck.rs
Outdated
// Check the clauses are well-formed when the param is substituted by it's default. | ||
// In trait definitions, the predicate `Self: Trait` is problematic. | ||
// Therefore we skip such predicates. This means we check less than we could. | ||
for pred in predicates.predicates.iter().filter(|p| !(is_trait && p.has_self_ty())) { |
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.
This condition (p.has_self_ty()
) seems pretty imprecise. Like, wouldn't that mean that code like this is accepted (which I am not sure we would have wanted to accept?)
trait Foo<T=String>: Bar<T> { }
trait Bar<T: Copy> { }
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.
That code in particular is rejected as expected, I suppose we generate a T: Copy
predicate for the Foo<T=String>
declaration, I added that as a test. See comment below on p.has_self_ty()
being coarse.
Edit: The alternatives proposed below side step this issue and that check because That comment was badly worded, it now says what I meant: In trait Foo<T = i32> where Self: Sized, T: Into<Self> {} Maybe could check that |
Edit: My current preference is not the proposal in this comment but rather alternative 3 discussed below. Responding to this inline comment which made me rethink things: The intention of the PR is to move "error on use" to "error on declaration", and I'm particularly concerned with situations where a library goes from If the previous paragraph makes sense and we agree it's a problem, the current approach is sometimes too strict and sometimes insufficient. What about: Given a trait predicate, if all params appearing in the LHS have defaults then it should be a backwards compatible predicate. We verify that by checking the WF of predicate with all defaults substituted simultaneously. So we would no longer substitute defaults individually as that is to strict, we would substitute in the RHS but an example like this is skipped because there is a non-defaulted in the LHS. Does this motivation and solution make more sense? |
8055bdd
to
b1d4d42
Compare
I agree that checking the predicates substituted with defaults is aggressive in many cases, in which it should be a lint instead of an error. Is it viable to make this a lint? Alternatives:
The last commit implements 2 for trait predicates, if it's what we want then I'll implement it for projection predicates as well. |
cb35fcb
to
197bea4
Compare
💔 Test failed - status-appveyor |
|
…, r=nikomatsakis [Underspecified semantics] Type check defaults at declaration. Fixes #46669. See the test for code that compiles on stable but will no longer compile. This falls under a "Underspecified language semantics" fix. **Needs crater**. On type and trait declarations, we currently allow anything that name checks as a type parameter default. That allows the user to write a default that can never be applied, or even a default that may conditionally be applied depending on the type of another parameter. Mostly this just defers the error to use sites, but also allows clever hacks such as `Foo<T, U = <T as Iterator>::Item>` where `U` will be able to apply it's default only when `T: Iterator`. Maybe that means this bug is a feature, but it's a fiddly behaviour that seems undesirable. This PR validates defaults at declaration sites by ensuring all predicates on the parameter are valid for the default. With the exception of `Self: Sized` which we don't want to check to allow things like `trait Add<RHS = Self>`.
💔 Test failed - status-appveyor |
@bors retry appveyor timeout |
⌛ Testing commit 3e84aed with merge 040c66fa39dfb84beb413d74c06eec2887aa6b0a... |
💥 Test timed out |
@bors clean retry r- |
Fixes #46669. See the test for code that compiles on stable but will no longer compile. This falls under a "Underspecified language semantics" fix. Needs crater.
On type and trait declarations, we currently allow anything that name checks as a type parameter default. That allows the user to write a default that can never be applied, or even a default that may conditionally be applied depending on the type of another parameter. Mostly this just defers the error to use sites, but also allows clever hacks such as
Foo<T, U = <T as Iterator>::Item>
whereU
will be able to apply it's default only whenT: Iterator
. Maybe that means this bug is a feature, but it's a fiddly behaviour that seems undesirable.This PR validates defaults at declaration sites by ensuring all predicates on the parameter are valid for the default. With the exception of
Self: Sized
which we don't want to check to allow things liketrait Add<RHS = Self>
.