RFC 66 sketch

How things work now

The current temporary design was described and proposd in a 2014 blog post. The best description of these rules is probably this comment in the source code. We will attempt to summarize here.

Under the current rules, temporaries are typically dropped at the end of a statement:

let x = foo.lock().do_something();
//          ^^^^^^               ^
//      Returns a `guard`...     |
//                     ...that is dropped here   

let x = do_something(&foo.lock());
//                        ^^^^   ^
//                          |    |
//                          |  ...that is dropped here
//                          |
//                    Returns a `guard`...

However, in some circumstances, we extend the lifetime of a temporary until the end of the enclosing block. This is based on a syntactic analysis that occurs before the type-check. The basic rule is "is this temporary certain to be stored in a local variable":

let x = &foo.lock();
//       ^^^^^^^^^^
//            |
//       Resulting guard will be stored in `x`, extend until
//       end of block.

This also covers temporary that are stored into structs (or other data structures) which in turn are stored into local variables:

let x = Something {
    field: &foo.lock();
    //     ^^^^^^^^^^
    //        |
    //   Resulting guard will be stored in `x`, extend until
    //   end of block (same scope as `x`).
};

The same applies to structs that are themselves temporaries stored into local variables:

let x = &Something {
    //  ^ 
    //  the `Something` will be stored into a temporary
    //  and reference to that stored into `x`
    field: &foo.lock();
};

Motivation

The current rules fail to detect cases where a reference to a temporary will be stored into a local variable. To some extent, that is inevitable, but there are some specific situations that occur quite often.

One particular failing of the current goals is that they are not abstractable. For eaxmple, we saw that we can do let x = Something { ... } and extend the lifetime for temporaries that appear in its fields; but if we have a constructor like Something::new, that doesn't work (playground):

struct Something<'me> {
    name: &'me str,
}

impl<'me> Something<'me> {
    fn new(name: &'me str) -> Something<'me> {
        Self { name }
    }
}

fn main() {
    let x = Something::new(&String::new());
    //                      ^^^^^^^^^^^   ^
    //                            |      ...dropped here
    //                    Temporary created...
    println!("{}", x.name);
    //               ^^^^ out of scope here
    
}

Or, as the compiler puts it:

error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:12:29
   |
12 |     let x = Something::new(&String::new());
   |                             ^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
   |                             |
   |                             creates a temporary which is freed while still in use
13 |     println!("{}", x.name);
   |                    ------ borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

It'd be nice if this worked.

Specific proposal

"Extended parameters"

We will analyze the signature of each function to identify extended parameters. These are parameters where:

  • the parameter's type is &'a T for some T
  • and the lifetime 'a is constrained by the return type
    • or it is declared to outlive some parameter 'b that is constrained by the return type.

This analysis can be done based purely on the declared signature of the function and doesn't require any kind of inference or type checking, just "type collecting".

Examples

fn foo<'a, 'b>(
    input: &'a String,  // Yes! Appears in return type.
    output: &'b String, // No! Does not, cannot be returned.
) -> &'a str {
    input
}
fn foo<'a, 'b>(
    input: &'a String,    // Yes! Appears in return type.
    output: &'b String,   // Yes! Appears in return type.
) -> (&'a str, &'b str) {
    input
}
fn foo<'a, 'b>(
    input: &'a String, 
    output: &'b String,   // Yes! Outlives 'a, which appears in return type.
) -> &'a str
where
    'b: 'a,
{
    output
}

Note that appearing in the return type is not enough; the lifetime must be constrained by the return type. These are the same rules we use to determine what is a valid impl (see e.g. these tests):

trait Mirror<'a> {
    type Output;
}

fn foo<'a, 'b>(
    input: &'a String,    // No, appears in return type, but not constrained by it.
    output: &'b String,   // No, appears in return type, but not constrained by it.
) -> <&'a str as Mirror<'b>>::Output
where
    'b: 'a,
{
    ...
}

Applying extended parameters

We will extend the grammar E& of expressions considered as extension expressions from

     E& = & ET
        | StructName { ..., f: E&, ... }
        | [ ..., E&, ... ]
        | ( ..., E&, ... )
        | {...; E&}
        | box E&
        | E& as ...
        | ( E& )

to include the ith parameter to a call (i.e., E& = E (E0 ... E(i-1), E&, ... ) when the signature of the callee E is analyzed and the ith parameter is a extended parameter (per the rules just described). For the purposes of this check, method calls like foo.bar(...) consider foo to be the 0th parameter.

Challenges

Currently the set of "extended lifetime temporaries" is determined before type-check, based purely on a walk of the HIR. The new rules require us to move that reasoning into the type-check itself. We likely want to do this by:

  • Moving some parts of the "region scope tree" out from region analysis and into the typeck results;
  • Growing the set as we type-check
    • for example, we might consider making calls to check_expr take a new parameter indicating whether they are in a "extended temporary" position.

Questions

Can this change the semantics of any code?

I don't think so, because I think that the only cases it covers are cases that would be type errors otherwise. But I may be wrong! We should devise a way to test this experimentally on crater (e.g., applying both analyses and reporting an error somehow).

Do we need to be concerned about new bugs?

Maybe, but I suspect that existing lints (or proposed lints) will mostly cover the concerns. That said, some lints like this one may no longer apply, we should double check that! (i.e., this code may now work as intended? Perhaps not, because it is using raw pointers.)

Select a repo