-
Notifications
You must be signed in to change notification settings - Fork 13.1k
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
Proposal: stabilize if_let_rescope
for Edition 2024
#131154
Comments
@rustbot labels +T-lang +A-edition-2024 |
Note that I'm not very good at analysing crater run results (not just what broke, but also what happened to the crates that don't build for other reasons), but I think it would be important to have a crater analysis that includes the exact number of breakages and crates that didn't build (or otherwise what the analysis did not cover), and if feasible classification of the nature of breakages. Regarding the lint, as mentioned it's not machine-applicable due to overlapping suggestions not possible in the current fix application infra, but also as is usual with proc-macro detection, any crates that forwards user spans with no distinguishing syntax context will cause |
FCP for stabilization: @rfcbot merge |
Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns. |
@rfcbot reviewed |
@rfcbot reviewed Copying my reasoning from the meeting notes for posterity: This introduces a disparity between match and if-let, which I don't like, but it moves towards earlier drop, which I do like, and it also means that struct DropOrder;
impl DropOrder {
fn test(&self) -> bool {
false
}
}
impl Drop for DropOrder {
fn drop(&mut self) {
eprintln!("drop");
}
}
fn main() {
if DropOrder.test() {
// ^^^^^^^^^ dropped before entering else
eprintln!("this body");
} else {
eprintln!("else body");
}
} You can see the behavior here, it prints "drop" first. In short, this is moving us closer to the world I want, which is one in which match is able to drop more eagerly. |
@rfcbot reviewed I'm confident in the semantics proposed, given the above reasoning, and that we can make the edition experience good enough for our users, though it might require some dialing in before we ship. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
Also, when we document this, we should point out that while it's possible to have surprises with |
@rustbot labels -I-lang-nominated We discussed this in the meeting today... and it's now in FCP. |
The final comment period, with a disposition to merge, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. This will be merged soon. |
sorry for the long comment Why does this change need to break the following snippet? https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=08a43c178a203ce952e9c1aee733646b fn main() {
use_value(if let Some(value) = droppy().borrow() {
value
} else {
return;
})
} looking at the generated MIR we get the following fn main() -> () {
let mut _0: ();
let mut _1: std::option::Option<&Droppy>;
let mut _2: &Droppy;
let _3: Droppy;
let mut _4: isize;
scope 1 {
debug value => _5;
let _5: &Droppy;
}
bb0: {
_3 = droppy() -> [return: bb1, unwind continue];
}
bb1: {
_2 = &_3;
_1 = Droppy::borrow(move _2) -> [return: bb2, unwind: bb7];
}
bb2: {
_4 = discriminant(_1);
switchInt(move _4) -> [1: bb3, 0: bb4, otherwise: bb9];
}
bb3: {
_5 = copy ((_1 as Some).0: &u32);
drop(_3) -> [return: bb5, unwind continue];
}
bb4: {
drop(_3) -> [return: bb6, unwind continue];
}
bb5: {
_0 = use_value::<&Droppy>(copy _5) -> [return: bb6, unwind continue];
}
bb6: {
return;
}
bb7 (cleanup): {
drop(_3) -> [return: bb8, unwind terminate(cleanup)];
}
bb8 (cleanup): {
resume;
}
bb9: {
unreachable;
}
} where in edition 2021 this instead uses the following, only dropping the temporaries from the bb2: {
_4 = discriminant(_1);
switchInt(move _4) -> [1: bb3, 0: bb4, otherwise: bb9];
}
bb3: {
_5 = copy ((_1 as Some).0: &Droppy);
_0 = use_value::<&Droppy>(copy _5) -> [return: bb5, unwind: bb7];
}
bb4: {
drop(_3) -> [return: bb6, unwind continue];
}
bb5: {
drop(_3) -> [return: bb6, unwind continue];
} What I would have expected is that this feature does not change the drop location of |
e.g. looking at a minimized version of https://github.com/rust-lang/rust/blob/master/tests/ui/feature-gates/feature-gate-if-let-rescope.rs: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=68208167c29c3eb875339a6e5ed90e15 I would expect the generated MIR in edition 2024 (diff from current edition 2021 output) to be the following: fn main() -> () {
let mut _0: (); // return place in scope 0 at src/main.rs:17:10: 17:10
let mut _1: A; // in scope 0 at src/main.rs:18:9: 18:14
let _2: (); // in scope 0 at src/main.rs:19:5: 22:6
let mut _3: std::option::Option<B<'_, A>>; // in scope 0 at src/main.rs:19:19: 19:24
let mut _4: &mut A; // in scope 0 at src/main.rs:19:19: 19:20
let mut _5: isize; // in scope 0 at src/main.rs:19:12: 19:16
let _6: std::option::Option<B<'_, A>>; // in scope 0 at src/main.rs:21:9: 21:14
let mut _7: &mut A; // in scope 0 at src/main.rs:21:9: 21:10
scope 1 {
debug a => _1; // in scope 1 at src/main.rs:18:9: 18:14
scope 2 {
}
}
bb0: {
StorageLive(_1); // scope 0 at src/main.rs:18:9: 18:14
_1 = A; // scope 0 at src/main.rs:18:17: 18:18
FakeRead(ForLet(None), _1); // scope 0 at src/main.rs:18:9: 18:14
StorageLive(_2); // scope 1 at src/main.rs:19:5: 22:6
StorageLive(_3); // scope 2 at src/main.rs:19:19: 19:24
StorageLive(_4); // scope 2 at src/main.rs:19:19: 19:20
_4 = &mut _1; // scope 2 at src/main.rs:19:19: 19:20
_3 = A::f(move _4) -> [return: bb1, unwind: bb10]; // scope 2 at src/main.rs:19:19: 19:24
// mir::ConstOperand
// + span: src/main.rs:19:21: 19:22
// + const_: Const { ty: for<'a> fn(&'a mut A) -> Option<B<'a, A>> {A::f}, val: Value(A::f) }
}
bb1: {
StorageDead(_4); // scope 2 at src/main.rs:19:23: 19:24
PlaceMention(_3); // scope 2 at src/main.rs:19:19: 19:24
_5 = discriminant(_3); // scope 2 at src/main.rs:19:19: 19:24
switchInt(move _5) -> [0: bb2, otherwise: bb4]; // scope 2 at src/main.rs:19:12: 19:16
}
bb2: {
falseEdge -> [real: bb3, imaginary: bb4]; // scope 2 at src/main.rs:19:12: 19:16
}
bb3: {
_2 = const (); // scope 2 at src/main.rs:19:25: 20:6
goto -> bb7; // scope 1 at src/main.rs:19:5: 22:6
}
bb4 {
+ drop(_3) -> [return: bb4Cont, unwind: bbWhoCares];
+ }
+
+ bb4Cont: {
StorageLive(_6); // scope 1 at src/main.rs:21:9: 21:14
StorageLive(_7); // scope 1 at src/main.rs:21:9: 21:10
_7 = &mut _1; // scope 1 at src/main.rs:21:9: 21:10
_6 = A::f(move _7) -> [return: bb5, unwind: bb9]; // scope 1 at src/main.rs:21:9: 21:14
// mir::ConstOperand
// + span: src/main.rs:21:11: 21:12
// + const_: Const { ty: for<'a> fn(&'a mut A) -> Option<B<'a, A>> {A::f}, val: Value(A::f) }
}
bb5: {
StorageDead(_7); // scope 1 at src/main.rs:21:13: 21:14
drop(_6) -> [return: bb6, unwind: bb9]; // scope 1 at src/main.rs:21:14: 21:15
}
bb6: {
StorageDead(_6); // scope 1 at src/main.rs:21:14: 21:15
_2 = const (); // scope 1 at src/main.rs:20:12: 22:6
goto -> bb7; // scope 1 at src/main.rs:19:5: 22:6
}
bb7: {
+ // In case we dropped `_3` in `bb4` we'd drop it here again, however, that
+ // should be fine as `Drop` before drop elaboration is conditional on whether
+ // the variable has already been dropped. This would cause `if let` to always
+ // require drop flags tracking which may cause perf issues, but I would like to
+ // at least see some testing for that
drop(_3) -> [return: bb8, unwind: bb10]; // scope 1 at src/main.rs:22:6: 22:7
}
bb8: {
StorageDead(_3); // scope 1 at src/main.rs:22:6: 22:7
StorageDead(_2); // scope 1 at src/main.rs:22:6: 22:7
_0 = const (); // scope 0 at src/main.rs:17:11: 23:2
StorageDead(_1); // scope 0 at src/main.rs:23:1: 23:2
return; // scope 0 at src/main.rs:23:2: 23:2
}
bb9 (cleanup): {
drop(_3) -> [return: bb10, unwind terminate(cleanup)]; // scope 1 at src/main.rs:22:6: 22:7
}
bb10 (cleanup): {
resume; // scope 0 at src/main.rs:17:1: 23:2
}
} |
Thank you for the suggestion @lcnr ! I admit that making the double drop would work. It would perfectly retain the current Edition 2021 On the other hand, however, the feature gate compute(if let Some(&value) = mutex.lock().unwrap().get(&key) {
// here we take a copy and our business with the lock is technically finished
value
} else {
// we can drop the lock guard here in the failure path ...
return;
}); // ... but in the happy path, throughout the span of `compute` invocation, the lock is held for no extra benefit If indeed a borrow checker error is raised, this at least provides an opportunity to users that a resource is held beyond its use for the decision-making here and it encourages a better, clearer structure of the program as an alternative. if let Some(value) = mutex.lock().unwrap().get(&key) {
// value: &V
compute(value);
} else {
return;
} With Edition 2021 and prior, one may attempt to say that to answer this question, it sufficed to "just find the deepest enclosing block and point at its end," or "look out for the semicolon at the end of the statement." In practice, this idiom does not work reliably. One example is when |
We need to reach a decision ASAP (really need to already have reached a decision), so I've been thinking over @lcnr's alternative proposal to drop temporaries at the end of the "then" arm (when the pattern matches) and before the "else" arm (when the pattern does not match). I certainly see the appeal and it can be more ergonomic. However, overall it doesn't seem like the right call to me, as it is not consistent with any existing forms and errs on the side of introducing more runtime bugs. Let me explain. Background: design axiomsAt the end of the day, there is no "correct" set of temporary rules. What seems obvious in one example ("clearly the temporary should live longer...") because rather unobvious in another ("what? the lock is being held for the entire Unfortunately we never got as far as deciding on an official set of design axioms around temporaries -- I still think that's worth doing -- but my personal take is that the axioms should be...
Let me walk through these and their rationale and implications. Why predictable? Without this tenet, we could make lifetime rules predicated on lifetime inference or some such, but we decided that was a bad idea early on because (a) the borrow check doesn't have all the info it needs to make an informed decision, such as references from unsafe code; and (b) users should not have to model the borrow check results to know when a destructor will run. Why is shorter safer? Shorter lifetimes produce borrow check errors, which is annoying, but longer lifetimes produce deadlocks and panics at runtime, which is worse. This is a pretty common source of bugs—take a look at [Understanding Memory and Thread Safety Practices and Issues in Real-World Rust Programs], which found that 30 out of 38 of the deadlocks they found were caused by double locking, with all their examples showing cases of temporary lifetimes. "Rust's complex [temporary] lifetime rules together with its implicit unlock mechanism make it harder for programmers to write blocking-bug-free code." (the word "temporary" is inserted by me, but what other parts of lifetime rules are complicated?) Finally, yes, we would like to be convenient where possible. In my original 2014 blog post I laid out two patterns
Both of those patterns are pretty ingrained in Rust code by now. Analyzing this case in terms of the axiomsThe question at hand is, given an if let Some(r) = temporary().as_ref() { A } else { B } ...when should we drop this So how should we choose between those three options, based on the axioms? It seems clear that "erring on the side of shorter" matches Rust2024.1 best but "DWIM" matches Rust 2024.2 best. (Rust2021 fails on both of those axioms.) But what about the first axiom, predictability? That's a bit more complicated. In terms of predictability, if let Some(r) = temporary().as_ref() { A } else { B }
if temporary().as_ref().is_some() { A } else { B } // assume that `r` is dead in the above
{ let Some(r) = temporary else { B }; A }
{ match temporary().as_ref() { Some(r) => A, None => B } } Here is how drops occur in the three variants:
Based on this, I argue that both Rust2021 and Rust2024.1 are more predictable than Rust 2024.2, which introduces a new combo. Are we really predictable?TBH, our temporary rules are syntax driven, but I don't thnk they are all that predictable. This change helps, but maybe there is a better solution we can pursue going forward. Also, after change, Looking forward, I feel like we need to do something to improve the temporary rules, and I wonder if "DWIM" is just too much in tension with predictability. I kind of like the idea of introducing some kind of explicit way to say "introduce a let binding for this above the current statement", like |
My reasoning for continuing to allow this was two-fold:
This leads me to believe we should have a proper rustc lint for these cases1 by relying on a The stabilization PR for the 2024.1 approach is ready for merge in #131984. I am still not totally convinced by the current approach. Given that that's a lang question, I won't block that PR though Footnotes
|
…rescope, r=traviscross,lcnr Stabilize if_let_rescope Close rust-lang#131154 Tracked by rust-lang#124085
…rescope, r=traviscross,lcnr Stabilize if_let_rescope Close rust-lang#131154 Tracked by rust-lang#124085
…rescope, r=traviscross,lcnr Stabilize if_let_rescope Close rust-lang#131154 Tracked by rust-lang#124085
Hmm, @traviscross pointed out that my comment was mistaken in some of the particulars, which I find mildly disturbing. Side note but the challenge in communicating precisely here goes to show the potential value of a spec, this may be a useful case study. In any case, I misunderstood the behavior. The actual definition is not what I thought. My example cannot correctly capture the full details. A better example are these four "rough equivalents": // if-let
test(if let Some(x) = maybe_droppy().as_ref() { A } else { B }, C);
// if (assume the `x` from `Some(x)` above is not used in A or B, since otherwise you'd get errors)
test(if maybe_droppy().is_some() { A } else { B }, C);
// let-else (errors if `x` is live in Rust 2021, actually)
let Some(x) = maybe_droppy().as_ref() else { B };
test(A, C);
// match
test(match maybe_droppy().as_ref() { Some(_) => A, None => B }, C); The key point here is that we have A and B but then we have other content C that is within the "temporary scope" of the call to
(*) it will be dropped before if take the else branch, after otherwise Reviewing the table like this, I am feeling uncertain (much as I hate to admit it) as to what is best. There was at some point a rule that @m-ou-se, @dingxiangfei2009, and I were considering that had to do with thinking about the answer to the questions:
I feel like we can refine the axioms to take this into account: e.g., if it is impossible (as in the The trouble lies in the "possible" cases, which often have to do with match arms -- basically we have to know the types involved to know whether Question for @traviscross and @dingxiangfei2009 -- how "optional" is this change? I feel very strongly that we need to change the drop rules for tail expressions; this seems like a "nice to have". |
Also, @traviscross can you verify my comment there :) |
So, I was wrong in some of the particulars, but I still do not prefer Rust2024.2, and my reasoning is that it seems like the least predictable option (violates axiom 1). It seems to me that the real impact of this change is to align the temporary rules for Offline, @traviscross pointed out to me that part of the reasoning here is to unblock let-chains -- i.e., you can view |
For our reference, here's a worked-through example of the behavior compared with the various refactorings discussed: |
Probably what is most interesting to me, in that work-through, is comparing the behavior of if let Ok(_v1) = temporary(p).as_ref() {
a()
} else {
b()
} ...with: 'top: {
let Ok(_v1) = temporary(p).as_ref() else {
break 'top b()
};
a()
} In the former, even with I know Niko has in the past argued for dropping the temporary immediately after considering the scrutinee. The What we're doing here, with the |
@rustbot labels +I-lang-nominated |
Rollup merge of rust-lang#131984 - dingxiangfei2009:stabilize-if-let-rescope, r=traviscross,lcnr Stabilize if_let_rescope Close rust-lang#131154 Tracked by rust-lang#124085
still lang-nominated, reopening |
I think let chains are motivation enough for moving off the 2021 semantics in 2024. While making the change it would be great to simplify the mental model if we can, but arguably both 2024.1 and 2024.2 simplify the mental model in different ways. The argument that this can lead to more predictable code is a strong one for me. I would also argue that if-let behaving like On the other side we have the argument that 2024.2 more closely matches what a user means in some cases ("DWIM"), and the fact that 2021.1 is breaking the existing behavior and thus breaking more code. On the "DWIM" side, looking at the examples of code that would be affected by the 2024.1 change so far doesn't motivate me to change the rules to 2024.2. An if-let nested inside of a function call isn't a case I think we should rotate on very much. Maybe other patterns would make me change my mind but so far I haven't been able to come up with anything very compelling. On the breaking change front, data would be helpful, but I still feel unable to evaluate. I haven't had time to dig in, but just looking at the crater triage comments I'm having trouble understanding what we learned. It seems like the results are still a bit ambiguous. So far I prefer 2024.1 unless I see evidence that we're breaking a significant amount of "idiomatic" code in an annoying and unnecessary way. |
+1. I'm happy with what's now in Rust 2024, and also with the design axioms Niko outlined. |
@rustbot labels -I-lang-nominated We discussed this in lang triage today. Through that discussion, we confirmed that we're happy with the Rust2024.1 behavior that has now landed in Rust 2024 (nightly). In doing this, we were motivated by the design axioms Niko noted above. We were motivated by how this change moves toward better alignment with We discussed how we may, or may not, want to eventually fully align with We acknowledged in our discussion that this is a tough call in many respects. |
Close #13679 changelog: [`if_let_mutex`]: disable lint from Edition 2024 since [stabilized if_let_rescope ](rust-lang/rust#131154)
Tracked by #124085
Summary
if_let_rescope
is an implementation of the #124085 through #107251. This change did not come with a RFC. This proposal will aim to describe the change to the language fully.if let
and the current unstabilizedif let
-chain has a caveat to the assignment of lifetime rules regarding temporary values generated from initializer expressions.Instead, after stabilizing this, we have this effect.
This will allow us to be more consistent with
let $pat = $expr else { $diverge; }
where temporaries from$expr
are dropped before entering the$diverge
branch in today's language.Given that this is an Edition breaking change, a lint is developed and tested in the field with crater runs.
What is being proposed for stabilization
In #107251, a new kind of scope data
IfThenRescope
is introduced and gated behind the edition and this feature gate. Upon stabilization, this scope will work like a terminating scope for variables and temporary values generated from thelet
-bindings.What is more significant is the case where
if let
is used as a non-trivial value-producing subexpression in a larger evaluation context, in which it will have semantic impact to lifetimes and object drop orders.Here are some breaking changes that will be introduced by stabilization.
A marginal amount of crates are discovered by previous crater runs in which this change leads to rejection from the borrow checker, because
droppy()
does not live long enough.The migration lint that is now implemented on
master
suggests a rewrite to restore the old semantics perfectly, but not automatically applicable due to the current limitation of machine applicable lints and our internal support for overlapping suggestion spans.A more subtle and silent breakage could be involving use of types with significant drop implementation and synchronization primitives such as
Mutex
es.Although the crater run did not find breakage in runtime behaviour through
cargo test
, we proceed with developing lints to detect those cases and rewrite them intomatch
es. Here the suggestion notes are machine applicable because we can coalesce the overlapping spans in one pass.In the latest crater run we found a marginal population of crates that are truly impacted by this lint, revealing corner cases with the lint which are now properly handled and tested.
Future interactions
Currently there is another possible use of pattern matching in control flows that complements
if let
chain is theis
operator proposed in rust-lang/rfcs#3573. This change both runs along the same principle of no unnecessary binding and long lifetime beyond the applicable scope from a pattern-matching predicate. With this change, the implementation ofis
would be believably less error-prone due to the unexpectedly long lifetimes.This leaves
match
being the only exception to this principle. In fact, our mitigation and migration strategy is based on it. It might remains as an acceptable exception and further be publicized and inadvertently advertised as such.In general, this change is not expected to influence or hinder major language design.
The text was updated successfully, but these errors were encountered: