13

a is a Vec<i32> which can be mutably and immutably referenced in one expression:

fn main() {
    let mut a = vec![0, 1];
    a[0] += a[1]; // OK
}

I thought this compiled because i32 implements Copy, so I created another type that implements Copy and compiled it like the first example, but it fails:

use std::ops::AddAssign;

#[derive(Clone, Copy, PartialEq, Debug, Default)]
struct MyNum(i32);

impl AddAssign for MyNum {
    fn add_assign(&mut self, rhs: MyNum) {
        *self = MyNum(self.0 + rhs.0)
    }
}

fn main() {
    let mut b = vec![MyNum(0), MyNum(1)];
    b[0] += b[1];
}

playground

error[E0502]: cannot borrow `b` as immutable because it is also borrowed as mutable
  --> src/main.rs:14:13
   |
14 |     b[0] += b[1];
   |     --------^---
   |     |       |
   |     |       immutable borrow occurs here
   |     mutable borrow occurs here
   |     mutable borrow later used here
  1. Why does my MyNum not behave in the same way as i32 even though it implements Copy?
  2. Why can the vector be mutably and immutably referenced in one expression?
pretzelhammer
  • 13,874
  • 15
  • 47
  • 98
tamuhey
  • 2,904
  • 3
  • 21
  • 50
  • 3
    It works with an array instead of a `Vec`. Not sure why. – trent May 11 '20 at 15:03
  • 3
    @trentcl: Probably because an array of `Copy` implements `Copy` but a `Vec` of `Copy` does not. – rodrigo May 11 '20 at 15:19
  • 2
    @rodrigo Good guess, but I don't think so. [It also works with `&mut [MyNum]`](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=fcfd3975442c96b83d6743b3cc1c8335) – trent May 11 '20 at 15:32
  • 2
    @trentcl: Then... there is only one explanation left: Magic! (compiler magic, of course). – rodrigo May 11 '20 at 15:51
  • See also [Cannot borrow as immutable because it is also borrowed as mutable in function arguments](https://stackoverflow.com/a/41189051/155423) – Shepmaster May 11 '20 at 15:53
  • @eggyal the fully desugared version does not work: `MyNum::add_assign(IndexMut::index_mut(&mut b, 0), Index::index(&b, 1));` – pretzelhammer May 11 '20 at 15:57
  • @pretzelhammer: Yes, I noticed I was just assigning not add-assigning. – eggyal May 11 '20 at 16:00
  • 1
    For `i32` neither the fully desugared version `i32::add_assign(IndexMut::index_mut(&mut a, 0), Index::index(&a, 1));` nor the partially desugared version `32::add_assign(&mut a[0], a[1]);` work. I think this may be a case of compiler magic like @rodrigo suggested... – pretzelhammer May 11 '20 at 16:03
  • As an aside, one can work around this with e.g. `let (lhs, rhs) = b.split_at_mut(1); lhs[0] += rhs[0];`. – eggyal May 11 '20 at 16:07
  • Issue has been opened in: https://github.com/rust-lang/rust/issues/72199 – tamuhey Aug 14 '20 at 04:53

1 Answers1

5

I believe the thing you're seeing here is that primitive types do not actually call their std::ops equivalents. Those std::ops may just be included for seamless trait extensions, etc. I think the blog post Rust Tidbits: What Is a Lang Item? partially explains this.

I exported the MIR of your example that works with primitive types. I got:

    bb5: {
        StorageDead(_9);                 // bb5[0]: scope 1 at src/main.rs:6:8: 6:9
        _10 = CheckedAdd((*_8), move _5); // bb5[1]: scope 1 at src/main.rs:6:5: 6:17
        assert(!move (_10.1: bool), "attempt to add with overflow") -> [success: bb6, unwind: bb4]; // bb5[2]: scope 1 at src/main.rs:6:5: 6:17
    }

I had a lot of difficulty exporting the MIR for the code that was erroring. Outputting MIR without borrow checking is new to me and I couldn't figure out how to do it.

This playground has a very similar thing, but compiles :)

It gives me an actual call to add_assign:

    bb3: {
        _8 = _9;                         // bb3[0]: scope 1 at src/main.rs:14:5: 14:9
        StorageDead(_10);                // bb3[1]: scope 1 at src/main.rs:14:8: 14:9
        StorageLive(_11);                // bb3[2]: scope 1 at src/main.rs:14:14: 14:22
        (_11.0: i32) = const 1i32;       // bb3[3]: scope 1 at src/main.rs:14:14: 14:22
                                         // ty::Const
                                         // + ty: i32
                                         // + val: Value(Scalar(0x00000001))
                                         // mir::Constant
                                         // + span: src/main.rs:14:20: 14:21
                                         // + literal: Const { ty: i32, val: Value(Scalar(0x00000001)) }
        _7 = const <MyNum as std::ops::AddAssign>::add_assign(move _8, move _11) -> [return: bb5, unwind: bb4]; // bb3[4]: scope 1 at src/main.rs:14:5: 14:22
                                         // ty::Const
                                         // + ty: for<'r> fn(&'r mut MyNum, MyNum) {<MyNum as std::ops::AddAssign>::add_assign}
                                         // + val: Value(Scalar(<ZST>))
                                         // mir::Constant
                                         // + span: src/main.rs:14:5: 14:22
                                         // + literal: Const { ty: for<'r> fn(&'r mut MyNum, MyNum) {<MyNum as std::ops::AddAssign>::add_assign}, val: Value(Scalar(<ZST>)) }
    }

How does the primitive case pass the borrow checker? Since add_assign is not called, the immutable reference can be dropped before the mutable reference is required. The MIR simply dereferences the needed location earlier on and passes it through by value.

    bb3: {
        _5 = (*_6);                      // bb3[0]: scope 1 at src/main.rs:6:13: 6:17
        StorageDead(_7);                 // bb3[1]: scope 1 at src/main.rs:6:16: 6:17
        ...
    }
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
chub500
  • 712
  • 6
  • 18
  • 2
    You can export MIR fairly reliably without having to get borrowck to pass on nightly with the -Zdump-mir flag -- that'll create a directory with a bunch of mir dumps. I generally just go for the last one, e.g. in this case you could look at `mir_dump/rustc.main.002-001.SimplifyCfg-qualify-consts.after.mir`. See https://rustc-dev-guide.rust-lang.org/mir/debugging.html for more details. – Mark Rousskov May 11 '20 at 16:26
  • Thanks! I don't think `playground` allows for this currently? It would be nice to be able to see a MIR on borrow check failures as in the case above. – chub500 May 11 '20 at 17:19
  • It is not only `AddAssign` but also `Index` that seems to be relevant here. [This example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=fcfd3975442c96b83d6743b3cc1c8335), which I linked in the question comments, also uses `AddAssign` but compiles fine. See also [Are operators in core really defined circularly?](https://stackoverflow.com/q/50254315/3650362) which elaborates on the built-in-ness of certain operations on primitive types. – trent May 11 '20 at 18:10
  • Yeah when I saw your comment I looked at the MIR, very strange. Clearly in your case the slice directly dereferences immediately (`_11 = &mut (*_7)[_12];`) - it never calls Index at all! Slices are also `primitive` magic! Lessons learned today. – chub500 May 11 '20 at 20:00
  • Thanks! Is there any way to compile my second example? That is, I want to compile a code that add-assign of a vector of my structure. – tamuhey May 12 '20 at 02:33
  • 1
    @Yohei Yes you can just assign `b[1]` to a variable first. The issue is with using the index in the same expression as the assignment. – Peter Hall May 12 '20 at 08:21
  • 2
    @Yohei - as @trentcl discovered - you can cast to a slice and then the line works. `let slice = b.as_mut_slice(); slice[0] += slice[1];`. – chub500 May 12 '20 at 12:49
  • Thanks! The slice trick is very interesting. Anyway, I feel it is inconsistent and strange that this question is not well-defined by trait. – tamuhey May 16 '20 at 06:03
  • I posted an issue in rust because I thought as you, this is definitely strange. https://github.com/rust-lang/rust/issues/72199 – chub500 May 16 '20 at 13:41