Change:
results in I am not sure if this issue exists only in combination with impl Trait of if it can be triggered in some other ways as well, haven't managed to get this bug some other way. cc do you know whether this behavior is known already?
The bug is not known to me. cc who has improved the borrow checking behavior. The suggestion is also weird.
The let-else actually desugars to this if-let: And you . However the suggestion actually makes sense for if-let, because putting a semicolon afterwards does indeed fix it. The suggestion is wrong for the let-else because let-else's desugaring is always in tail position in its enclosing block, any statements thereafter get moved into the if-branch and so no amount of semicolons can take the desugaring out of tail position.
Also note that the old desugaring, which was , does not suffer this particular problem because the temporary has a name and lives outside the tail position expression. That way there is no ordering problem with the ends of x's and the unnamed temporary's lifetimes. So a few things: a named temporary like that is a good suggestion to make instead for let-else, and potentially for if-let as well. presence or absence of a semicolon on a tail position expression of type () having that much significance is pretty bad. Arguably it should not be significant whatsoever. is being a bit obtuse. It isn't explained very well even for the if-let version why the unnamed temporary is still alive when x is being dropped. Conceptually I think borrowck is trying to say that the real implicit return expression is actually that empty space inside the braces of type (). I don't know. if it did explain better, is there a technical reason why borrowck can't figure out that it could simply drop the unnamed temporary first? Because that would be ideal.
Could we just wrap the in the let-else desugaring with a ?
Well isn't that convenient! Yeah, I suppose we could.
I've just tried this, and it also suffers the same thing did, with the unreachable code warnings when the desugaring ends up with a return in both branches. It is probably complaining about the DropTemps expression itself being unreachable. We can't slap an on it lest it ignore all unreachable code inside, but I think we can disable the emission of such warnings on any DropTemps in typeck's . Right? (Edit, maybe instead in , because this DropTemps will rarely if ever be such that typeck could warn about it.) Example warning:
Ah cool, turned out to be pretty easy.
This compiles successfully as of , which includes . So I'd say it's fixed, but it needs a test before the issue can be closed. includes a test for this, but it's not merged and probably not going to be. The other test in that PR also looks interesting.

Discussion:
initializer_span, else_block, visibility_scope, <ins> *remainder_scope, </ins> remainder_span, pattern, )

initializer_span: Span, else_block: &Block, visibility_scope: Option<SourceScope>, <ins> remainder_scope: region::Scope, </ins> remainder_span: Span, pattern: &Pat<'tcx>, ) -> BlockAnd<()> { <del> let scrutinee = unpack!(block = self.lower_scrutinee(block, init, initializer_span)); let pat = Pat { ty: init.ty, span: else_block.span, kind: Box::new(PatKind::Wild) }; let mut wildcard = Candidate::new(scrutinee.clone(), &pat, false); self.declare_bindings( visibility_scope, remainder_span, pattern, ArmHasGuard(false), Some((None, initializer_span)), ); let mut candidate = Candidate::new(scrutinee.clone(), pattern, false); let fake_borrow_temps = self.lower_match_tree( block, initializer_span, pattern.span, false, &mut [&mut candidate, &mut wildcard], ); // This block is for the matching case let matching = self.bind_pattern( self.source_info(pattern.span), candidate, None, &fake_borrow_temps, initializer_span, None, None, None, ); // This block is for the failure case let failure = self.bind_pattern( self.source_info(else_block.span), wildcard, None, &fake_borrow_temps, initializer_span, None, None, None, ); </del> <ins> let (matching, failure) = self.in_if_then_scope(remainder_scope, |this| { let scrutinee = unpack!(block = this.lower_scrutinee(block, init, initializer_span)); let pat = Pat { ty: init.ty, span: else_block.span, kind: Box::new(PatKind::Wild) }; let mut wildcard = Candidate::new(scrutinee.clone(), &pat, false); this.declare_bindings( visibility_scope, remainder_span, pattern, ArmHasGuard(false), Some((None, initializer_span)), ); let mut candidate = Candidate::new(scrutinee.clone(), pattern, false); let fake_borrow_temps = this.lower_match_tree( block, initializer_span, pattern.span, false, &mut [&mut candidate, &mut wildcard], ); // This block is for the matching case let matching = this.bind_pattern( this.source_info(pattern.span), candidate, None, &fake_borrow_temps, initializer_span, None, None, None, ); // This block is for the failure case let failure = this.bind_pattern( this.source_info(else_block.span), wildcard, None, &fake_borrow_temps, initializer_span, None, None, None, ); this.break_for_else(failure, remainder_scope, this.source_info(initializer_span)); matching.unit() }); </ins> // This place is not really used because this destination place // should never be used to take values at the end of the failure // block.

} drops.add_entry(block, drop_idx); <del> // `build_drop_tree` doesn't have access to our source_info, so we </del> <ins> // `build_drop_trees` doesn't have access to our source_info, so we </ins> // create a dummy terminator now. `TerminatorKind::Resume` is used // because MIR type checking will panic if it hasn't been overwritten. self.cfg.terminate(block, source_info, TerminatorKind::Resume);

<ins> // run-pass // // from issue #93951, where borrowck complained the temporary that `foo(&x)` was stored in was to // be dropped sometime after `x` was. It then suggested adding a semicolon that was already there. #![feature(let_else)] use std::fmt::Debug; fn foo<'a>(x: &'a str) -> Result<impl Debug + 'a, ()> { Ok(x) } fn let_else() { let x = String::from("Hey"); let Ok(_) = foo(&x) else { return }; } fn if_let() { let x = String::from("Hey"); let _ = if let Ok(s) = foo(&x) { s } else { return }; } fn main() { let_else(); if_let(); } </ins>

// run-pass #![feature(let_else)] <ins> use std::fmt::Display; use std::rc::Rc; </ins> use std::sync::atomic::{AtomicU8, Ordering}; static TRACKER: AtomicU8 = AtomicU8::new(0);

} } <ins> fn foo<'a>(x: &'a str) -> Result<impl Display + 'a, ()> { Ok(x) } </ins> fn main() { assert_eq!(TRACKER.load(Ordering::Acquire), 0); let 0 = Droppy::default().inner else { return }; assert_eq!(TRACKER.load(Ordering::Acquire), 1); println!("Should have dropped 👆"); <ins> { // cf. https://github.com/rust-lang/rust/pull/99518#issuecomment-1191520030 struct Foo<'a>(&'a mut u32); impl<'a> Drop for Foo<'a> { fn drop(&mut self) { *self.0 = 0; } } let mut foo = 0; let Foo(0) = Foo(&mut foo) else { *&mut foo = 1; todo!() }; } { let x = String::from("Hey"); let Ok(s) = foo(&x) else { panic!() }; assert_eq!(s.to_string(), x); } { // test let-else drops temps after statement let rc = Rc::new(0); let 0 = *rc.clone() else { unreachable!() }; Rc::try_unwrap(rc).unwrap(); } { let mut rc = Rc::new(0); let mut i = 0; loop { if i > 3 { break; } let 1 = *rc.clone() else { if let Ok(v) = Rc::try_unwrap(rc) { rc = Rc::new(v); } else { panic!() } i += 1; continue }; } } { // test let-else drops temps before else block // NOTE: this test has to be the last block in the `main` // body. let rc = Rc::new(0); let 1 = *rc.clone() else { Rc::try_unwrap(rc).unwrap(); return; }; unreachable!(); } </ins> }