-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
change the strategy for diverging types #40224
Conversation
r? @eddyb (rust_highfive has picked a reviewer for you, use r? to override) |
Still experimenting here but this shows promise. run-pass and compile-fail both basically pass, though there is one stubborn compile-fail test giving me a double warning for some reason I've not yet figured out. =) |
Thanks for fixing stable regressions @nikomatsakis! |
Thanks for this! I just encountered yet another package that was broken by the last
Do we need to assign |
b7c0da5
to
0054ecb
Compare
src/librustc_typeck/check/mod.rs
Outdated
/// side produced `a` and one side produced `b`. If either of | ||
/// these types is `!`, then the result is the other. Otherwise, | ||
/// returns the LUB of the two types. | ||
pub fn join_tys(&self, |
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.
😻
I believe we have to treat it as let x = if foo {
22
} else {
return bar;
}; That is, if you wrote the I agree it's a bit undisciplined, but only a bit. There seems to be no perfect answer in terms of "how much" to check in cases like these. |
0535e3e
to
f2c980c
Compare
I did a crater run, though from an earlier revision (f63d9cc81c9ee3b9637b0d0343916371235967ea). There were 2 regressions and 1 timeout:
I haven't had time to investigate yet. I'll start a run in any case with the latest revision. |
New crater run gives the same two regressions. I think these are also the same problem causing the build to die. In my newer code, I am basically never creating diverging type variables; this is intentional, I wanted to see how far we could get without any fallback. It works well but we are failing with an unresolved type variable in this doctest: ```rust
use std::thread;
let handle = thread::spawn(move || {
panic!("oops!");
});
let result = handle.join();
assert!(result.is_err()); At the moment, I'm not interesting in spending my weekend time on this bug any further, so I'll investigate later... |
f2c980c
to
f6beabb
Compare
// None | | ||
// Some(&Adjustment { kind: Adjust::NeverToAny, .. }) => (), | ||
// _ => bug!("expr already has an adjustment on 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.
I think this used to be an assert ensuring coercions are not overwritten.
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.
er dang it, I meant to keep the bug!
, but not the match
6b4eeb5
to
5984dd7
Compare
Crater report shows zero regressions:
I'm a little surprised not to see any "fixed" entries, but I think that's because many of the regressions were either "fixed" by adding explicit type annotations or occurred in tests etc. |
src/librustc_typeck/check/mod.rs
Outdated
@@ -3311,6 +3347,20 @@ impl<'a, 'gcx, 'tcx> FnCtxt<'a, 'gcx, 'tcx> { | |||
_ => self.warn_if_unreachable(expr.id, expr.span, "expression") | |||
} | |||
|
|||
// Non-lvalues that produce values of type `!` indicate a |
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.
The lvalue thing looks like a hack. For example, isn't a dereference of an *const !
an lvalue, as in let x = *(ptr: *const !);
?
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.
Yeah, that's interesting. I agree the lvalue rule may be a bit overly broad, although it's not obvious to me that this is wrong. For example, consider x: &!
-- referencing *x
does not cause divergence, rather it's supposed to just never happen. That is, the fact that x
has type &!
and you are able to access it reflects the fact that this code is supposed to be dead. The same is true of dereferencing a pointer of type *const !
, I would argue. It does seem like it would make sense to issue some sort of warning here, though!
In any case, failing to set the "diverges!" flag then only affects unreachable code warnings and whether something like let x: ! = Some(*(ptr: *const !));
is legal (it would not be). Put another way, the diverges flag is a conservative approximation, and it should always be safe to set it less (right?).
In any case, I have not yet understood your alternative proposal, so let me try to read and process that.
I should have looked at the code earlier, but, here am I. The lvalue thing looks like a hack. For example, isn't a dereference of an I think the cleanest way to solve this is to have both "bottom up" divergence (if node is entered, will node exit?) and "top down" (can node be reached from the start) reachability, where reachability is only used for "unreachable code" warnings. Because the main thing divergence affects is whether blocks that return into Reachability should probably be passed "by &mut" and cloned when you need a lattice join. I'm not sure about whether the reachability lattice should be let x = if cond {
panic!()
} else {
panic!();
42 //~ WARNING unreachable code
};
42 //~ WARNING? unreachable code Top down unreachability means we don't emit unreachable code warnings if the diverging statement was already unreachable (e.g. |
// this match to get the same treatment: | ||
// | ||
// let _: Option<?T> = match x { | ||
// Ok(_) => Default::default(), |
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.
The _match
code uses try_coerce
for the first arm, so you still have this problem if the arms are reversed.
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.
Ah, good catch! I am not entirely happy with that bit of code, but it seemed like the most expedient way to get the correct behavior. I had hoped to revisit it after introducing subtyping obligations (?T <: ?U
). In any case, I'll add some examples with the match arms reversed and see what happens...
So, using In my view, accessing an lvalue of type In contrast, In this light, Anyway, I've got to run, so I'll try to do a better comparison to your proposal after I get my daughter out to school.. |
OK, I've thought more about this but not yet reached a firm conclusion. I started preparing a super-long gist with some thoughts, but I'm still churning on those, so I just wanted to post a more limited comment amending my previous one. =) In particular, I think everything I wrote about This is because it essentially plays the role of a kind of cast -- a way for users to signal that they know the code is unreachable. Kind of a safer, statically proven form of pub fn unreachable(x: Void) -> ! {
match x {}
} But this still builds on this notion I was going for that the set of things we consider to be "diverging expressions" ought to be a largely static set, relatively isolated from type inference (it cannot be fully isolated, because of method dispatch). I think the set of diverging expressions would be as follows:
This property propagates upwards through enclosing expressions by default unless they join multiple control-flow paths (in which case it the upward propagation is included in the rules above). Making the set of expressions that the compiler considers to signal "unreachability" be relatively clear and syntactic seems to fit the Rust tradition to me (e.g., it is the reason we added However, I am beginning to question some of the constraints I was starting from. For example, I assumed that we wanted this to type-check (because a test exists, iirc): fn foo() -> ! {
panic!();
22
} But in retrospect I'm not sure we do. After all, I broke the unit-fallback test because it is wrong-headed, and I think this may well be wrong-headed too. It's kind of hard to make sense of type-checking dead-code, but I think that one reasonable interpretation is that "one could replace the diverging expression with something that would lead to a correct type-check". That seems to not be true in this case -- so why do we apply this dead tail expression? But there may well be a reasonable reason. One thing that I definitely think we do want to work is guaranteeing that the Anyway, this "short" comment grew to be nearly as long and disjoint as the other, but I'm going to post it anyhow. I'll probably iterate a bit more on this branch -- but I want to try and land something soon, even if we wind up tweaking it some more, since the current behavior really ought not to make it into a beta. |
I agree with this. The current diverges flag is sort of playing both of these rules, and it seems a bit confusing to me. I'd probably prefer to have two distinct flags, as you say, and also to thread using parameters and return values (but I've not tried to do this yet; it may be quite painful). The current system just seems a touch error-prone to me. I'm not sure I want to do this in this branch though. Maybe. |
OK, I'm still going back and forth about how to treat |
Fixing the One thing that annoys me though is this case: tls::with_opt(move |tcx| {
let msg = format!("{}:{}: {}", file, line, args);
match (tcx, span) {
(Some(tcx), Some(span)) => tcx.sess.diagnostic().span_bug(span, &msg),
(Some(tcx), None) => tcx.sess.diagnostic().bug(&msg),
(None, _) => panic!(msg)
}
}); this is judged to have an unconstrained return type in my code, and it is because the way I am typing The easiest way to fix this would be to have the match code check if all the arms are known to have type |
e3db7a1
to
03c2389
Compare
For the most part, the current code performs similarly, although it differs in some particulars. I'll be nice to have these tests for judging future changes, as well.
Before I was checking this in `demand_coerce` but that's not really the right place. The right place is to move that into the coercion routine itself.
The `try_coerce` method coerces from a source to a target type, possibly inserting adjustments. It should guarantee that the post-adjustment type is a subtype of the target type (or else that some side-constraint has been registered which will lead to an error). However, it used to return the (possibly adjusted) source as the type of the expression rather than the target. This led to less good downstream errors. To work around this, the code around blocks -- and particular tail expressions in blocks -- had some special case manipulation. However, since that code is now using the more general `CoerceMany` construct (to account for breaks), it can no longer take advantage of that. This lead to some regressions in compile-fail tests were errors were reported at "less good" locations than before. This change modifies coercions to return the target type when successful rather the source type. This extends the behavior from blocks to all coercions. Typically this has limited effect but on a few tests yielded better errors results (and avoided regressions, of course). This change also restores the hint about removing semicolons which went missing (by giving 'force-unit' coercions a chance to add notes etc).
They are so annoying to update, and haven't caught any bugs afaik.
We no longer give suggestions; this is presumably related to the changes I made in coercion. However, those suggestions appear to be wrong anyhow!
0d3f472
to
2414222
Compare
@bors r=eddyb |
📌 Commit 2414222 has been approved by |
change the strategy for diverging types The new strategy is as follows. First, the `!` type is assigned in two cases: - a block with a diverging statement and no tail expression (e.g., `{return;}`); - any expression with the type `!` is considered diverging. Second, we track when we are in a diverging state, and we permit a value of any type to be coerced **into** `!` if the expression that produced it is diverging. This means that `fn foo() -> ! { panic!(); 22 }` type-checks, even though the block has a type of `usize`. Finally, coercions **from** the `!` type to any other are always permitted. Fixes #39808. Fixes #39984.
☀️ Test successful - status-appveyor, status-travis |
The new strategy is as follows. First, the
!
type is assignedin two cases:
{return;}
);!
is considered diverging.Second, we track when we are in a diverging state, and we permit a value
of any type to be coerced into
!
if the expression that producedit is diverging. This means that
fn foo() -> ! { panic!(); 22 }
type-checks, even though the block has a type of
usize
.Finally, coercions from the
!
type to any other are alwayspermitted.
Fixes #39808.
Fixes #39984.