Pattern Matching
After the fetch step extracts the first nibble from an opcode, the CPU needs to decide what to do with it. The nibble is a number from 0 to 15 — each value represents a different instruction category. In most languages you would reach for a switch statement. In Rust, you use match, which is both more powerful and safer.
Basic syntax
#![allow(unused)] fn main() { let nibble: u8 = 6; match nibble { 0x1 => { /* JP */ }, 0x6 => { /* LD Vx, byte */ }, 0xA => { /* LD I, addr */ }, _ => println!("unknown: {}", nibble), } }
_ is the catch-all arm — it matches anything not covered above. In Rust, match must be exhaustive: every possible value of the matched type must be handled. If you leave out the catch-all and there are values you have not covered, the compiler refuses to compile. This is not a warning — it is an error.
match returns a value
Unlike switch, match is an expression. It produces a value, which you can assign directly:
#![allow(unused)] fn main() { let label = match nibble { 0x1 => "JP", 0x6 => "LD Vx", _ => "unknown", }; }
Every arm must produce the same type. The compiler checks this.
Multiple patterns and ranges
#![allow(unused)] fn main() { match x { 0x0 | 0x8 | 0xF => println!("multi-opcode group"), 1..=9 => println!("single digit"), _ => println!("other"), } }
| lets one arm handle multiple values. ..= matches an inclusive range.
Guards
A guard is an extra condition after the pattern:
#![allow(unused)] fn main() { match x { n if n % 2 == 0 => println!("even: {}", n), n => println!("odd: {}", n), } }
The pattern matches first, then the guard is evaluated. If the guard is false, the arm is skipped and matching continues.
Nested match for opcodes
Some opcode categories contain multiple distinct instructions that share the same first nibble. The 0x0 category has both 00E0 (CLS) and 00EE (RET). A single level of matching on the first nibble is not enough — we need a second level on the full opcode value:
#![allow(unused)] fn main() { let nibble = (opcode & 0xF000) >> 12; match nibble { 0x0 => match opcode { 0x00E0 => self.display.clear(), 0x00EE => { /* RET */ } _ => println!("unknown: {:#06X}", opcode), }, 0x1 => { /* JP addr */ }, 0x6 => { /* LD Vx, byte */ }, 0xD => { /* DRW */ }, _ => println!("unknown: {:#06X}", opcode), } }
The outer match dispatches on the category. The inner match refines within that category. This pattern repeats for the 0x8 group (nine register operations) and the 0xF group (timer, memory, and font opcodes).
In the emulator: The complete opcode dispatch is a
matchon the first nibble, with nested matches for the0x0,0x8, and0xFgroups. See The First Opcode: JP for where this structure first appears, 8XY* — Arithmetic and Logic for the inner match on the0x8group, and Bitwise Operations for how the nibble is extracted before being matched.