What's the difference bewteen while let Some(node) = curr and while let Some(node) = curr.as_mut()

Hi

I try to write Single Linked List to practice Rust.
But I just confused about following code.

#[derive(Debug, PartialEq, Eq)]
pub struct Node<T> {
    pub val: T,
    pub next: Option<Box<Node<T>>>,
}

#[derive(Debug, Default, PartialEq, Eq)]
pub struct SinglyLinkedList<T> {
    pub head: Option<Box<Node<T>>>,
}

impl<T> SinglyLinkedList<T> {
    pub fn new() -> Self {
        SinglyLinkedList { head: None }
    }

    /// 1. insert in head
    pub fn push_front(&mut self, val: T) {
        let old_head = self.head.take();
        let new_head = Some(Box::new(Node{val, next: old_head}));
        self.head = new_head;
    }

    /// 2. insert tail - version 1
    pub fn push_back(&mut self, val: T) {
        let new_node = Box::new(Node { val, next: None });

        let mut curr = &mut self.head;
        while let Some(node) = curr {
            curr = &mut node.next;
        }
        *curr = Some(new_node);
    }

    /// 3. insert tail - version 2
    pub fn push_back(&mut self, val: T) {
        let new_node = Box::new(Node { val, next: None });

        let mut curr = &mut self.head;
        while let Some(node) = curr.as_mut() {
            curr = &mut node.next;
        }
        *curr = Some(new_node);
    }
}

In 3. insert tail - version 2, the compiler would report error about

32 |         *curr = Some(new_node);
   |         ^^^^^
   |         |
   |         `*curr` is assigned to here but it was already borrowed
   |         borrow later used here

"But my logic is: curr.as_mut() creates a temporary reference that reborrows a mutable reference from curr, and node then reborrows a mutable reference from that temporary reference. After the loop, node should return the loan to the temporary reference, which in turn returns the loan to curr.

Therefore, curr should be accessible on line 32.

Thanks in advance to anyone who can correct my mental model!"


this part is correct.

this part is wrong.

I think what you are missing is that because you re-assign the tempoary reference to the variable curr inside the loop, the lifetime of curr and the lifetime of the tempoary reference must be unified.

if you rewrite the code and annotate the type signatures with lifetimes in detail, you'll see the problem:

// note, this is not real rust syntax

// let's call this lifetime `'a`
let mut curr: &'a mut Option<Box<Node<T>>> = &'a mut self.head;

loop {
    // let's call this tempoary lifetime `'b`
    let __tmp: Option<&'b mut _> = Option::as_mut(curr /* reborrowed as &'b mut Option<_> */);
    match __tmp {
        None => break,
        Some(node: &'b mut _) => {
            //*******************************************
            // note here
            //*******************************************
            let next: &'c mut Option<_> = &'c mut node.next;
            curr = next;
        }
    }
}
*curr = Some(new_node);

let's solve the constraints:

  • in the as_mut() call, the temporary lifetime 'b is reborrowed from 'a:
    • 'a: 'b
  • inside the loop, next is reborrowed from node:
    • 'b: 'c
  • then the assignment curr = next:
    • 'c: 'a

the only possible solution is 'a == 'b == 'c. in other words, when the temporary references is dead (after the loop ends), curr must also be invalid.


but what's with the second version? why does it compile?

if you think about it, the type of curr is a mut reference, but you use the pattern Some(node) to match against it, as if it has an Option type?

well, the while let Some(node) = curr is not as simple as it may seems, due to match ergonomics. the full syntax is actually like this:

// again, this is not real rust syntax
let mut curr: &'a mut Option<Box<_>> = &'a mut self.head;
while let &'a mut Some(ref mut node) = curr {
    //...
}

notice there's no tempoary lifetime (the 'b and 'c in previous example)? that's the entire trick: bind the loop variable by ref mut, which reborrows from curr directly, instead of some temporary references.

the boundary of a function call to Option::as_mut() prevents this the borrow checker from performing this trick.

I also want to point out this problem only occurs because you need to re-assign the curr pointer in an loop. if you use a recursive approach, then there's no problem of Option::as_mut(), because each reborrow shrinks its active region, a temporary reference don't needs to be extended outside its scope.

pub fn push_back(&mut self, val: T) {
    fn push_back_inner<T>(node_location: &mut Option<Box<Node<T>>>, new_node: Box<Node<T>>) {
        match node_location.as_mut() {
            None => *node_location = Some(new_node),
            Some(node) => push_back_inner(&mut node.next, new_node),
        }
    }
    push_back_inner(&mut self.head, Box::new(Node { val, next: None }));
}

Hi nerditation

Thanks for your clear response.

I want to make sure I understand the first version correctly.

You explained that in the as_mut() version, the constraints are roughly:

'a: 'b // curr is reborrowed by as_mut()
'b: 'c // node.next is reborrowed from node
'c: 'a // next is assigned back to curr

which forces:

'a = 'b = 'c

But what happens in the first version?

// again, this is not real rust syntax*
let mut curr: &'a mut Option<Box<_>> = &'a mut self.head;
while let &'a mut Some(ref mut node) = curr {
    //...
}

There is still a reborrow from node to next, and next is assigned back to curr.

So I would expect curr = next to impose some relationship between 'b and 'a as well.

Why does this not cause the same problem as the as_mut() version?

In other words, what exactly is different about the lifetime of node/next in the first version that allows curr = next to work, while the as_mut() version forces the temporary borrow to be unified with 'a?

ok, I think I probably used the wrong terminology here. let me try again.

in this version, it is not a reborrow, I think I can call it "borrow splitting", i.e. to borrow a partial value (e.g. a variant of an enum, or a field of a `struct) from the whole value, with the same lifetime. in the case of splitted borrow, the parent reference is "consumed" and converted to a "child" reference. (here, the word "parent" is in terms of values/objects, not the region/loan relations in the tree-borrow/stack-borrow model)

while a reborrow borrows the same value as a whole but with reduced lifetime. in the case of a reborrowed, the lifetime of the parent mut reference is "disabled" when the child lifetime is active. (in this case however, the parent-child relation is about lifetimes).

note, in most cases, these are mixed together, so it's subtle to differentiate.

in the Option::as_mut() case, it forces a reborrow beause of the function call (when the argument is being passed to the function).

when the borrow checker solves the constraint, it is forced to unify the lifetime of the temporary reborrow 'b with the lifetime of the variable declaration 'a.

however, 'a cannot be reduced to match the region of 'b ('b begins in the loop, but 'a is alive before the loop), the only solution is to extend 'b to match 'a, which means as long as 'a is alive, it remains "temporarily" borrowed, permanently.

that's why the error message says: "borrow is later used here" -- "here" refers to the end of region 'b, which is just the end of 'a.

now that I think about it, I realize this issue is actually the NLL problem case 3 in disguise. it's a consequence of the current borrow checker model.

I believe this code should work under polonius[1] just fine. if you are on nightly, try RUSTFLAGS='-Z polonius' and it should compile without error.


  1. the next borrow checker, if you never heard of it ↩︎

After playing around a bit, it seems to be the introduction of an Option<&mut _> that stymies the borrow checker (probably #47680). For example, this also fails.

    pub fn push_back(&mut self, val: T) {
        let new_node = Box::new(Node { val, next: None });

        let mut curr = &mut self.head;
        while let Some(node) = match curr {
            &mut Some { 0: ref mut inner } => Some { 0: inner },
            &mut None => None,
        } {
            curr = &mut node.next;
        }
        *curr = Some(new_node);
    }

Most fail cases I tried worked with -Zpolonius=legacy but still fails with -Zpolonius=next, so this will probably be with us awhile. Using a sequence if let instead of while let works with -Zpoloinus=next, so the loop is relevant too.


To me, partial/field borrows through a &mut _ are still reborrows. They don't have to be the whole value.[1] I consider the distinguishing quality to be the ability to borrow some path -- sequence of field accesses and built-in dereferences -- without borrowing the intermediate references themselves, and thus they can e.g. go out of scope without invalidating the reborrow. The original borrows stay active even if the original references go out of scope thanks to the lifetime constraints.


  1. and the borrow duration can be the same ↩︎

Hi nerditation,

Thank you for the detailed explanation!

May I ask one more question?

Many Rust developers introduce a separate lifetime parameter for references reborrowed from a parent reference. I understand the subtype relation 'a: 'b, but I'm confused because a lifetime is defined as the span from when a reference is created to its last use (under NLL).

How should I understand the following case?

let mut x = 20; 
let y = &mut x;   // lifetime 'a
let z = &mut *y;  // lifetime 'b
*z = 10;

If y is never used again after z is created, why does 'a: 'b still need to hold (or how does the compiler reason about 'a: 'b here)?

'a is a lifetime connected to the borrow of x. If 'a: 'b didn't hold, then uses of z would not keep x borrowed. That would allow, for example, creating &x and sending it to some thread to be read in a loop while writing to *z on another thread -- a data race -- undefined behavior.

So the practical effect is that uses of z keep x exclusively borrowed (in addition to *y).

Uses of a reference[1] keep the associated lifetime alive... but also keep any other lifetime that outlives it alive, too. So it's not always just the use of one specific reference that defines a lifetime.


  1. or other types that contain a lifetime ↩︎

as already pointed out, this is not accurate, if you think the last use of the reference is where the variable y is directly mentioned.

I recommend this video if you want a deeper understanding of the bororw checker: