-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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 the loop_break_value
feature.
#37487
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @Aatch (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see the contribution instructions for more information. |
body_block = loop_block; | ||
} | ||
|
||
let might_break = this.in_loop_scope( |
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: formatting is extra-weird here. extracting destination into variable here might be better.
I also feel that you might not need the destination to be conditional (i.e. Option<T>
) here either. Instead, just assign unit to the destination if there’s break
without EXPR
.
} | ||
|
||
// The “return” value of the loop body must always be an unit, but we cannot |
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 remember this well-formed-ness business being quite non-trivial, so I feel like this comment should stay. That being said its entirely possible that introducing break EXPR
made this well-formedness story obsolete, in which case this whole code might be ready to be cleaned up.
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.
AFAICT the comment is about reusing the value of the loop's block (which is always ()
) as the value of the loop itself. When you consider that break EXPR
exists, though, it becomes apparent (IMO) that this is nonsensical because the value of the loop is not always ()
. That's why I removed most of the comment.
let (exit_block, extent) = { | ||
let loop_scope = self.find_loop_scope(span, label); | ||
(exit_selector(loop_scope), loop_scope.extent) | ||
}; | ||
self.exit_scope(span, extent, block, exit_block); | ||
self.cfg.start_new_block().unit() |
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.
Could you please just inline this? Having a helper method with just 2 lines with 2 callers doesn’t seem to justify it anymore.
(break_block, extent, destination.clone()) | ||
}; | ||
if let Some(dest) = destination { | ||
if let Some(value) = value.take() { |
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: value.take() is superfluous and this is ignoring the continuation block returned by either branch. This whole function could instead look like this:
let scope = this.find_loop_scope(expr_span, label);
scope.might_break = true;
if let Some(ref dst) = scope.destination {
block /* this is important! */ = match value {
Some(value) => unpack!(block = this.into(dst, block, value)),
None => this.cfg.push_assign_unit(block, source_info, dst),
};
}
this.exit_scope(expr_span, scope.extent, scope.break_block, block);
this.cfg.start_new_block().unit()
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 reason why this code is so awkward is that we can't keep this
borrowed when we get to further calls like this.into
. (find_loop_scope
borrows &mut self
and that's tied to the returned &mut LoopScope
).
Also, the first branch already takes care of the returned block (it uses unpack!
; into
returns BlockAnd<()>
) while the second branch doesn't return a block (push_assign_unit
returns ()
).
.take()
is a remnant though, my bad.
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.
Also, the first branch already takes care of the returned block (it uses unpack!
Ah, I missed the block =
inside the unpack
, my bad.
The reason why this code is so awkward is that we can't keep this borrowed
You can manually relinquish the borrow by doing something along the lines of:
let (extent, break_block) = {
let scope = this.find_loop_scope(expr_span, label);
scope.might_break = true;
if let Some(ref dst) = scope.destination {
block /* this is important! */ = match value {
Some(value) => unpack!(block = this.into(dst, block, value)),
None => this.cfg.push_assign_unit(block, source_info, dst),
};
}
(scope.extent, scope.break_block)
};
this.exit_scope(expr_span, extent, break_block, block);
this.cfg.start_new_block().unit()
This should work because both Extent
and BasicBlock
are Copy AFAIR.
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.
error[E0499]: cannot borrow `*this` as mutable more than once at a time
--> src/librustc_mir/build/expr/stmt.rs:91:45
|
87 | let scope = this.find_loop_scope(expr_span, label);
| ---- first mutable borrow occurs here
...
91 | unpack!(block = this.into(dest, block, value))
| ----------------^^^^--------------------------
| | |
| | second mutable borrow occurs here
| in this macro invocation
...
97 | };
| - first borrow ends here
I don't think we can avoid cloning destination
without unsafe code because the borrow checker doesn't know that this.into
will never pop the loop scope (among other reasons).
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 don't think we can avoid cloning destination without unsafe code because the borrow checker doesn't know that this.into will never pop the loop scope (among other reasons).
Right, for this reason the clone is not unnecessary either, so you ought to remove the comment.
|
||
#![feature(loop_break_value)] | ||
|
||
enum Void {} |
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.
use the !
type instead of defining Void
.
@@ -228,4 +228,5 @@ pub impl Foo for Bar { | |||
register_diagnostics! { | |||
E0472, // asm! is unsupported on this target | |||
E0561, // patterns aren't allowed in function pointer types | |||
E0571, // `break` with an argument in a `while` loop |
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.
What about break
in for
loops? Can we investigate desugaring while
loops into regular loop
instead of adding this special case?
A silent and very breaking change. Might need discussion and crater runs. |
While you are right, I just want to mention that the rules were inconsistent before and MIR building was in the minority, since both the "check loops" pass and label resolution (as of #37360) believed that a |
Hm, I just found out that |
Alright, I removed the conditional loop destination - |
We try hard to avoid syntax breaking changes, this needs to be put under discussion. cc @rust-lang/lang @rust-lang/compiler |
I would have assumed that |
In what sense "silent"? I interpret that to mean "silently changes the semantics of your code, without causing a compilation error" -- is that the case here, or did you have another meaning in mind? |
@nikomatsakis the exact example was provided (I guess I ought’ve’d quoted more context):
This changes semantics of a certain code snippet from something to something else. Though I wonder what the behaviour was before MIR, with the old trans. |
@nagisa Ah, I see! You were not referring to the parsing change, but rather this, which I overlooked:
Interesting. This doesn't actually match my intuition -- but my intuition is weak here, and I suspect that it may change from day to day. But it does seem like the code is (or was) inconsistent in this respect. I think we should make a firm decision here and document it; we could also make some effort to detect code that may change semantics. cc @jseyfried -- in preparing #37360, did you survey different parts of the compiler to get a feeling for what the "majority" of them tried to do with |
☔ The latest upstream changes (presumably #37540) made this pull request unmergeable. Please resolve the merge conflicts. |
We discussed some of the corner cases in the @rust-lang/lang meeting and came to these conclusions:
|
@nikomatsakis
My intuition is that the |
Interesting desugaring, I suppose I didn't consider inverting the condition before. Should we desugar |
@eddyb Yeah, I think that would be a good idea. |
@eddyb heh, for a while we were going to remove @jseyfried I agree that the logical place is that |
@goffrie did you see my previous comment? how do you feel about trying to update the implementation as described? |
Will do. I'll have some time to work on it tomorrow. As far as this PR goes
On Nov 8, 2016 1:57 PM, "Niko Matsakis" notifications@github.com wrote:
|
seems good, maybe open an issue on this topic? |
Alright, I'm now treating the
There's #37576. |
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 basically looks good -- happy to r+ if you address nits plus answer that one question? (Maybe I'm missing something there...). Nice job!
Some(match self.hir_map.expect_expr(loop_id).node { | ||
hir::ExprWhile(..) => LoopKind::WhileLoop, | ||
hir::ExprLoop(_, _, source) => LoopKind::Loop(source), | ||
_ => span_bug!(e.span, "break label resolved to a non-loop"), |
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 usually find it helpful to include some more debugging info in cases like these. e.g.,
ref r => span_bug!(e.span, "break label resolved to a non-loop: {:?}", r),
that said, having the span is probably enough here
} else if let Loop(kind) = self.cx { | ||
Some(kind) | ||
} else { | ||
None |
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.
is this case an error that will be reported elsewhere? if so, can you leave a comment saying so (and maybe hinting at where?)
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'm adding a comment. This case is a break
outside of a loop, which will be caught just below.
} else { | ||
// `break` without argument acts like `break ()`. | ||
e_ty = tcx.mk_nil(); | ||
origin = TypeOrigin::Misc(expr.span); |
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.
We could probably give better errors here if we used a more specific origin, sort of like we do for match arms.
// No way to know whether it's diverging because | ||
// of a `break` or an outer `break` or `return. | ||
self.diverges.set(Diverges::Maybe); | ||
|
||
tcx.mk_nil() | ||
ctxt.unified | ||
} else { | ||
tcx.types.never |
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.
hmm, do we have any tests around this case? I'm a bit surprised we don't get an "unconstrained type variable" for unified
, at least in some cases. What happens if you do something like this, for example?
fn foo() {
loop { };
}
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.
ctxt.unified
is only used if ctxt.may_break
is true, i.e. there was at least one break
, which indicates that the type variable has been unified with something (either the type of a break
argument, or ()
).
☔ The latest upstream changes (presumably #37545) made this pull request unmergeable. Please resolve the merge conflicts. |
☔ The latest upstream changes (presumably #37732) made this pull request unmergeable. Please resolve the merge conflicts. |
Good enough. @goffrie after you rebase, ping me and I will r+. |
Dear @nikomatsakis, |
@nikomatsakis alright, rebased! |
@bors r+ |
📌 Commit 712082e has been approved by |
before it is stabilized, yes -- but in the meantime, you can checkout RFC 1624 |
🔒 Merge conflict |
☔ The latest upstream changes (presumably #37642) made this pull request unmergeable. Please resolve the merge conflicts. |
This implements RFC 1624, tracking issue rust-lang#37339. - `FnCtxt` (in typeck) gets a stack of `LoopCtxt`s, which store the currently deduced type of that loop, the desired type, and a list of break expressions currently seen. `loop` loops get a fresh type variable as their initial type (this logic is stolen from that for arrays). `while` loops get `()`. - `break {expr}` looks up the broken loop, and unifies the type of `expr` with the type of the loop. - `break` with no expr unifies the loop's type with `()`. - When building MIR, `loop` loops no longer construct a `()` value at termination of the loop; rather, the `break` expression assigns the result of the loop. `while` loops are unchanged. - `break` respects contexts in which expressions may not end with braced blocks. That is, `while break { break-value } { while-body }` is illegal; this preserves backwards compatibility. - The RFC did not make it clear, but I chose to make `break ()` inside of a `while` loop illegal, just in case we wanted to do anything with that design space in the future. This is my first time dealing with this part of rustc so I'm sure there's plenty of problems to pick on here ^_^
rebased :/ |
@bors r=nikomatsakis |
Since the @bors r+ |
@bors r- |
@bors r+ |
📌 Commit 9d42549 has been approved by |
Implement the `loop_break_value` feature. This implements RFC 1624, tracking issue #37339. - `FnCtxt` (in typeck) gets a stack of `LoopCtxt`s, which store the currently deduced type of that loop, the desired type, and a list of break expressions currently seen. `loop` loops get a fresh type variable as their initial type (this logic is stolen from that for arrays). `while` loops get `()`. - `break {expr}` looks up the broken loop, and unifies the type of `expr` with the type of the loop. - `break` with no expr unifies the loop's type with `()`. - When building MIR, loops no longer construct a `()` value at termination of the loop; rather, the `break` expression assigns the result of the loop. - ~~I have also changed the loop scoping in MIR-building so that the test of a while loop is not considered to be part of that loop. This makes the rules consistent with #37360. The new loop scopes in typeck also follow this rule. That means that `loop { while (break) {} }` now terminates instead of looping forever. This is technically a breaking change.~~ - ~~On that note, expressions like `while break {}` and `if break {}` no longer parse because `{}` is interpreted as an expression argument to `break`. But no code except compiler test cases should do that anyway because it makes no sense.~~ - The RFC did not make it clear, but I chose to make `break ()` inside of a `while` loop illegal, just in case we wanted to do anything with that design space in the future. This is my first time dealing with this part of rustc so I'm sure there's plenty of problems to pick on here ^_^
This implements RFC 1624, tracking issue #37339.
FnCtxt
(in typeck) gets a stack ofLoopCtxt
s, which store thecurrently deduced type of that loop, the desired type, and a list of
break expressions currently seen.
loop
loops get a fresh typevariable as their initial type (this logic is stolen from that for
arrays).
while
loops get()
.break {expr}
looks up the broken loop, and unifies the type ofexpr
with the type of the loop.break
with no expr unifies the loop's type with()
.()
value attermination of the loop; rather, the
break
expression assigns theresult of the loop.
I have also changed the loop scoping in MIR-building so that the testof a while loop is not considered to be part of that loop. This makes
the rules consistent with resolve: fix label scopes #37360. The new loop scopes in typeck also
follow this rule. That means that
loop { while (break) {} }
nowterminates instead of looping forever. This is technically a breaking
change.
On that note, expressions like
while break {}
andif break {}
nolonger parse because
{}
is interpreted as an expression argument tobreak
. But no code except compiler test cases should do that anywaybecause it makes no sense.
break ()
insideof a
while
loop illegal, just in case we wanted to do anything withthat design space in the future.
This is my first time dealing with this part of rustc so I'm sure
there's plenty of problems to pick on here ^_^