Disparate mutable references in destructured types in while let

Currently working on a problem where I reverse a linked list. I was able to successfully write it but I feel like I may have an invalid number of mutable and immutable references. Rubberducking here by explaining what I think is happening but let me know in more detail!

// #[derive(PartialEq, Eq, Clone, Debug)]
// pub struct ListNode {
//   pub val: i32,
//   pub next: Option<Box<ListNode>>
// }
// 
// impl ListNode {
//   #[inline]
//   fn new(val: i32) -> Self {
//     ListNode {
//       next: None,
//       val
//     }
//   }
// }

type WrappedNode = Option<Box<ListNode>>;
impl Solution {
    pub fn reverse_list(head: WrappedNode) -> WrappedNode {
        let mut next = head;
        let mut prev: WrappedNode = None;
        while let Some(mut current) = next { // 1
            next = current.next;             // 2
            current.next = prev;             // 3
            prev = Some(current);            // 4
        }
        prev
    }
}

I have marked the important lines above.
// 1: I am moving the wrapped box of "next" into the variable "current" in the scoped within the loop. This is not a copy if my understanding is correct but a move.
//2: I am moving current.next to next (which is currently invalid?) thus invalidating current.next
//3: I am moving prev to current.next (which is currently invalid?) thus invalidating prev
//4: ??? Why can I now move the value from current- I thought i just invalidated one of its fields by moving

Staying within your terminology, current.next became valid again when you moved the value from prev into it in the previous line // 3.

Instead of valid one could also call it initialized and instead of invalid there's some choice of wording amongst uninitialized/deinitialized/moved-out-of. (I don't know what the best terms would be per say.)

The compiler is taking track of all fields of values such as current. When current.next becomes moved-out-of, the whole value current is considered partially moved-out-of. The effect is that both variables can no longer be used except for assigning a new value (or accessing fields that are still valid). By re-initializing the .next field with a new value, all fields of current are in an initialized state again (the other field current.val was never invalidated) and the whole value becomes usable again.

1 Like

I'm not sure exactly what you meant there, but incidentally -- if you have no unsafe in your Rust, one of these three is true by design:

  • Your program has no undefined behavior (UB) or unsoundness [1] :+1:
  • A dependency is unsound -- it created or allowed UB via its own unsafe :angry:
  • You (or a dependency of yours) hit a compiler bug :scream:

(Your only dep is std and you're not doing anything convoluted [2], so the last two can be effectively ignored in this case.)


  1. ability to trigger UB in safe code ↩︎

  2. for lack of a better term ↩︎

3 Likes