8XY* — Arithmetic and Logic

Nine opcodes share the 0x8 prefix. All operate on two registers Vx and Vy, with the last nibble identifying the specific operation. They are the CPU's complete set of register-to-register arithmetic.

Encoding

8XYN
│││└─ N: operation (0–7, E)
││└── Y: source register
│└─── X: destination register
└──── opcode category 8

Decode structure

All nine share the same setup:

#![allow(unused)]
fn main() {
0x8 => {
    let x  = ((opcode & 0x0F00) >> 8) as usize;
    let y  = ((opcode & 0x00F0) >> 4) as usize;
    let op =   opcode & 0x000F;

    match op {
        0x0 => { /* LD  */ }
        0x1 => { /* OR  */ }
        // ...
    }
}
}

The nine operations

0 — LD Vx, Vy — copy Vy into Vx.

1 — OR Vx, Vy

#![allow(unused)]
fn main() {
self.v[x] |= self.v[y];
}

2 — AND Vx, Vy

#![allow(unused)]
fn main() {
self.v[x] &= self.v[y];
}

3 — XOR Vx, Vy

#![allow(unused)]
fn main() {
self.v[x] ^= self.v[y];
}

4 — ADD Vx, Vy — adds with carry flag:

#![allow(unused)]
fn main() {
let (result, carry) = self.v[x].overflowing_add(self.v[y]);
self.v[x]   = result;
self.v[0xF] = carry as u8;  // 1 if overflow, 0 if not
}

5 — SUB Vx, Vy — Vx = Vx - Vy, VF = 1 if no borrow:

#![allow(unused)]
fn main() {
let (result, borrow) = self.v[x].overflowing_sub(self.v[y]);
self.v[x]   = result;
self.v[0xF] = !borrow as u8;  // inverted: 1 means "did NOT borrow"
}

6 — SHR Vx — shift right by 1, VF = bit shifted out:

#![allow(unused)]
fn main() {
self.v[0xF] = self.v[x] & 0x1;  // save LSB before it disappears
self.v[x] >>= 1;
}

7 — SUBN Vx, Vy — Vx = Vy - Vx, VF = 1 if no borrow (reverse of SUB).

E — SHL Vx — shift left by 1, VF = bit shifted out:

#![allow(unused)]
fn main() {
self.v[0xF] = (self.v[x] >> 7) & 0x1;  // save MSB before it disappears
self.v[x] <<= 1;
}

The VF ordering rule

For SHR and SHL, VF must be set before the shift — not after. The bit to save exists only before the operation; after the shift, it is gone. Getting this order wrong produces a subtly incorrect implementation that passes most tests but fails on edge cases.


Rust concepts used here: Compound assignments (|=, &=, ^=, >>=, <<=) are in Compound Assignment. overflowing_add / overflowing_sub returning (value, bool) uses Tuples and Destructuring. The VF ordering dependency is the same one called out in Compound Assignment.