Compound Assignment Operators

When we implement the register operation opcodes, the code almost writes itself. The bitwise opcodes — OR, AND, XOR, shift — map so directly to Rust's compound assignment operators that reading the implementation feels like reading the opcode specification.

The syntax

Compound assignment combines an operation with assignment in one step. The two forms are exactly equivalent:

#![allow(unused)]
fn main() {
x += y    // x = x + y
x -= y    // x = x - y
x *= y    // x = x * y
x /= y    // x = x / y

x |= y    // x = x | y  (bitwise OR)
x &= y    // x = x & y  (bitwise AND)
x ^= y    // x = x ^ y  (bitwise XOR)
x >>= n   // x = x >> n (right shift)
x <<= n   // x = x << n (left shift)
}

In the register opcodes

The five bitwise operations in the 8XY* group become one line each:

#![allow(unused)]
fn main() {
self.v[x] |= self.v[y];   // OR  Vx, Vy
self.v[x] &= self.v[y];   // AND Vx, Vy
self.v[x] ^= self.v[y];   // XOR Vx, Vy
self.v[x] >>= 1;           // SHR Vx  (shift right by 1)
self.v[x] <<= 1;           // SHL Vx  (shift left by 1)
}

Each line reads like the opcode mnemonic itself. The implementation is almost a transliteration of the specification.

A subtle ordering trap

The shift opcodes — SHR and SHL — require setting VF before performing the shift, because the shift destroys the bit that VF is supposed to capture.

For SHR, VF should receive the bit that gets shifted out — the current least-significant bit:

#![allow(unused)]
fn main() {
// correct: save the bit first, then shift
self.v[0xF] = self.v[x] & 0x1;
self.v[x] >>= 1;

// wrong: the bit is gone by the time we try to read it
self.v[x] >>= 1;
self.v[0xF] = self.v[x] & 0x1;  // reads the wrong value
}

Both lines compile. Both look plausible at a glance. But the second version stores the bit that came in at the LSB position after the shift — not the bit that was shifted out. It is the kind of bug that only shows up when you run a test ROM that checks VF after a shift, and the result is subtly wrong.

The rule is always the same: any time an operation consumes information that must be saved first, save it before you run the operation.


In the emulator: All five compound bitwise assignments appear in 8XY* — Arithmetic and Logic. Note that the ADD opcodes (7XNN, 8XY4) do not use += — they use wrapping_add and overflowing_add instead, because plain arithmetic can panic in debug mode. See Integer Overflow and Wrapping for why.