A weird thing about while let with ref

playground
this will not work

while let Some(n) = &mut temp.next {
    temp = n.next.as_mut().unwrap()
}

this will

while let Some(ref mut n) = temp.next {
     temp = n.next.as_mut().unwrap()
}
3 Likes

Normally I have more to say than thus, but:

O_o


Edit: I remember reading that some stdlib macros expand to match $expr { e => ... } rather than using let due to "extremely nuanced lifetime issues." Could that possibly be related? Or maybe that just has to do with temporaries, and this is... I don't know what this is.

1 Like

I think this is the same thing as macros expanding to a match rather than using let.

while let pretty much desugars to

while let $pat = $expr {
    $body
}
loop {
    match $expr {
        $pat => { $body },
        _ => break
    }
}

So if you have &mut temp.next at the top, it will lock temp.next for the entire match statement. But if you do temp.next with a Some(ref mut n) then it only locks the branch, and it restricts the scope of the borrow using nll.
Note that a match is basically this, (using made up syntax)

match $expr {
    $pat => $expr,
    ...,
    _ => $default_expr
}
let match_value = $expr;

if discriminant(match_value) == $pat {
    let $pat = match_value;
    $expr
}
...
else if ... {
    ...
}
...
else {
    $default_expr
}

So if you put a &mut _ at the top, that borrow will propagate through the entire chain, but if it is in the pattern, Rust can better reason about it.

Disclaimer: This is not the desugaring for match, but it does explain the reason for it's behavior.

4 Likes

but there is a loop {} outer scope, should not affects assignment after went out of loop.
and I tried to explicit add scope like this:

{
    while let Some(n) = &mut temp.next {
        temp = n.next.as_mut().unwrap()
    }
}

won't work either

wait.. what!? you mean the code afterward will be treated as else { //the code } ???

It is unfortunate that the compiler cannot show MIR when there is a compile error, because this basically must come down to whatever changes in the MIR.

2 Likes

Yes, this exactly

Okay, so I replaced the innards of the loop with a trivial expression 42;. This allowed me to compare the MIRs. Unfortunately it still does not make sense to me.


The MIR shown in the following graph is the MIR for when &mut temp.next is used.

  • Red parts are only present for &mut temp.next, not for ref mut n.
  • Blue parts change to ((*_3).1) when ref mut n is used.
// rust code
while let Some(n) = &mut temp.next {
    42;
}

Now, we have to imagine replacing the orange line with the MIR of temp = n.next.as_mut().unwrap(). This gets annoying and complicated to show because each of the function calls breaks it up into a new basic block in order to introduce unwind edges. If I try to simplify that, the MIR for the loop that compiles looks something like:

MIR for loop that compiles (no image)
// rust code
while let Some(ref mut n) = temp.next {
    temp = n.next.as_mut().unwrap();
}
bb5: {
    _7 = discriminant(((*_3).1));
    switchInt(move _7) -> [1isize: bb6, otherwise: bb9];
}

bb6: {
    StorageLive(_8);
    _8 = &mut ((((*_3).1) as Some).0);
    StorageLive(_9);
    StorageLive(_10);
    StorageLive(_11);
    _11 = &mut ((*(*_8)).1);
    _10 = const Option::<T>::as_mut(move _11);
    StorageDead(_11);
    _9 = const Option::<T>::unwrap(move _10);
    StorageDead(_10);
    _3 = &mut (*(*_9));
    StorageDead(_9);
    StorageDead(_8);
    goto -> bb5;
}

bb9: {
    StorageDead(_8);
    StorageDead(_3);
    drop(_2) -> bb10;
}

bb10: {
    return;
}

Putting it all together I get:

where red and blue have the same meaning as before, and the green line would appear to be the important one.


If this is correct, then to address @RustyYato :

  • Yes, &mut temp.node results in a borrow with a slightly wider scope...
  • but... this borrow still ends before every iteration. StorageDead(_6) appears at the end of bb6, before it enters the next iteration, so I don't understand why there is an issue.

The primary difference seems to be that, in the loop that compiles, n (_8) borrows directly from temp (_3), but in the loop that fails to compile, it only borrows indirectly through _6...

1 Like

Ok, so it looks like because of the indirect borrow through _6, which then dies at the end of the block. Rust thinks that the borrow doesn't live long enough, and errors out? But when it directly borrows from _3 everything is fine because the lifetimes are all the same.

This looks suspiciously similar to reborrows.

This. One point that should have been raised is the sugar present in the

while let Some(n) = &mut temp.next {

Before match ergonomics, this would have been written as

while let &mut Some(ref mut n) = &mut temp.next {
  • this is a reborrow of temp.next innards (borrowed for the whole implicit match, and reborrowed for the non break-ing branch);

vs:

while let Some(ref mut n) = temp.next {
  • this is a borrow of temp.next innards (direct borrow for the non break-ing branch).

Now, as to why a borrow acts differently than a reborrow, I think that @ExpHP's MIR exploration has led us as close as possible to an explanation.

I guess it may be related to NLL's limitations, which would hopefully be overcome by Polonius. We'll see.

2 Likes

Not sure if it's the same user (if not, then it's quite fantastic timing!), but somebody posted a more minimal example to the bug tracker. I posted similar MIR images there.

pub struct Demo {
    foo: Option<Box<Demo>>
}

pub fn simplified (mut a: &mut Demo){
    match &mut a.foo {
        &mut Some(ref mut a_foo) => a = a_foo,
        None => ()
    }
    a.foo = None
}

Removing the &mut from the expr and pattern makes it compile. The a.foo = None is also essential to the error.

This shows that it has nothing to do with loops, or match ergonomics.

4 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.