Why does making a local fix this borrow checker error?

In this snippet the first method compiles fine but the second gives an error. this is confusing because the len() method returns a value not a reference so I wouldn't expect it to keep the object "locked."

#![allow(dead_code)]

struct Foo {
    buffer: Vec<u8>,
    read_start: usize,
}

impl Foo {
    fn foo_works(&mut self) {
        let range = self.read_start..self.buffer.len();
        self.buffer.copy_within(range, 0);
    }

    fn foo_doesnt_work(&mut self) {
        self.buffer.copy_within(self.read_start..self.buffer.len(), 0);
    }
}

The error:

   Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `self.buffer` as immutable because it is also borrowed as mutable
  --> src/lib.rs:15:50
   |
15 |         self.buffer.copy_within(self.read_start..self.buffer.len(), 0);
   |         -----------------------------------------^^^^^^^^^^^^^^^^^----
   |         |           |                            |
   |         |           |                            immutable borrow occurs here
   |         |           mutable borrow later used by call
   |         mutable borrow occurs here
   |
help: try adding a local storing this argument...
  --> src/lib.rs:15:50
   |
15 |         self.buffer.copy_within(self.read_start..self.buffer.len(), 0);
   |                                                  ^^^^^^^^^^^^^^^^^
help: ...and then using that local as the argument to this call
  --> src/lib.rs:15:9
   |
15 |         self.buffer.copy_within(self.read_start..self.buffer.len(), 0);
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` due to previous error

Playground Link

The first google result I get for this makes me think NLL should have fixed it?

When making a method call, the borrow of the receiver happens first. Then your call to .len() invalidates that borrow.

Two-phase borrows (the RFC I linked to) does let you do this for simple cases, but only simple cases. There's a tracking issue to extend it, but it is hard to do without introducing bugs, so it may or may not happen.

I think the problem is not the len() keeping the object locked, it’s the copy_within method already creating a mutable reference before .len() is called in the first place. To be precise:

  • self.buffer.copy_within(self.read_start..self.buffer.len(), 0); will first evaluate and in particular dereference the (mutably captured) β€œself.buffer” (the self-argument to the .copy_within method call) into &mut [u8],
  • then try to call .len() on the already mutably borrowed self.buffer

Because a dereference was involved, which is allowed to have side-effects, the special-case rule of two-phase-borrows doesn't apply either, whereas e.g. something like

    fn foo_works(&mut self) {
        let buffer = self.buffer.as_mut_slice();
        buffer.copy_within(self.read_start..buffer.len(), 0);
    }

works.

1 Like

Wouldn't you always want to separately evaluate (and let any temporary borrows they perform complete) each argument before doing the receiver borrow for the method? The function can't run without its arguments, so it is a given they are going to be evaluated first anyway. Technically the receiver is an argument, but it is special in the sense that the receiver is what gets "locked" for the duration of the call.

The receiver is just another argument. A method call foo.bar(baz, qux) is supposed to just desugar to something like Xyz::bar(foo, bar, qux), or Xyz::bar(&foo, bar, qux), etc, depending on the receiver type. I guess it is defined this way in order to be predictable. Rust deliberately chooses not to leave the order of evaluation of function arguments unspecified, unlike other languages like C or C++ IIRC...

In particular with method-call chains, this behavior also makes a lot of sense:

x.foo(bar()).baz(qux());

will evaluate (as IMO intuitive/natural/expected) in the order: bar, then foo, then qux, then baz.

The fact that bar and foo are evaluated before qux requires that the self-argument of baz is evaluated before its β€œqux()” argument.

2 Likes

I guess the thing about this example is that the order really does not matter, len() is pure and the deref is pure. I guess we need effects or disjoint borrows...

For dereference it's not guaranteed, since DerefMut::deref_mut can do arbitrary things, and compiler won't analyze the function bodies.

Yep, this is why I was saying Rust would need effects (which would make it so from function annotations alone a caller could know a function is pure).

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.