Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

One function call creates double borrow error while other does not

I am a bit confused about what the problem is here, or rather why it happens in one scenario and does not in other.

I am writing a gameboy emulator and implementing a function that increments a register, this is the function, it works fine without any errors:

    pub fn increment_reg8(
        &mut self,
        register_name: RegisterName,
        register_byte: RegisterByteName,
    ) -> u64 {
        let target_register = &mut self.registers[register_name][register_byte];
        let original_value = *target_register;

        *target_register += 1;
        if *target_register == 0 {
            self.set_flag(Flag::Zero);
        } else {
            self.clear_flag(Flag::Zero);
        }

        4
    }

Now I have tried changing the if statement to

self.set_flag_to(Flag::Zero, *target_register == 0);

Which pretty much equals the same if statement from before

    fn set_flag_to(&mut self, flag: Flag, value: bool) {
        if value {
            self.set_flag(flag);
        } else {
            self.clear_flag(flag);
        }
    }

Why does this change create double borrow error?

like image 484
Daniel Avatar asked Aug 07 '26 11:08

Daniel


1 Answers

This is an interesting corner case of the borrow checker. self is being used by two things: set_flag (equivalently, clear_flag) and target_register, which is a mutable reference to data inside of self. Let's look at your first example.

*target_register += 1;
if *target_register == 0 {
    self.set_flag(Flag::Zero);
} else {
    self.clear_flag(Flag::Zero);
}

A naive borrow checker would reject this. target_register has a mutable reference to part of self, and set_flag / clear_flag need mutable access to all of self, so that's a double borrow.

But Rust's borrow checker isn't naive. It's quite clever. It sees that target_register is never used again, so it automatically drops the value in order to get the lease on self back. Rust just silently converted your code to this.

*target_register += 1;
if *target_register == 0 {
    drop(target_register);
    self.set_flag(Flag::Zero);
} else {
    drop(target_register);
    self.clear_flag(Flag::Zero);
}

Where drop is std::mem::drop.

Now your second example.

self.set_flag_to(Flag::Zero, *target_register == 0);

Again, set_flag_to wants a mutable reference to self, and target_register is already a mutable reference to self. Argument evaluation order in Rust happens left to right, and the self.method() syntax is syntax sugar for this.

YourType::set_flag_to(self, Flag::Zero, *target_register == 0);

So the self argument gets evaluated. This borrows all of self mutably. Flag::Zero isn't complicated, so it passes the borrow checker. But now we need to access target_register again. But we already borrowed self mutably, so we can't do that. And since we need target_register after the self borrow, we can't auto drop anything like we did before.

If you change the evaluation order so that target_register is evaluated before self, it'll pass again.

let is_zero = *target_register == 0;
self.set_flag_to(Flag::Zero, is_zero);

This will pass the borrow checker, since Rust silently converts it into

let is_zero = *target_register == 0;
drop(target_register);
self.set_flag_to(Flag::Zero, is_zero);

and we can borrow &mut self when we need to for that last line.

like image 70
Silvio Mayolo Avatar answered Aug 10 '26 02:08

Silvio Mayolo