//! Assert-based goldens for the PPC disassembler. //! //! Each test owns an inline list of `(raw, addr, label)` cases. On a //! normal run, the test reads the corresponding fixture JSON and asserts //! that `format(decode(raw, addr))` reproduces every field exactly. On //! first creation (fixture file missing) or with `REGEN_GOLDENS=1` set, //! the test (re)writes the fixture from `format()` output. //! //! Workflow: //! ```sh //! cargo test -p xenia-cpu --test disasm_goldens # assert //! REGEN_GOLDENS=1 cargo test -p xenia-cpu --test disasm_goldens # regen //! ``` //! //! The hand-encoded test cases below cover the silent-bug regression //! cases that lived in the old println-based `disasm_audit.rs` harness //! (now deleted). use std::path::PathBuf; use serde::{Deserialize, Serialize}; use xenia_cpu::decoder::{DecodedInstr, decode}; use xenia_cpu::disasm::format; #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] struct GoldenRow { label: String, raw: String, addr: String, mnemonic: String, operands: String, #[serde(default, skip_serializing_if = "Option::is_none")] ext_mnemonic: Option, #[serde(default, skip_serializing_if = "Option::is_none")] ext_operands: Option, #[serde(default, skip_serializing_if = "Option::is_none")] branch_target: Option, } #[derive(Debug, Deserialize, Serialize)] struct GoldenFile { rows: Vec, } fn fixture_path(name: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("golden") .join(name) } fn build_rows(cases: &[(u32, u32, &str)]) -> Vec { cases .iter() .map(|&(raw, addr, label)| { let d = decode(raw, addr); let t = format(&d); GoldenRow { label: label.to_string(), raw: format!("0x{raw:08X}"), addr: format!("0x{addr:08X}"), mnemonic: t.mnemonic, operands: t.operands, ext_mnemonic: t.ext_mnemonic, ext_operands: t.ext_operands, branch_target: t.branch_target.map(|t| format!("0x{t:08X}")), } }) .collect() } /// Compare what `format()` produces against the committed JSON snapshot. /// Set `REGEN_GOLDENS=1` to overwrite the snapshot from current output. /// Missing snapshot is treated as "first creation": writes and panics so /// CI can't accidentally accept blank goldens. fn assert_or_regen(fixture_name: &str, cases: &[(u32, u32, &str)]) { let rows = build_rows(cases); let path = fixture_path(fixture_name); let regen = std::env::var("REGEN_GOLDENS").is_ok(); if regen || !path.exists() { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).unwrap(); } let serialized = serde_json::to_string_pretty(&GoldenFile { rows }).unwrap(); std::fs::write(&path, serialized + "\n").unwrap(); if !regen { panic!( "Generated fixture {} (was missing). Inspect, commit, then re-run.", path.display() ); } return; } let src = std::fs::read_to_string(&path).unwrap(); let golden: GoldenFile = serde_json::from_str(&src).unwrap(); assert_eq!( rows.len(), golden.rows.len(), "row count differs from {} (live={}, fixture={}). Run with REGEN_GOLDENS=1 if the test cases changed intentionally.", path.display(), rows.len(), golden.rows.len() ); for (i, (got, expected)) in rows.iter().zip(golden.rows.iter()).enumerate() { assert_eq!( got, expected, "row {} ({}) differs in {}\n live: {got:#?}\n fixture: {expected:#?}", i, expected.label, path.display() ); } } // ── Encoding helpers ──────────────────────────────────────────────────────── // PPC bit numbering: bit 0 is MSB, bit 31 is LSB. Most helpers below emit // instructions in canonical hand-readable form: opcode << 26 | . #[allow(clippy::too_many_arguments)] fn xform_xo3(rd: u32, ra: u32, rb: u32, oe: u32, xo: u32, rc: u32) -> u32 { (31 << 26) | (rd << 21) | (ra << 16) | (rb << 11) | (oe << 10) | (xo << 1) | rc } fn xform_logic(rs: u32, ra: u32, rb: u32, xo: u32, rc: u32) -> u32 { (31 << 26) | (rs << 21) | (ra << 16) | (rb << 11) | (xo << 1) | rc } fn dform(op: u32, rt: u32, ra: u32, imm: i16) -> u32 { (op << 26) | (rt << 21) | (ra << 16) | ((imm as u16) as u32) } fn iform_b(target_disp: i32, aa: u32, lk: u32) -> u32 { // I-form: opcode 18 | LI<<2 | AA<<1 | LK let li = (target_disp as u32) & 0x03FF_FFFC; (18 << 26) | li | (aa << 1) | lk } fn bform_bc(bo: u32, bi: u32, target_disp: i32, aa: u32, lk: u32) -> u32 { // B-form: opcode 16 | BO<<21 | BI<<16 | BD<<2 | AA<<1 | LK let bd = (target_disp as u32) & 0x0000_FFFC; (16 << 26) | (bo << 21) | (bi << 16) | bd | (aa << 1) | lk } fn xlform_bclr(bo: u32, bi: u32, lk: u32) -> u32 { // XL-form: opcode 19 | BO<<21 | BI<<16 | XO=16<<1 | LK (19 << 26) | (bo << 21) | (bi << 16) | (16 << 1) | lk } fn xlform_bcctr(bo: u32, bi: u32, lk: u32) -> u32 { (19 << 26) | (bo << 21) | (bi << 16) | (528 << 1) | lk } fn rlwinm(rs: u32, ra: u32, sh: u32, mb: u32, me: u32, rc: u32) -> u32 { (21 << 26) | (rs << 21) | (ra << 16) | (sh << 11) | (mb << 6) | (me << 1) | rc } fn rldicl(rs: u32, ra: u32, sh: u32, mb: u32, rc: u32) -> u32 { // MD-form: sh[4:0] at PPC bits 16-20 (host bits 11-15); sh[5] at PPC bit 30 (host bit 1). // mb[4:0] at PPC bits 21-25 (host bits 6-10); mb[5] at PPC bit 26 (host bit 5). let sh_lo = sh & 0x1F; let sh_hi = (sh >> 5) & 1; let mb_lo = mb & 0x1F; let mb_hi = (mb >> 5) & 1; (30 << 26) | (rs << 21) | (ra << 16) | (sh_lo << 11) | (mb_lo << 6) | (mb_hi << 5) | (0 << 2) | (sh_hi << 1) | rc } fn mfspr(rd: u32, spr: u32) -> u32 { let spr_swapped = ((spr & 0x1F) << 5) | ((spr >> 5) & 0x1F); (31 << 26) | (rd << 21) | (spr_swapped << 11) | (339 << 1) } fn mtspr(rs: u32, spr: u32) -> u32 { let spr_swapped = ((spr & 0x1F) << 5) | ((spr >> 5) & 0x1F); (31 << 26) | (rs << 21) | (spr_swapped << 11) | (467 << 1) } // ── Tests ─────────────────────────────────────────────────────────────────── #[test] fn base_mnemonics() { let cases: &[(u32, u32, &str)] = &[ // X-form ALU (Rc and OE bits) (xform_xo3(3, 4, 5, 0, 266, 0), 0x82000000, "add r3,r4,r5"), (xform_xo3(3, 4, 5, 0, 266, 1), 0x82000000, "add. r3,r4,r5"), (xform_xo3(3, 4, 5, 1, 266, 0), 0x82000000, "addo r3,r4,r5"), (xform_xo3(3, 4, 5, 1, 266, 1), 0x82000000, "addo. r3,r4,r5"), (xform_xo3(3, 4, 0, 0, 104, 0), 0x82000000, "neg r3,r4"), (xform_xo3(3, 4, 5, 0, 235, 0), 0x82000000, "mullw r3,r4,r5"), (xform_xo3(3, 4, 5, 0, 491, 0), 0x82000000, "divw r3,r4,r5"), (xform_xo3(3, 4, 5, 0, 75, 1), 0x82000000, "mulhw. r3,r4,r5"), (xform_xo3(3, 4, 5, 0, 11, 1), 0x82000000, "mulhwu. r3,r4,r5"), (xform_xo3(3, 4, 5, 0, 233, 0), 0x82000000, "mulld r3,r4,r5"), // X-form logical (xform_logic(4, 3, 5, 28, 0), 0x82000000, "and r3,r4,r5"), (xform_logic(4, 3, 5, 444, 0), 0x82000000, "or r3,r4,r5 (non-mr: rs!=rb)"), (xform_logic(4, 3, 5, 316, 0), 0x82000000, "xor r3,r4,r5"), (xform_logic(4, 3, 5, 124, 0), 0x82000000, "nor r3,r4,r5"), (xform_logic(4, 3, 5, 476, 0), 0x82000000, "nand r3,r4,r5"), (xform_logic(4, 3, 5, 284, 0), 0x82000000, "eqv r3,r4,r5"), (xform_logic(4, 3, 5, 60, 0), 0x82000000, "andc r3,r4,r5"), (xform_logic(4, 3, 5, 412, 0), 0x82000000, "orc r3,r4,r5"), // X-form shift (xform_logic(4, 3, 5, 24, 0), 0x82000000, "slw r3,r4,r5"), (xform_logic(4, 3, 5, 536, 0), 0x82000000, "srw r3,r4,r5"), (xform_logic(4, 3, 5, 792, 0), 0x82000000, "sraw r3,r4,r5"), (xform_logic(4, 3, 5, 27, 0), 0x82000000, "sld r3,r4,r5"), (xform_logic(4, 3, 5, 539, 0), 0x82000000, "srd r3,r4,r5"), // srawi / sradi (immediate shifts) ((31 << 26) | (4 << 21) | (3 << 16) | (16 << 11) | (824 << 1), 0x82000000, "srawi r3,r4,16"), // Atomics ((31 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (150 << 1) | 1, 0x82000000, "stwcx. r3,r4,r5"), ((31 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (214 << 1) | 1, 0x82000000, "stdcx. r3,r4,r5"), ((31 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (20 << 1), 0x82000000, "lwarx r3,r4,r5"), ((31 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (84 << 1), 0x82000000, "ldarx r3,r4,r5"), // Compares (dform(11, 0, 3, 16), 0x82000000, "cmpwi cr0, r3, 16"), (dform(11, 2 << 2, 3, 16), 0x82000000, "cmpwi cr2, r3, 16"), (dform(10, 0, 3, 16), 0x82000000, "cmplwi cr0, r3, 16"), ((31 << 26) | (3 << 16) | (4 << 11), 0x82000000, "cmpw r3,r4 in cr0"), ((31 << 26) | (1 << 21) | (3 << 16) | (4 << 11), 0x82000000, "cmpd r3,r4"), ((31 << 26) | (3 << 16) | (4 << 11) | (32 << 1), 0x82000000, "cmplw r3,r4"), // D-form ALU/load/store (dform(14, 3, 1, 16), 0x82000000, "addi r3, r1, 16"), (dform(15, 3, 1, 0x100), 0x82000000, "addis r3, r1, 0x100 (ra!=0)"), (dform(7, 3, 4, 5), 0x82000000, "mulli r3, r4, 5"), (dform(8, 3, 4, 5), 0x82000000, "subfic r3, r4, 5"), (dform(12, 3, 4, 16), 0x82000000, "addic r3, r4, 16"), (dform(13, 3, 4, 16), 0x82000000, "addic. r3, r4, 16"), (dform(24, 3, 4, 0x10), 0x82000000, "ori r4, r3, 0x10 (non-nop)"), (dform(25, 3, 4, 0x10), 0x82000000, "oris r4, r3, 0x10"), (dform(26, 3, 4, 0x10), 0x82000000, "xori r4, r3, 0x10"), (dform(28, 3, 4, 0x10), 0x82000000, "andi. r4, r3, 0x10"), // Loads/stores D-form (dform(32, 5, 1, 0x20), 0x82000000, "lwz r5, 0x20(r1)"), (dform(36, 5, 1, 0x20), 0x82000000, "stw r5, 0x20(r1)"), (dform(34, 5, 1, 0x20), 0x82000000, "lbz r5, 0x20(r1)"), (dform(40, 5, 1, 0x20), 0x82000000, "lhz r5, 0x20(r1)"), (dform(48, 5, 1, 0x20), 0x82000000, "lfs f5, 0x20(r1)"), (dform(50, 5, 1, 0x20), 0x82000000, "lfd f5, 0x20(r1)"), (dform(54, 5, 1, 0x20), 0x82000000, "stfd f5, 0x20(r1)"), // DS-form 64-bit loads ((58u32 << 26) | (5 << 21) | (1 << 16) | 0x20, 0x82000000, "ld r5, 0x20(r1)"), ((62u32 << 26) | (5 << 21) | (1 << 16) | 0x20, 0x82000000, "std r5, 0x20(r1)"), // Sync / barrier (parameterless) ((31 << 26) | (598 << 1), 0x82000000, "sync 0 (extends to sync)"), ((19 << 26) | (150 << 1), 0x82000000, "isync"), ((31 << 26) | (854 << 1), 0x82000000, "eieio"), // Cache hints ((31 << 26) | (1 << 16) | (2 << 11) | (54 << 1), 0x82000000, "dcbst r1, r2"), ((31 << 26) | (1 << 16) | (2 << 11) | (86 << 1), 0x82000000, "dcbf r1, r2"), ((31 << 26) | (1 << 16) | (2 << 11) | (278 << 1), 0x82000000, "dcbt r1, r2"), ((31 << 26) | (1 << 16) | (2 << 11) | (1014 << 1), 0x82000000, "dcbz r1, r2"), ((31 << 26) | (1 << 21) | (1 << 16) | (2 << 11) | (1014 << 1), 0x82000000, "dcbz128 r1, r2"), // CR logical (without simplification triggers) ((19 << 26) | (4 << 21) | (5 << 16) | (6 << 11) | (33 << 1), 0x82000000, "crnor 4,5,6 (no simplify)"), ((19 << 26) | (4 << 21) | (5 << 16) | (6 << 11) | (257 << 1), 0x82000000, "crand 4,5,6"), ((19 << 26) | (4 << 21) | (5 << 16) | (6 << 11) | (449 << 1), 0x82000000, "cror 4,5,6 (no simplify)"), // Trap (no simplification: TO=11 doesn't match the table) ((31 << 26) | (11 << 21) | (3 << 16) | (4 << 11) | (4 << 1), 0x82000000, "tw 11, r3, r4 (uncommon TO)"), ((2u32 << 26) | (11 << 21) | (3 << 16) | (123u32 & 0xFFFF), 0x82000000, "tdi 11, r3, 123"), // mtcr (extended): mtcrf 0xFF, r5 ((31 << 26) | (5 << 21) | (0xFF << 12) | (144 << 1), 0x82000000, "mtcrf 0xFF, r5 → mtcr"), // mfcr / mfmsr / mtmsr / mtmsrd ((31 << 26) | (5 << 21) | (19 << 1), 0x82000000, "mfcr r5"), ((31 << 26) | (5 << 21) | (83 << 1), 0x82000000, "mfmsr r5"), ((31 << 26) | (5 << 21) | (146 << 1), 0x82000000, "mtmsr r5"), ((31 << 26) | (5 << 21) | (178 << 1), 0x82000000, "mtmsrd r5"), // FPU base ((63u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (21 << 1), 0x82000000, "fadd f3, f4, f5"), ((63u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (20 << 1), 0x82000000, "fsub f3, f4, f5"), ((63u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (18 << 1), 0x82000000, "fdiv f3, f4, f5"), ((63u32 << 26) | (3 << 21) | (5 << 21) | (5 << 11) | (25 << 1), 0x82000000, "fmul f3, f0, f5 (encoded)"), ((63u32 << 26) | (3 << 21) | (4 << 16) | (40 << 1), 0x82000000, "fneg f3, f4"), ((63u32 << 26) | (3 << 21) | (4 << 16) | (72 << 1), 0x82000000, "fmr f3, f4"), // mtfsf — XFL form (Fix 1). FM at LSB bits 17-24 (PPC bits 7-14). // Encoding: opcode 63 | FM<<17 | frB<<11 | XO=711<<1 | Rc. ((63u32 << 26) | (0xFF << 17) | (5 << 11) | (711 << 1), 0x82000000, "mtfsf 0xFF, f5 (Rc=0)"), ((63u32 << 26) | (0xFF << 17) | (5 << 11) | (711 << 1) | 1, 0x82000000, "mtfsf. 0xFF, f5 (Rc=1)"), ]; assert_or_regen("base_mnemonics.json", cases); } #[test] fn extended_mnemonics() { let cases: &[(u32, u32, &str)] = &[ // ori r0, r0, 0 → nop (dform(24, 0, 0, 0), 0x82000000, "nop"), // addi r3, r0, imm → li (dform(14, 3, 0, 16), 0x82000000, "li r3, 16"), (dform(14, 3, 0, -1), 0x82000000, "li r3, -1"), // addi r3, r4, neg → subi (dform(14, 3, 4, -16), 0x82000000, "subi r3, r4, 16"), // addis r3, r0, imm → lis (dform(15, 3, 0, 0x1234), 0x82000000, "lis r3, 0x1234"), // addis r3, r4, neg → subis (dform(15, 3, 4, -1), 0x82000000, "subis r3, r4, 0xFFFF"), // or rA, rS, rS → mr (xform_logic(4, 3, 4, 444, 0), 0x82000000, "mr r3, r4"), (xform_logic(4, 3, 4, 444, 1), 0x82000000, "mr. r3, r4"), // and rA, rS, rS → mr (also) (xform_logic(4, 3, 4, 28, 0), 0x82000000, "mr (via and)"), // nor rA, rS, rS → not (xform_logic(4, 3, 4, 124, 0), 0x82000000, "not r3, r4"), // subf → sub (operand swap) (xform_xo3(3, 4, 5, 0, 40, 0), 0x82000000, "subf → sub r3, r5, r4"), // rlwinm simplifications (rlwinm(4, 3, 4, 0, 31 - 4, 0), 0x82000000, "slwi r3, r4, 4"), (rlwinm(4, 3, 32 - 4, 4, 31, 0), 0x82000000, "srwi r3, r4, 4"), (rlwinm(4, 3, 8, 0, 31, 0), 0x82000000, "rotlwi r3, r4, 8"), (rlwinm(4, 3, 0, 4, 31, 0), 0x82000000, "clrlwi r3, r4, 4"), (rlwinm(4, 3, 0, 0, 27, 0), 0x82000000, "clrrwi r3, r4, 4"), (rlwinm(4, 3, 8, 0, 7, 0), 0x82000000, "extlwi r3, r4, 8, 8"), // rlwinm with Rc (rlwinm(4, 3, 4, 0, 31 - 4, 1), 0x82000000, "slwi. r3, r4, 4"), // rlwinm Sylpheed regression (rlwinm(11, 11, 0, 31, 31, 1), 0x82000000, "rlwinm. r11,r11,0,31,31 (no simplify)"), // rldicl simplifications (rldicl(4, 3, 0, 32, 0), 0x82000000, "clrldi r3, r4, 32"), (rldicl(4, 3, 64u32 - 8, 8, 0), 0x82000000, "srdi r3, r4, 8"), (rldicl(4, 3, 8, 0, 0), 0x82000000, "rotldi r3, r4, 8"), // cmpi / cmpli → cmpwi/cmpdi/cmplwi/cmpldi (dform(11, 0, 3, 16), 0x82000000, "cmpwi cr0, r3, 16"), (dform(11, (1 << 21) | (2 << 23), 3, 16) | (1 << 21), 0x82000000, "cmpdi (L=1) variant"), // bclr 20, 0 → blr (xlform_bclr(20, 0, 0), 0x82000000, "blr"), (xlform_bclr(20, 0, 1), 0x82000000, "blrl"), // bcctr 20, 0 → bctr (xlform_bcctr(20, 0, 0), 0x82000000, "bctr"), (xlform_bcctr(20, 0, 1), 0x82000000, "bctrl"), // bclr conditional (xlform_bclr(12, 2, 0), 0x82000000, "beqlr (BO=12, BI=2 → cr0.eq true)"), (xlform_bclr(4, 2, 0), 0x82000000, "bnelr"), // bc with full BO/BI: branch always (BO=20) (bform_bc(20, 0, 0x40, 0, 0), 0x82000000, "bc → b 0x82000040"), (bform_bc(20, 0, 0x40, 0, 1), 0x82000000, "bc l → bl 0x82000040"), // Conditional bc → beq/bne/etc (bform_bc(12, 2, 0x40, 0, 0), 0x82000000, "bc 12,cr0.eq → beq 0x82000040"), (bform_bc(4, 2, 0x40, 0, 0), 0x82000000, "bc 4,cr0.eq → bne 0x82000040"), (bform_bc(12, 0, 0x40, 0, 0), 0x82000000, "bc 12,cr0.lt → blt 0x82000040"), (bform_bc(4, 0, 0x40, 0, 0), 0x82000000, "bc 4,cr0.lt → bge 0x82000040"), (bform_bc(12, 1, 0x40, 0, 0), 0x82000000, "bc 12,cr0.gt → bgt 0x82000040"), (bform_bc(4, 1, 0x40, 0, 0), 0x82000000, "bc 4,cr0.gt → ble 0x82000040"), // Conditional with non-zero CR field (bform_bc(12, 2 + 8, 0x40, 0, 0), 0x82000000, "bc 12, cr2.eq → beq cr2, 0x...040"), // bdnz / bdz (decrement-CTR branches) (bform_bc(16, 0, 0x40, 0, 0), 0x82000000, "bdnz 0x82000040"), (bform_bc(18, 0, 0x40, 0, 0), 0x82000000, "bdz 0x82000040"), // I-form branches (iform_b(0x40, 0, 0), 0x82000000, "b +0x40 → 0x82000040"), (iform_b(0x40, 0, 1), 0x82000000, "bl +0x40 → 0x82000040"), (iform_b(0x40, 1, 0), 0x82000000, "ba 0x40 absolute"), (iform_b(0x40, 1, 1), 0x82000000, "bla 0x40 absolute"), // Trap immediate simplifications ((2u32 << 26) | (4 << 21) | (3 << 16) | (123u32 & 0xFFFF), 0x82000000, "tdeqi r3, 123"), ((3u32 << 26) | (16 << 21) | (3 << 16) | (123u32 & 0xFFFF), 0x82000000, "twlti r3, 123"), // mfspr → mflr / mfctr / mfxer (mfspr(3, 8), 0x82000000, "mflr r3"), (mfspr(3, 9), 0x82000000, "mfctr r3"), (mfspr(3, 1), 0x82000000, "mfxer r3"), // mtspr → mtlr / mtctr / mtxer (mtspr(3, 8), 0x82000000, "mtlr r3"), (mtspr(3, 9), 0x82000000, "mtctr r3"), (mtspr(3, 1), 0x82000000, "mtxer r3"), // crnor with same source bits → crnot ((19 << 26) | (4 << 21) | (5 << 16) | (5 << 11) | (33 << 1), 0x82000000, "crnot 4, 5"), // crxor with all same → crclr ((19 << 26) | (4 << 21) | (4 << 16) | (4 << 11) | (193 << 1), 0x82000000, "crclr 4"), // creqv with all same → crset ((19 << 26) | (4 << 21) | (4 << 16) | (4 << 11) | (289 << 1), 0x82000000, "crset 4"), // cror with same source bits → crmove ((19 << 26) | (4 << 21) | (5 << 16) | (5 << 11) | (449 << 1), 0x82000000, "crmove 4, 5"), // sync L=1 → lwsync ((31 << 26) | (1 << 21) | (598 << 1), 0x82000000, "lwsync"), // tw 31, 0, 0 → trap ((31 << 26) | (31 << 21) | (4 << 1), 0x82000000, "trap"), // Fix 2: bclr/bcctr with BO=20 and BI≠0 still emits blr/bctr ext. // BO=20 ignores both CTR test and CR test, so BI is don't-care. (xlform_bclr(20, 4, 0), 0x82000000, "blr (BO=20, BI=4 — BI is don't-care)"), (xlform_bclr(20, 7, 1), 0x82000000, "blrl (BO=20, BI=7)"), (xlform_bcctr(20, 4, 0), 0x82000000, "bctr (BO=20, BI=4)"), // Fix 3: trap unsigned simplified mnemonics (TO=1, 2, 5, 6 — logical // compare conditions). Register form (tw/td) and immediate (twi/tdi). ((31u32 << 26) | (2 << 21) | (3 << 16) | (4 << 11) | (4 << 1), 0x82000000, "twllt r3, r4 (TO=2)"), ((31u32 << 26) | (1 << 21) | (3 << 16) | (4 << 11) | (4 << 1), 0x82000000, "twlgt r3, r4 (TO=1)"), ((31u32 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (68 << 1), 0x82000000, "tdlge r3, r4 (TO=5)"), ((31u32 << 26) | (6 << 21) | (3 << 16) | (4 << 11) | (4 << 1), 0x82000000, "twlle r3, r4 (TO=6)"), ((3u32 << 26) | (2 << 21) | (3 << 16) | (16u32 & 0xFFFF), 0x82000000, "twllti r3, 16"), ((2u32 << 26) | (5 << 21) | (3 << 16) | (16u32 & 0xFFFF), 0x82000000, "tdlgei r3, 16"), ]; assert_or_regen("extended_mnemonics.json", cases); } #[test] fn vmx128_registers() { // Standard VMX (op=4) — 5-bit registers v0..v31. Verifies that the // low-register path renders correctly through the new formatter. let std_vmx = [ // vaddubm v3, v4, v5 : op=4, 3-op key=0 ((4u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | 0, 0x82000000, "vaddubm v3, v4, v5"), // vaddfp v3, v4, v5 : op=4, vx=10 ((4u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | 10, 0x82000000, "vaddfp v3, v4, v5"), // vand v3, v4, v5 : vx=1028 ((4u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | 1028, 0x82000000, "vand v3, v4, v5"), // vor v3, v4, v5 : vx=1156 ((4u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | 1156, 0x82000000, "vor v3, v4, v5"), // vxor v3, v4, v5 : vx=1220 ((4u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | 1220, 0x82000000, "vxor v3, v4, v5"), // vsel v3, v4, v5, v6 : op=4, va_key=42 (4-op) ((4u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (6 << 6) | 42, 0x82000000, "vsel v3,v4,v5,v6"), // vperm v3, v4, v5, v6 : va_key=43 ((4u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (6 << 6) | 43, 0x82000000, "vperm v3,v4,v5,v6"), // vmaddfp v3, v4, v5, v6 : va_key=46 (operand swap: vd, va, vc, vb) ((4u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (6 << 6) | 46, 0x82000000, "vmaddfp v3, v4, v6, v5 (swap)"), // mfvscr v3 : vx=1540 ((4u32 << 26) | (3 << 21) | 1540, 0x82000000, "mfvscr v3"), // mtvscr v5 : vx=1604, vb=v5 ((4u32 << 26) | (5 << 11) | 1604, 0x82000000, "mtvscr v5"), ]; // VMX128 op=5 — uses vd128/va128/vb128 (7-bit registers, high bits at // 21+22). These are the silent-bug-area encodings; we exercise low // register indices here because the secondary-opcode key for op=5 // includes bits 21-22, constraining vd128 high bits to 0 in this form. // High-index examples for vd128 live in the op=6 series below. let vmx128_op5 = [ // vaddfp128 v3, v4, v5 : op=5, key2=0b000001 ((5u32 << 26) | (3 << 21) | (4 << 16) | (5 << 11) | (0 << 6) | (1 << 0), 0x82000000, "vaddfp128 (encoded sloppily)"), ]; // VMX128 op=6 — vrlimi128 has secondary key in bits 23-25 + 26-27, so // bits 21-22 ARE the high bits of vd128 (canonical silent-bug-area). // These instructions exercise vd128 = 32, 64, 96 — covering the bit-21 // and bit-22 split that ppc.rs's old extractor (now deleted) miscoded. let vrlimi128 = |vd: u32, vb: u32, imm: u32, z: u32| -> u32 { // op=6, vd128 = bits 6-10 + bit 21 + bit 22, vb128 = bits 16-20 + bits 30+31, // IMM = bits 11-15, Z = bits 24-25, key2 = (bits 23-25 << 4) | bits 26-27 = 0b1110001 let vd_lo = vd & 0x1F; let vd_b21 = (vd >> 5) & 1; let vd_b22 = (vd >> 6) & 1; let vb_lo = vb & 0x1F; let vb_b30 = (vb >> 5) & 1; let vb_b31 = (vb >> 6) & 1; // bits 23-25 = 111, bits 26-27 = 00, bit 27 = 1 → key2 lower 4 bits = 0001 // Encoded: bits 23-25 = 111, bits 26-27 = 00 are actually overlapping with z field (bits 24-25) // The plan view: (bits 23 << 6) | (bits 24-25 << 4) | (bits 26-27 << 2) but the table uses different. // Easiest: hand-encode known bit pattern matching decoder.rs's match: // key2 = (extract_bits(code, 23, 25) << 4) | extract_bits(code, 26, 27) = 0b1110001 // bits 23-25 = 111, bits 26-27 = 01 // Bit positions 23-27 = 11101 (5 bits, MSB at 23). // PPC bit 23 (LSB index 8): set // PPC bit 24 (LSB index 7): set -- this is z bit 0 // PPC bit 25 (LSB index 6): set -- this is z bit 1 // PPC bit 26 (LSB index 5): unset // PPC bit 27 (LSB index 4): set // We let z = bits 24-25 stored with vd128 bits at 21-22. // To preserve key2 = 0b1110001, we need bits 24-25 = 11, bit 26 = 0, bit 27 = 1. // BUT bits 24-25 ARE the z field; if we set them = 11 the z value is 3. // So Z is constrained for vrlimi128. Choose Z = 3 (matches Sylpheed examples). let z3 = z & 0x3; (6u32 << 26) | (vd_lo << 21) | (imm << 16) | (vb_lo << 11) | (vd_b21 << 10) // bit 21 (LSB pos 10) | (vd_b22 << 9) // bit 22 (LSB pos 9) | (1 << 8) // bit 23 | (z3 << 6) // bits 24-25 | (0 << 5) // bit 26 | (1 << 4) // bit 27 | (vb_b30 << 1) // bit 30 | vb_b31 // bit 31 }; // Note: VMX128 op6 secondary keys constrain bits 21-23. For // vrlimi128 (key2 = 0b1110001 over bits 21-23 + 26-27) the only // valid vd128 range is 96..=127 — lower values change the secondary // key into some other instruction. The cases below record what the // disassembler emits for the borderline encodings, so a regression // in either the lookup table or the formatter would surface here. let vmx128_high = [ // bits 21-22 = 00 → key2 ≠ vrlimi128 → decodes to vsrw128 (key5 // branch). Locks current behavior; shows the silent-bug-area // encoding constraint. (vrlimi128(0, 12, 4, 3), 0x82000000, "encoding vd_hi=00: actually vsrw128"), // bits 21-22 = 10 → still not vrlimi128. (vrlimi128(32, 12, 4, 3), 0x82000000, "encoding vd_hi=10: actually vsrw128 v32"), // bits 21-22 = 01 → key1 matches vpermwi128. (vrlimi128(64, 12, 4, 3), 0x82000000, "encoding vd_hi=01: actually vpermwi128"), // bits 21-22 = 11 → key2 matches vrlimi128 with vd128=96. (vrlimi128(96, 12, 4, 3), 0x82000000, "vrlimi128 v96, v12, 4, 3 (real)"), (vrlimi128(127, 127, 4, 3), 0x82000000, "vrlimi128 v127, v127, 4, 3 (real)"), ]; // Fix 4: VMX128 multiply-add 4-operand layouts. Per canary, the addend // is the VD register re-used; operand order differs between the three // mnemonics. Encodings hand-built to satisfy decode_op5's key2 secondary // opcode (vmaddfp128=0b001101, vmaddcfp128=0b010001, vnmsubfp128=0b010101) // with bit 22=0 (forced by key2's high nibble) so vd128 high bit 1 = 0. // vd128 low = 3 (bits 6-10); va128 = 3 | (bit29<<5) = 35; vb128 = 5. // Distinct VD vs VA verifies the layout isn't trivially aliasing VD. // // layout (canary): // vmaddfp128 VD, VA, VB, VD → "v3, v35, v5, v3" // vmaddcfp128 VD, VA, VD, VB → "v3, v35, v3, v5" // vnmsubfp128 VD, VA, VD, VB → "v3, v35, v3, v5" let vmx128_4op = [ // vmaddfp128: bits 24=1, 25=1, 27=1, bit 29=1 (VA high), VB=5 (0x146028D4u32, 0x82000000, "vmaddfp128 v3, v35, v5, v3"), // vmaddcfp128: bits 23=1, 27=1, bit 29=1, VB=5 (0x14602914u32, 0x82000000, "vmaddcfp128 v3, v35, v3, v5"), // vnmsubfp128: bits 23=1, 25=1, 27=1, bit 29=1, VB=5 (0x14602954u32, 0x82000000, "vnmsubfp128 v3, v35, v3, v5"), ]; let mut all = Vec::new(); all.extend_from_slice(&std_vmx); all.extend_from_slice(&vmx128_op5); all.extend_from_slice(&vmx128_high); all.extend_from_slice(&vmx128_4op); assert_or_regen("vmx128_registers.json", &all); } #[test] fn sradi_shift_32_decodes_to_32() { // sradi rA, rS, 32: sh=32 → sh[4:0]=0, sh[5]=1 // After PPCBUG-040 fix, sh64() must return 32, not 1. let instr: DecodedInstr = decode(rldicl(3, 4, 32, 63, 0), 0); // rldicl with mb=63 is not sradi, but tests sh64() extraction. assert_eq!(instr.sh64(), 32, "sh64 must return 32 for sh=32 (sh5=1, sh_lo=0)"); } #[test] fn sh64_shift_1_decodes_correctly() { // sh=1: sh[4:0]=1, sh[5]=0 → sh64() must return 1 let instr: DecodedInstr = decode(rldicl(3, 4, 1, 0, 0), 0); assert_eq!(instr.sh64(), 1, "sh64 must return 1 for sh=1"); } #[test] fn sh64_shift_63_decodes_correctly() { // sh=63: sh[4:0]=31=0x1F, sh[5]=1 → sh64() must return 63 let instr: DecodedInstr = decode(rldicl(3, 4, 63, 0, 0), 0); assert_eq!(instr.sh64(), 63, "sh64 must return 63 for sh=63"); }