What is the lifetime constraint if the supporting fix is a non-reference variable

Consider this case:

struct A<'a>(&'a mut i32);

fn show<'a>(i:&'a mut i32)->&'a mut i32{
    {
        let a = A(i);
        return a.0;  // #1 &'c mut *(a.0)
    }
}

#1 can be desugared as &'c mut *(a.0), and according to the rules 2094-nll - The Rust RFC Book, the lifetime 'c must be constrained by some lifetimes(i.e. 'x: 'c), the supporting prefixes for this loan are *(a.0), a.0, and a, the lifetime 'x is determined by a, however, a is not a reference, it's a non-reference variable. So, I don't know how the lifetime constraint be determined in this case.

I suppose the variable a could be assumed to have the lifetime 'a, however, the following example has proven it's not.

fn show<'a>(i:&'a mut i32)->&'a mut i32{
    let r;
    {
        let a = A(i);
        r = &a;
    }
   r;
}

If a could have lifetime 'a, the example would be correctly compiler because 'a is valid everywhere in the function body, however, the compiler complains that a does not live long enough, which means a should have the lifetime associated with its block scope in which it is declared.

So, I wonder what the reborrow constraints are in the first example when the calculated ultimate supporting prefix is a non-reference variable.

In both of your examples, the variable a has type A<'a> (using the 'a defined in the function signature).

In the first example, return a.0, there's no reborrowing going on at all, and your desugaring is incorrect: a.0 has the type &'a mut i32, and you're moving the field out of a to return it.

In the other, however, you introduce a second, new lifetime on the line r = &a. This makes r have the type &'short A<'a>, and it's 'short that doesn't live long enough— It ends when the variable a goes out of scope at the end of the inner block.

1 Like

The explicit reborrowing is also compiled

fn show_explicit_reborrow<'a>(i:&'a mut i32)->&'a mut i32{
    {
        let a = A(i);
        return & mut *(a.0);  // #1 &'c mut *(a.0)
    }
}

That gets into how NLL works with place expressions, which I'm not familiar enough with to talk about in detail. But in essence, because a.0 has the type &'a mut i32 and there are no conflicting accesses of a.0 or i, the borrow checker can allow the lifetime of the reborrow to equal 'a.

This is still notably different from the non-compiling case, where you're storing a reference to a structure that will be dropped at the end of the scope.

1 Like

paging @quinedot

a.0 is a &'a mut i, and is one of the supporting prefixes that gets deref'd.

Variable a's type (A<'_>) is constrained to at most 'a as per the assignment.

I don't understand what is "proved". Your failing example doesn't make sense at multiple levels, you're not returning anything and if you were, you can't coerce a &_ to a &mut _.

Did you just mean "a isn't type A<'a> because the type wasn't printed as A<'a> in the error"? If I had to guess that's because the lifetimes involved are distinct lifetime variables whose constraints must hold. Remember the compiler doesn't really assign some strict set of lifetimes everywhere, it just tries to prove the constraints necessary for soundness hold.

At any rate, error messages may give clues as to what the borrow checker does, but isn't really proof of anything. Their annotations aren't normative (and at least sometimes, erroneous). Certainly you can't derive why the compiling version works from this example that doesn't even return the correct type.

Field accesses don't result in a lifetime constraint. But you also had a &mut deref, and that does result in a lifetime constraint.

2 Likes

For the first question: how to determine the reborrowing constraint

I thought the reborrow constraint is always determined by the last supporting prefix path in the supporting prefixes. After reading your prior answers, I think it's my misunderstanding. The reborrow constraint is determined by all of the supporting prefixes that are DereferenceExpression (i.e. have the form * e), each lifetime taken from them constitutes the constraints of the reborrow, right?

For example:

let foo = Foo { ... };
let p: &'p mut Foo = &mut foo;
let q: &'q mut &'p mut Foo = &mut p;
let r: &'r mut Foo = &mut **q;

The supporting prefixes of **q are **q, *q, q, hence the supporting prefixes that determine the constraint in this example are **q, *q. So the constraints are taken from their operand, they are *q and q, whose have the lifetime 'p and 'q, hence the reborrow constraints are 'p:'r and 'q:'r, Am I right?

In my first example in this question, the supporting prefixes are *(a.0), a.0, and a but the supporting prefix that has the form *e is just *(a.0), so the reborrow constraint used here is 'a(taken from a.0).


Variable a's type (A<'_>) is constrained to at most 'a as per the assignment.

This is my confusion in the second example. I thought A(i) can at most have the lifetime 'a, so r = &a can borrow a reference from a that has the lifetime 'a and 'a is valid everywhere in the function body, r holding the reference can be used everywhere in the function body. In the whole process, I seem to ignore the lifetime of the variable declared by let a = A(i); itself, a can only be alive in the inner scope, so it cannot produce a reference that exceeds the lifetime of a itself. Is my now understanding right?

Right.

Yep!

You can't borrow a local variable a: A<'a> for 'a (you can't create a &'a A<'a> from a) any more than you can create a &'static &'static str pointing at s in this example.

let s: &'static str = "";
let r: &'static &'static str = &s;

Borrowing a is not the same thing as borrowing *(a.0).

The place a goes away at the end of its scope. The place *(a.0), which has a &'a mut _ pointing at it with a lifetime 'a that came from elsewhere, doesn't go away until sometime after 'a which is greater than the function body.

'a is valid not only everywhere inside the function body, but for some indeterminate region beyond the function body. The function body is but a lower bound. You can't create borrows to locals longer than the function body.

What do you mean by "it cannot produce a reference" and by "the lifetime of a itself"?

You can reborrow *(a.0) for up to 'a, even though the variable a goes away at the end of the scope.

fn show_explicit_reborrow<'a>(i:&'a mut i32)->&'a mut i32{
    {
        let a = A(i);
        return & mut *(a.0);  // #1 &'c mut *(a.0)
    }
}

The inner block does nothing here, so let's ignore it.

a goes out of scope at the end of the block. What happens then?

  • No Drop implementation, no need to take a &mut and have a "deep write"
    • Drop glue applies to each field in place
    • Drop glue of a reference is a no-op
  • "StorageDead" on the variable a and any fields thereof

So it's a shallow write. Is the loan of *(a.0) relevant?

  • lvalue is a shallow prefix of the loan path
    • shallow prefixes are found by stripping away fields, but stop at any dereference
    • so: writing a path like a is illegal if a.b is borrowed
    • but: writing a is legal if *a is borrowed, whether or not a is a shared or mutable reference

Nope, it's fine. Only a or a.0 themselves being borrowed would be problematic. *(a.0) being (re-)borrowed is fine. It's not going away yet.

Effectively it works the same as this.

fn show<'a>(i: &'a mut i32) -> &'a mut i32 {
    let i2 = i;
    let i3 = &mut *i2;
    i3
}

Which is probably technically the same as this

fn show<'a>(i: &'a mut i32) -> &'a mut i32 {
    i // implicit reborrow and `i` itself goes away (probably)
}
2 Likes

I meant, the variable a has its lifetime at best in the innermost block. Maybe a can make the misunderstanding. So, consider this changed one:

fn foo<'a>( i:&'a mut i32){
   let r;
   {
      let b = A(i);
      r = &b;
   }
   r;
}

which can be analyzed as:

'body:{
    let r:&'larger A<'a>;  
    'b:{
      let b = A::<'a>(i);
      r = &'x b;
    }
    r; //'larger should keep valid here
}

The declared variable b can at best have the lifetime 'b, so the lifetime 'x at best is 'b, instead, r requires the lifetime 'larger, which is larger than what b can at most supply, which is the reason the compiler complains here, right?

So, I meant the lifetime of b is 'bb, which should satisfy 'b: 'bb.

The type of a has a lifetime:

let a: A<'_> = ...
//       ^^

But that's not the same thing as the scope of a. It is often also called a lifetime, but like the RFC author, I prefer a distinct term to distinguish the scope from Rust lifetimes -- those things we annotate with ' which the compiler analyzes with constraints, etc.

You could build a mental model around that, and what you wrote is a reasonable explanation of why that code must error.

But being technical about it,[1] that is not why the compiler complains because that is not the analysis the compiler performs. The compiler complains because going out of scope is a use of the variable; in this case it's a shallow write, and a shallow write of b is incompatible with 'x needing to be valid further down the program.

Lifetimes don't effect the scope of values, and the scope of values are not part of the 'x: 'y style constraints that the compiler uses in its analysis.

Perhaps it all makes sense in your mental model, but I can't answer questions about that.


  1. given that this thread opened by referring to the NLL RFC ↩︎

2 Likes

I see. So, in terms of "lifetime", we only talk about these lifetime parameters. The scope of a variable cannot be called lifetimes.

Instead, we can say a lifetime is valid in some regions and the regions are the scopes, right? I remember you used the utterance in some answers. Even more, we can say the lifetime denotes a region since the NLL reference says so:

any loans whose region does not include P are killed;

The compiler complains because going out of scope is a use of the variable; in this case it's a shallow write, and a shallow write of b is incompatible with 'x needing to be valid further down the program.

So, this is the formal analysis of why the compiler complains here, and a complete answer is similar to your previous comment What is the lifetime constraint if the supporting fix is a non-reference variable - #8 by quinedot

when b is going out of its scope, the operation is StorageDead, which performs "shallow write" access to the lvalue b, and at that point, the loan ('x, shared, b) should be in the scope due to we use r outside, and the loan is the loan for the path b, so, the loan is the relevant borrowing, and such two operations are conflicting due to one of them is a write operation.

"Region" is just another name for "(Rust) lifetime" as far as I'm aware.

They are not necessarily (lexical) scopes / blocks, though. That's the point of NLL (non-lexical lifetimes). Moreover, they can be flow-control sensitive and have holes.


Everything else in your comment looks correct to me.

Yes, I know region can denote a non-contiguous scope. However, NLL does use "scope" to determine whether a loan is alive.

We can then represent the set of loans that are in scope at a particular point using a bit-set and do a standard forward data-flow propagation.

So, I think the lifetime parameter denotes a region and the region can be associated with scopes, merely, a region may consist of several scopes and they may not be contiguous. Otherwise, how do we determine whether a loan is in scope according to its region(i.e. its lifetime)?

When the RFC is talking about lifetimes/regions and loans being "in scope", it is talking in terms of the analysis, not in terms of lexical blocks.

Maybe you're not talking about lexical blocks either any more. I'm not sure what you mean, really, so it's hard to reply.

Lifetimes are calculated as a set of points in the MIR of the program. Are you talking about that? That's not a set of points/scopes influencing or limiting what a lifetime is or can be, that's the output of the lifetime analysis itself.

They are what is used to calculate what loans are active at each point in the MIR.

How they are calculated is the first 2/3rds of the RFC.

2 Likes

I found the definition of describing how a lifetime is in scope

The first phase of the borrow checker computes, at each point in the CFG, the set of in-scope loans.

But I still didn't find the relevant rule to define how the complete calculation is done, is it the Layer 1: Control-flow within a function

Layers 1 through 4. Depending on the example you can sometimes ignore the layers other than 1.

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.