[Pre-Pre-RFC] Yet another discussion of if-let and while-let lifetime

This might be a breaking change, although it may not affect too much code.

There is always someone talking about the question that if-let and while-let lives too long.

If we focus on if clause and if-let clause, a much strange things could happen:

// Works
if           x.borrow().is_negative(){ *x.borrow_mut() = 0 }else{*x.borrow_mut() = 0} 
// Panics
if let false=x.borrow().is_negative(){ *x.borrow_mut() = 0 }else{*x.borrow_mut() = 0} 

It seems that, temporary variables' drop times should not be so inconsistent.

I met such problem a year ago, and finally found what I want. There are a bunch of people want to drop temporary variables as soon as possible, and others want to keep temporary variables until if-let and while-let finishes. Why not add some keyword to these two different action?

current:

let x = RefCell::new(0i32);
// Succeeds
match {let tmp = x.borrow().is_negative();tmp} {
    _ => { *x.borrow_mut() = 0 },
};
// Panics
match x.borrow().is_negative() {
    _ => { *x.borrow_mut() = 0 },
}
// Panics, too
if let flag=x.borrow().is_negative(){ *x.borrow_mut() = 0 }

method 1: add if move / while move / match move

// `move x=expr()` equals to `let x={let tmp=x.expr();tmp}` in if-let and while-let clauses
// `match move expr()` equals to `match {let tmp=x.expr();tmp}` in match-clauses.
match move x.borrow().is_negative() {
    _ => { *x.borrow_mut() = 0 },
}
if move flag=x.borrow().is_negative(){ *x.borrow_mut() = 0 }

method 2, change the default behavior of lf-let and while-let (also match), use a new keyword (e.g., ref,extend, lazy, etc.) to delay the drop execution.

if let flag=x.borrow().is_negative(){
    // x.borrow() drops as soon as possible, thus calling `x.borrow_mut()` is OK.
}
if lazy x.borrow().is_negative(){
    // x.borrow would only drop after the whole block is executed.
    // thus, it would panic by calling a x.borrow_mut().
}
match x.borrow().is_negative(){_=>{*x.borrow_mut() = 0}}//will not panic since `x.borrow()` dropped before the execution of `x.borrow_mut()`
match lazy x.borrow().is_negative(){_=>{
// here, x.borrow() still alive.
}}

I prefer method 2 since most of the cases, the temporary variables are unnecessary for programmers, but method 2 is a breaking change. If we do not want to modify a lot, method 1 might be acceptable.

Are there any disadvantages and limitations of such modification?

(I'm speaking as myself and not for wg-grammar.)

move $expr isn't available[1] in the grammar, because move closures exist; if you wrote match move || foo() it would be ambiguous whether you mean match (move || foo()) or match move (|| foo()).

The clippy::significant_drop_in_scrutinee lint should lint against this. If it doesn't, open an issue (or PR — it's just adding an attribute to the type).


  1. You can take the cop-out direction that match move || is always a move closure, since temporary lifetime extension doesn't (?) apply when the scrutinee is a closure expression, but this is just saying the ambiguity doesn't matter, not actually addressing it. ↩ī¸Ž

1 Like

The fact that temporaries live for the entire statemment is very important for pattern matching and breaking that will not cause just slight breakage, I think. I am actually opposed to this proposal since I think the motivation isn't strong enough; the example with is_negative() is not convincing because you can just use if. Yes, symmetry is nice, but being able to introduce bindings to temporaries is much more important. I do acknowledge there are cases where if is not enough, but I think they are rare enough. Also, the main problem is finding the problem, since the fix is pretty trivial once you know it. So unless we're going to change the default (which I think is going to break lots of code), this is not going to help at all.

I think the solution is not a language construct, but more tools (linters etc., maybe even runtime tools) to find this problem, and more documentation about this.

3 Likes

I think, as @matthew-mcallister stated in Lifetime in match if let and while let - #6 by matthew-mcallister, this is a genuine footgun: yet another issue was just filed about it this morning.

There are temporaries which need to live for the entire statement, and there are temporaries which are not. This pre-RFC proposes adding a keyword for the programmer to control dropping earlier, but I don't see that as warranted (one could always just bind the temporary within a block); nevertheless it should be possible to analytically determine whether the temporary can be dropped early (and, if so, do it). I've dug around a little, but can't find any fleshed out proposals for doing this, but I can see it being a useful enhancement to the language.

Edit: having read the linked threads a bit more thoroughly, and in particular Lifetime in match if let and while let - #4 by CAD97, I can see now that an automated analysis will not be able to make this determination and any such change could break existing code. It probably then is a no-go without some explicit language construct (which I still think is unnecessary).

I once thought I wrote strong reasons that assist my opinion, but as the code

if let Some(x)=vec_mutex.lock().unwrap().get_mut()

failed to compile, I finally recognized that, it is not only something related to if-let, but also something to NLL.

think about this question: when should temporaries drop?

let x=vec_mutex.lock().unwrap().get_mut(0);//failed to compile
/*
rustc --edition 2021 test.rs  -o test && ./test
error[E0716]: temporary value dropped while borrowed
 --> test.rs:4:11
  |
4 |     let x=vec_mutex.lock().unwrap().get_mut(0);//failed to compile
  |           ^^^^^^^^^^^^^^^^^^^^^^^^^           - temporary value is freed at the end of this statement
  |           |
  |           creates a temporary which is freed while still in use
5 |     println!("{}",x.unwrap());
  |                   - borrow later used here
  |
  = note: consider using a `let` binding to create a longer lived value

error: aborting due to previous error

For more information about this error, try `rustc --explain E0716`.
*/

This could be a old question.

If we could determine the correct drop time of let-clauses, we might have no difficult deal with if-let clauses.

But now, who against "drop variables after whole if-let clause" could only say:

// Works, documented in https://doc.rust-lang.org/stable/reference/destructors.html#temporary-scopes. thanks for @chrefr 's comment.
if           x.borrow().is_negative(){ *x.borrow_mut() = 0 }else{*x.borrow_mut() = 0} 
// Panics, documented in https://doc.rust-lang.org/stable/reference/expressions.html#temporaries
if let false=x.borrow().is_negative(){ *x.borrow_mut() = 0 }else{*x.borrow_mut() = 0};

It is documented:

https://doc.rust-lang.org/stable/reference/destructors.html#temporary-scopes

1 Like