# let-else desugaring ideas ## Recommendation * We did not find a way to express the desired lifetimes with the existing HIR nodes. * We recommend extending the HIR node for `Let` with an `Option` representing the else and desugaring at a later stage. * We explored alternative HIR extensions but they seemed too complex and sketchy enough to hold their weight. ## Schemes we could do today We did not find a desugaring scheme that works today. Here are the options we considered. ### Scheme 0: If-let (this is what we use today) ```rust 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`. ### Scheme 1: Match-in-place ```rust 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 ```rust let temp = &Droppy::default() else { return; } ``` so that `Droppy` drops too soon (its lifetime is no longer extended). ### Scheme 2: Bind temporaries to lets You could imagine doing a "deep" desugaring so that, somehow ```rust let temp = &Droppy::default() else { return; } ``` becomes ```rust 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". ### Scheme 3: use DropTemps 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. ## Solutions, if we are willing to change HIR ### A. Extend HIR with let-else as a native construct, desugar at THIR * HIR contains let-else as a native construct * The temporary lifetime code can easily manage that in the same way as a let is managed * Desugar at THIR point to a match * This may simplify error messages for "`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. ### B. Extend HIR with "extended-temporary" as a node * HIR gets a new ExtendedTemporary node * it is meant to be used when we know that values matched against this expression will be stored into let bindings at the block scope * The temporary lifetime code uses that as a "root" point, along with the right-hand-side of a let * so we desugar to ```rust let pat = expr1 else { expr2 }; // expr3...; let (bindings...) = match extended-temporary(expr1) { pat => (bindings...), _ => expr2 }; expr3... ``` and things work. ## Side note We are unfortunately inconsistent between if and if-let https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=765846eddafa1e5dc049c336754980b4 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. ## Appendix: Examples Here is some Rust code where we were playing around with different desugarings to see if they were equivalent. ```rust #![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"); } } ```