How to understand "Extending based on expressions"?

Consider this example

struct Test{
   i:i32
}
impl Test{
  fn get_i(&self)->&i32{
      &self.i
  }
}
fn get_test()->Test{
   Test{i:0}
}
fn main(){
   let i = &get_test().i; // Ok
   let i2 = get_test().get_i(); //error
}

Destructors - The Rust Reference says

For a let statement with an initializer, an extending expression is an expression which is one of the following:

  • The initializer expression.

Isn't that &get_test().i and get_test().get_i() are both so-called initializer expression? Why is the temporary value produced by get_test in the first case extended but the second is not? So, how to understand the temporary value extension criteria correctly?

My take:

//      vvvvvvvvvvvvv    The initializer expression
let i = &get_test().i;
//       ^^^^^^^^^^      Call expression
//       ^^^^^^^^^^^^    Field access expression
//      ^^^^^^^^^^^^^    Borrow Expression

What are the extended expression?

  • The initializer expression &get_test().i (which is a borrow expression)
  • The operand of the extending borrow expression, get_test().i
    • Apparently the operand here is the parent of the field, which I agree is unclear
    • Or if a field gets temporary-scope extended, so does the parent

What has its temporary scope extended?

The operand of any extending borrow expression has its temporary scope extended.

That would be get_test().i (or perhaps get_test() itself).


//       vvvvvvvvvvvvvvvvvv    The initializer expression
let i2 = get_test().get_i();
//                  ^^^^^^^    Call expression
//       ^^^^^^^^^^^^^^^^^^    Method call expression

What's are the extended expressions? Just the initializer expression, because a method call expression isn't a borrow expression, array, cast, braced struct, tuple, or block.

There's no extending borrow expression and nothing has its temporary scope extended.

We can rewrite this one:

//       vvvvvvvvvvvvvvvvvvvvvvvv    The initializer expression
let i2 = Test::get_i(&get_test());
//                    ^^^^^^^^^^     Call expression
//                   ^^^^^^^^^^^     Borrow expression
//       ^^^^^^^^^^^^^^^^^^^^^^^^    Call expression

But again only the initializer expression is an extended expression, a call expression is not a borrowing expression, array, cast, braced struct, tuple, or block.

This time there is a borrow expression, but it's still not an extending borrowing expression, and nothing has its temporary scope extended.

3 Likes

So, the key point is that only operands of the extending borrow expression are eligible to make the temporary value extend, Right?

So, let's take a look at this case

//      vvvvvvvvvvvvvvvvvvvvvvvvvv The initializer expression, which is a block expression
let x = { [Some { 0: &temp(), }] };
//                   ^^^^^^^ (maybe)The operand of braced struct, I'm not sure
//        ^^^^^^^^^^^^^^^^^^^^^^  The final expression(array) of the block expression that is an extending expression
          

Since the braced struct as the operand of the final expression(i.e an array) of the block expression that is an extending expression, we can get the conclusion that

  • the braced struct is an extending expression
  • The operand of the braced struct is also an extending expression

So, the &temp() is an extending expression, whereas &temp() again is a borrowing expression, hence the operand temp() can be extended.


But, back to your third case, we make a bit change

let i2 = &Test::get_i(&get_test());

Now, is get_test() extended? My reason is the initializer expression &Test::get_i(&get_test()) is an extending expression and it's also a borrowing expression, hence the operand is also an extending expression, according to

And &get_test() itself is a borrowing expression, hence the operand get_test() should be extended?

I agree with everything about the key point and the operand of the braced struct.

Given the compiler error if you try to use i2, it is not lifetime extended, so I guess we can conclude that the arguments to a call expression aren't considered operands, just the return value.

(At least if we take the terminology and descriptions on this page as normative, which it explicitly is not, and doubly so for lifetime extension. Sorry, there is still no Rust or rustc spec.)

1 Like

Thanks. So, the problem is we haven't precisely defined which stuff is the operand of an expression yet. Hence, according to the reference, more or less, if the arguments were operands of a function call expression, the case would be compiled. However, actually, the compiler emits an error, so, we infer that the arguments of the function call are not operands of the function call expression.

As far as I understand the explanation in the reference, the important concept here to understand the behavior in the example is not operand, but extending expression.

Nonetheless, first my understanding of operand: The operand(s) of an expression… well, let’s actually quote the reference

Many expressions contain sub-expressions, called the operands of the expression.

IMO, for clarification, it should be said, that only the immediate sub-expressions are operands, and even then in expressions that contain things like types, match-arms, statements, etc… the meaning of “operand” isn’t entirely clear to me. On the other hand, we don’t need to know, as the page about temporary lifetime elision only needs you to define what “operand” means for a few kinds of expressions. While I’m convinced that a method-call or function-call expression would call all function argument expressions as operands, for the example at hand, it’s only relevant that a borrow expression – &… – has a single operand, the whole … expression.

Hence in

let i2 = &Test::get_i(&get_test());

We have &Test::get_i(&get_test()) being an initializer expression, and thus an extending expression.

&Test::get_i(&get_test()) is a borrow expression. Since we determined it’s an extending expression, it is an extending borrow expression, and its operand is Test::get_i(&get_test()). This is the only operand, none of the sub-expressions of this are operands of &Test::get_i(&get_test()), too; get_test() would only be an operand of &get_test(), not of the whole &Test::get_i(&get_test()).

As the operand of an extending borrow expression, Test::get_i(&get_test()) is an extending expression, too. It’s a function call expression. There’s no rule for extending function call expressions to do anything. The whole Test::get_i(&get_test()) is an extending expression, but none of its sub-expressions become extending expressions. That’s an important realization, parts of an expressions are distinct entities in this discussion and do not automatically inherit any property, unless explicitly specified.

In particular &get_test() is an operand of an extending (function call) expression, but not an operand of an extending borrow expression, or of an extending array, cast, braced struct, or tuple expression, i.e. it does not become an extending expression by the rules explained in the reference.

Also, get_test() is an operand of a borrow expression, but not an operand of an extending borrow expression, since &get_test() is not an extending expression.

So after all, we have in

let i2 = &Test::get_i(&get_test());

a list of extending expressions:

  • &Test::get_i(&get_test())
  • Test::get_i(&get_test())

Finally, the effect of an expression being extending is described as follows:

The operand of any extending borrow expression has its temporary scope extended.

An “expression (that) has its temporary scope extended” is quite a mouthful, and can be confused with “extending expression”, so maybe let’s call the former one short “HAETS”-expression (for “Has An Extended Temporary Scope”).

The extending expressions above include exactly one borrow expression, i.e. &Test::get_i(&get_test()), and its operand Test::get_i(&get_test()) will thus become a HAETS expression.


Now, for determining whether this took care of all our problematic temporaries, we must also answer the follow-up question: Which expressions create temporaries here? Mostly, value expressions used in place contexts create temporaries! (IMO it’s useful to ignore the special case for temporaries for operands…) Without going quoting those relevant definitions now, I can determine that

  • &Test::get_i(&get_test()) is a value expression used in a value context (the context comes from the i2 pattern, which binds by-value)[1]
  • Test::get_i(&get_test()) is a value expression used in a place context (!!)
  • &get_test() is a value expression used in a value context
  • get_test() is a value expression used in a place context (!!)

Thus we get two temporaries, one for Test::get_i(&get_test()), and one for get_test(). The former is also a HAETS expression, so its temporary will be extended. On the other hand get_test() is not an extending expression, so that its temporary will not be extended.

So the scope of the temporary containing the value of Test::get_i(&get_test()) will be the whole containing block or function body, while the scope of the temporary containing the value of get_test() will be only the whole let …; statement.


The other example

let i = &get_test().i;

To clear up some confusion indicated in parentheses: The operand of &get_test().i is get_test().i, not get_test(). But get_test() is the operand of get_test().i, and

If a borrow, dereference, field, or tuple indexing expression has an extended temporary scope then so does its operand. If an indexing expression has an extended temporary scope then the indexed expression also has an extended temporary scope.

So if get_test().i is an extending expression, then get_test() is one, too. Wait… no that’s the possible mistake I explained earlier. We have this important distinction between the concepts “expression (that) has an extended temporary scope” and “extending expression”; wow, what a wording… let’s keep using the term “HAETS”-expression instead. Anyways… with this rule in mind, we can determine that

let i = &get_test().i;

The &get_test().i expression is extending, since it’s an initializer,
then get_test().i is an extending expression as the operator of an extending borrow expression.
With of these two extending expressions, the former is also a borrow expression, so its operand, get_test().i is HAETS.

Since get_test().i is a field expression has an extended temporary scope (HAETS), it’s operand get_test(), becomes HAETS, too.

Looking through place vs. value expressions and contexts:

  • &get_test().i is a value expression in a value context[2]
  • gets_test().i is a place expression in a place context
  • gets_test() is a value expression in a place context (!!)

So there’s a single temporary, for the value of gets_test(), which gets an extended scope since the expression gets_test() is HAETS.


  1. I’m not actually sure whether this interpretation is correct, or initializer expressions are always considered place expression contexts for the question of whether or not temporaries are created; however, even if we interpret a temporary being created here, then it’s immediately moved out of again, too, so it’s not of much interest ↩︎

  2. or maybe a place expression context, but again, it doesn’t matter much whether we get another temporary here ↩︎

2 Likes

Consider this example

struct Data(i32);
impl Data{
    fn method(&self)->&i32{
        &self.0
    }
}
fn construct_data()->Data{
    Data(0)
}
fn main() {
   let rf = &construct_data().method();
   println!("{rf}");
}

In short, &construct_data().method() is an extending borrowing expression, so the operand construct_data().method() is a “HAETS”-expression, then the reference says

If a borrow, dereference, field, or tuple indexing expression has an extended temporary scope then so does its operand. If an indexing expression has an extended temporary scope then the indexed expression also has an extended temporary scope.

The indexed expression may be construct_data()? So, why is the temporary value of construct_data() not extended?

Eh… “indexing expression” is referring to expressions of the form “expr1[expr2]”, e.g. for indexing arrays/vecs/slices/etc…

The “indexed expression” of an indexing expression expr1[expr2] is the expr1 part/operand.

So all that this bolded sentence in your quote says is that an expression of the form expr1[expr2] is HAETS, then expr1 is HAETS, too (but the other operant, expr2, isn’t) :slight_smile:

2 Likes

Thanks. a borrow, dereference, field, or tuple indexing expression is respectively corresponding to:

  • &Expression
  • *Expression
  • Expression.fieldName
  • TupleExpression.[0...N]

Right? So, Expression.method(...) is not the case mentioned in the list?

Exactly :slight_smile:

By the way…

I believe, each of these recursive definitions (both for HAETS, and for “extending” expression) should – technically – also need an extra rule for “(…)”-parenthesized expressions[1]; but I assume that there was an implicit assumption that “(…)”-parentheses should be transparently ignored after they finished helping with precedence.


  1. So:

    If (Expression) is HAETS, so is Expression;
    if (Expression) is extending, so is Expression. ↩︎

I think so. I also think using grammar to state which expressions are "HAETS" would be clearer.

For completeness: borrow also includes &mut Expression.

1 Like

Links to all the definitions that are implicitly referenced here:

If a borrow, dereference, field, or tuple indexing expression has an extended temporary scope then so does its operand. If an indexing expression has an extended temporary scope then the indexed expression also has an extended temporary scope.

Each of the places I linked starts with a grammar, fortunately :slight_smile:

1 Like

I believe, adding such links would be a good and easy improvement of this section of the reference. Anyone feel free to open a PR with this change.

Edit: Here’s the PR :slight_smile:.

1 Like

@steffahn @quinedot
Consider this example in Rust reference

// The temporary that stores the result of `temp()` lives in the same scope
// as x in these cases.
let x = (&*&temp(),);

The comment says the temporary value will be extended. So, analyze this example

//      vvvvvvvvvvvv  The initializer expression
let x = (&*&temp(),);
//        ^^^^^^^^   The Deference expression   
//       ^^^^^^^^^  The borrow expression  
//      ^^^^^^^^^^^^  The tuple expression                     

Ok, with these annotations, it is apparently that, the initializer expression is an extending expression, which is also a tuple expression, so its operand is also an extending expression, which is a borrow expression, so its operand is an extending expression. As far now, we know the deference expression is an extending expression. However, there is no rule to say the operand of an extending deference expression is also an extending expression, so we cannot determine whether its operand is an extending borrow expression for which its operand temp() would have its lifetime to be extended. Or, Is something I missed?

Updated:

A scattered rule may apply to the case

If a borrow, dereference, field, or tuple indexing expression has an extended temporary scope then so does its operand.

Since the aforementioned analysis, the deference expression *&temp() as the operand of an extending borrow expression has its lifetime to be extended, then the above rule says so does it operand, hence, &temp() has its lifetime extended, recursively, the operand temp() of the borrow expression is also be extended. The above rule applies twice to this case.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.