Let
with an Option
representing the else and desugaring at a later stage.
We did not find a desugaring scheme that works today. Here are the options we considered.
let pat = expr1 else { expr2 }; //
expr3...;
if let pat = expr1 {
expr3...
} else {
expr2
}
This doesn't work because temporaries from expr1
are in scope over all of expr3
.
let pat = expr1 else { expr2 }; //
expr3...;
let (bindings...) = match expr1 { pat => (bindings...), _ => expr2 };
expr3...
This doesn't work because let pat = expr1
evaluates expr1
with (potentially) extended temporary lifetimes, but match expr1
does not, and so this changes the behavior of an example like
let temp = &Droppy::default() else { return; }
so that Droppy
drops too soon (its lifetime is no longer extended).
You could imagine doing a "deep" desugaring so that, somehow
let temp = &Droppy::default() else { return; }
becomes
let temp0 = Droppy::default();
let temp = &temp0 else { return; }
But to do this correctly requires (a) a knowledge of which temporaries are extended lifetime, which we don't want to have available at HIR construction time (it is available today, but shouldn't be for RFC 66), and (b) it is wicked complex and not truly a "desugaring", more like a "compiling".
https://github.com/rust-lang/rust/pull/94012 proposed using DropTemps, which causes the lifetime to be made shorter, but this does not work for the cases where you DO want an extended lifetime.
else
clause of let...else
does not diverge"Niko's opinion is that we desugar too much at HIR construction time and should do less.
let pat = expr1 else { expr2 }; //
expr3...;
let (bindings...) = match extended-temporary(expr1) { pat => (bindings...), _ => expr2 };
expr3...
and things work.
We are unfortunately inconsistent between if and if-let
this seems like a wart, but you can sort of rationalize it as "the data is potentially being stored into bindings now and so it lives till the end of the if-let (versus an if, which always drops temporary before entering the body)". Note that the if-let/match behavior is a common footgun though and we should lint.
Here is some Rust code where we were playing around with different desugarings to see if they were equivalent.
#![feature(let_else)]
#[derive(Default)]
struct Droppy(u32);
impl Drop for Droppy {
fn drop(&mut self) {
println!("I dropped");
}
}
fn main() {
// Scheme 1
{
let _ = Droppy::default().0 else { return };
println!("Drop should have occurred");
}
{
let () = match Droppy::default().0 { _ => (), _ => return };
println!("Drop should have occurred");
}
// Original
{
let temp = &Droppy::default() else { return };
println!("Drop should not have occurred");
}
// Scheme 1 (doesn't work)
{
let temp = match &Droppy::default() { x => x, _ => return };
println!("Drop should not have occurred");
}
// Scheme 2 (works, but too complex)
{
let foo = &Droppy::default();
let temp = match foo { x => x, _ => return };
println!("Drop should not have occurred");
}
}