Which rule is violated when accessing a variable after getting a mutable reference?

I'm new to Rust and I'm learning it through official book.

I have a code which doesn't violate any rules discussed in the book but I still get a compile error. I'm interested to know which rule I'm violating or is there any rule which is missed in the book?

Here is my code:


fn main() {
    let mut x = 5;
    let y = &mut x;
    let z = x;

    println!("{y}")
}

There is a compile error at line let z = x which I don't understand why it is considered as error.
According to the book, when assigning primitive types, the Copy trait comes into play, which means it simply copies the bits one by one. So, when I write let z = x, there is no borrowing or ownership transfer, and therefore, those rules do not apply to this line of code.

Here are my questions?

  1. Which rule is violating in this code?
  2. Is that rule explained in the book or not?
Recap Rules

Ownership rules:

  • Each value in Rust has an owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Borrowing rules:

  • At any given time, you can have either one mutable reference or any number of immutable references.
  • References must always be valid.

Here this rule is violating :

Despite the syntax, &mut _ is more accurately an exclusive reference, not just a mutable reference.[1] The direct reading of the value in order to copy it is incompatible with being exclusively borrowed.

I don't know offhand if what's in the Book would technically explain this. But I do know that function body borrow checking is too complex for the Book to explain completely in a more general sense.

In broad strokes, the compiler finds out where places (like variables, fields, and dereferences) are borrowed and how (shared or exclusive), and checks every use of every place to see if conflicts with any borrows.

Reading x is incompatible with x being exclusively borrowed (but would be fine if it was only shared borrowed).


  1. and a &_ is a shared reference and not always immutable, which is typically taught much later when you get to Mutex an RefCell, etc ↩︎

4 Likes

The thing is that you cannot mutably borrow a value and then use or move the value itself until the mutable borrow is no longer in use. You created 'y', after which you are trying to assign 'x' to 'z' which is so-called move operation. It is not allowed because 'x' is already borrowed mutably by 'y'.

I violates the "mutable XOR shared" rule:

At any given time, you can have either one mutable reference or any number of immutable references.

As you correctly identified, i32 is Copy, so it will not be moved by let z = x;.

But, although this is not visible in this code, the copy requires a shared reference (&T) to the value and returns a copy of the value of type T. The shared reference is created automatically by the compiler, and lives just for the instant that the value is copied.
That invisible shared reference is what conflicts with the mutable reference held by y.

4 Likes

It doesn't show this specific scenario. The closest thing is in this section,
https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references
where it talks about the rule that a mutable and immutable ref for a variable cannot both be active:

Users of an immutable reference don’t expect the value to suddenly change out from under them!

The same thing could be said for users of the original owned variable -- they don't expect the value to change out from under them either. In fact this is a simpler scenario than when an immutable ref is active.

Nit about references, copies, and other operations

While this is a fine mental model for copies (copying a value and taking a shared reference are both "deep reads"), it's not how copies are actually implemented.

The complete borrow checking algorithm cannot be described by taking references alone. A mental model based on such can be adequate for a ways (could be a perfectly reasonable starting place), but might need to be refined later (or just accepted as an approximation).

For example a move is a "deep write", like taking an exclusive reference, but you can move a variable that doesn't have a mut binding.

Or as a more involved example, writes to a place are "shallow writes" which are more flexible than taking a &mut (a "deep write").

Trying to go into too much detail when introducing Rust would be more confusing than elucidating, which is part of why the Book will never completely describe the borrow checker. But (as someone who is annoyed by having to relearn things which were taught inaccurately and was frustrated with Rust learning material in particular), I at least like to give a heads-up when things are being simplified for the sake of getting the gist across.

4 Likes

Thanks for providing clarification! I was intentionally simplifying, but your post contains new information for me too.

In fact, your example boggles my mind: I can't understand why the first snippet does compile. Like if instead of an assignment, wrap was redefined with let, it would shadow the old variable, which would stay in memory and be referenceable. But does the same thing happen when the same variable is reassigned?!

Can you explain how/why this works (or send a link to some reading material)?

No, it's not the same; borrows of wrap itself (&wrap) would conflict with the overwrite but not the shadowing.

The borrow checker understands that overwriting a reference doesn't overwrite what the reference points to, basically.

Here's the relevant part of the NLL RFC. Heads up: I wouldn't recommend a beginner to the rfc (it took me a few long passes to wrap my head around after I had my sea legs).[1]

An assignment statement LV = RV is a shallow write to LV
[...]
For shallow accesses to the path lvalue, we consider borrows relevant if they meet one of the following criteria:

  • there is a loan for the path lvalue;
    • so: writing a path like a.b.c is illegal if a.b.c is borrowed
  • there is a loan for some prefix of the path lvalue;
    • so: writing a path like a.b.c is illegal if a or a.b is borrowed
  • 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

  1. If someone does decide to tackle it: the location sensitive/problem case #3 parts of the RFC were delayed and not yet stable. ↩︎

1 Like