Difference between temporary scope and "normal" scope

Reference about temporary scopes:

The temporary scope of an expression is the scope that is used for the temporary variable that holds the result of that expression when used in a place context, unless it is promoted.

Apart from lifetime extension, the temporary scope of an expression is the smallest scope that contains the expression and is one of the following:

  • The entire function.
  • A statement.
  • The body of an if, while or loop expression.
  • The else block of an if expression.
  • The condition expression of an if or while expression, or a match guard.
  • The body expression for a match arm.
  • The second operand of a lazy boolean expression.

When given code below:

struct PrintOnDrop(&'static str);

impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("{}", self.0);
    }
}

let x = PrintOnDrop("first operand").0 == "" || PrintOnDrop("second operand").0 == "";

Why isn't the LHS of lazy boolean expression a temporary scope, too? What's the practical/technical difference?

Actually, it looks like &&/|| creates a temporary scope for both the lhs and the rhs: [playground]

// full code on the linked playground
let _ = (
    track("pre").0 == "",
    track("lhs").0 == "" || track("rhs").0 == "",
    track("end").0 == "",
);
init pre
init lhs
drop lhs
init rhs
drop rhs
init end
drop end
drop pre

Note the nesting here, which, elaborating scopes, looks like it's:

{
    let pre;
    {
        let lhs;
    }
    {
        let rhs;
    }
    let end;
}

So this looks like a reference bug to me, and it should say that either operand of a lazy boolean expression dominates a non-extended temporary scope.

The general rule is that temporaries live for as long as they could still be referenced, as references are the primary motivator behind temporaries remaining live. It'd be very annoying if f(&g()) wasn't {let tmp; f({tmp = g(); &tmp})} and was instead f({let tmp = g(); &tmp}), since the latter won't compile.

The "real" answer is probably that it's mostly just a side effect of how the constructs were originally compiled/desugared, and was maintained after that. As a relevant example, it's likely that $lhs || $rhs gets lowered into something that looks more like an if expression reasonably early in the compiler.

Keeping invisible timing like drop order stable is more important than it being perfectly justified from first principles in every edge case. So there can be some surprises sometimes, but changing the behavior to "fix" it over an edition is generally seen as worse. For safe code, it's generally not an issue (drop glue ideally shouldn't be doing things other than cleanup) and for unsafe code it's more important that it only needs to be learned once.

1 Like