//! PowerPC (Xbox 360 Xenon) text disassembler. //! //! Single source of truth for assembly text formatting. Sits on top of the //! canonical decoder in [`crate::decoder`] and consumes [`DecodedInstr`] //! (8-byte `Copy`, no allocations) so the interpreter's decode cache stays //! lean — formatting allocates, but only when a sink calls [`format`]. //! //! [`format`] returns a [`DisasmText`] carrying both base and extended //! (simplified) mnemonic forms. Callers (text printer, JSON sink, DuckDB //! row writer) consume the fields directly instead of re-parsing. use crate::decoder::{DecodedInstr, extract_vx128_uimm5}; use crate::opcode::PpcOpcode; /// Formatted disassembly of a single instruction. /// /// Owns its strings. `mnemonic`/`operands` are the structured base form /// (e.g. `"addi"`, `"r3, r1, 16"`); `disasm` is the legacy padded display /// form (e.g. `"addi r3, r1, 16"`). The `ext_*` triple is `Some` when /// a simplified/extended mnemonic applies (e.g. `addi r3,0,imm` → /// `li r3, imm`). `branch_target` is the resolved absolute target for /// direct branches (`b`/`bl`/`bc`/`bcl`); `None` for indirect branches /// and non-branches. #[derive(Debug, Clone)] pub struct DisasmText { pub mnemonic: String, pub operands: String, pub disasm: String, pub ext_mnemonic: Option, pub ext_operands: Option, pub ext_disasm: Option, pub branch_target: Option, } impl DisasmText { /// Preferred display form: extended if present, else base. #[inline] pub fn display(&self) -> &str { self.ext_disasm.as_deref().unwrap_or(&self.disasm) } } // ── Internal builders ─────────────────────────────────────────────────────── #[inline] fn pad_into(mnem: &str, operands: &str, width: usize) -> String { if width <= mnem.len() + 1 { // No padding fits — fall back to single-space join. if operands.is_empty() { mnem.to_string() } else { format!("{mnem} {operands}") } } else { format!("{: DisasmText { let disasm = pad_into(mnem, &operands, pad); DisasmText { mnemonic: mnem.to_string(), operands, disasm, ext_mnemonic: None, ext_operands: None, ext_disasm: None, branch_target: None, } } fn with_ext( base_mnem: &str, base_ops: String, base_pad: usize, ext_mnem: &str, ext_ops: String, ext_pad: usize, ) -> DisasmText { let disasm = pad_into(base_mnem, &base_ops, base_pad); let ext_disasm = pad_into(ext_mnem, &ext_ops, ext_pad); DisasmText { mnemonic: base_mnem.to_string(), operands: base_ops, disasm, ext_mnemonic: Some(ext_mnem.to_string()), ext_operands: Some(ext_ops), ext_disasm: Some(ext_disasm), branch_target: None, } } fn with_target(mut t: DisasmText, target: u32) -> DisasmText { t.branch_target = Some(target); t } fn long_word(raw: u32) -> DisasmText { let operands = format!("0x{raw:08X}"); base(".long", operands, 8) } // ── Helpers (register names, sign extension, condition decoding) ──────────── #[inline] fn gpr(r: usize) -> String { format!("r{r}") } #[inline] fn fpr(r: usize) -> String { format!("f{r}") } #[inline] fn vr(r: usize) -> String { format!("v{r}") } fn crb(b: u32) -> String { let cr = b / 4; let bit = b % 4; let bit_name = ["lt", "gt", "eq", "so"][bit as usize]; if cr == 0 { bit_name.to_string() } else { format!("4*cr{cr}+{bit_name}") } } fn spr_name(spr: u32) -> String { match spr { 1 => "XER".into(), 8 => "LR".into(), 9 => "CTR".into(), _ => format!("spr{spr}"), } } #[inline] fn sign_ext(val: u32, bits: u32) -> i32 { let shift = 32 - bits; ((val << shift) as i32) >> shift } /// Map trap TO field to condition suffix (e.g. 16 → "lt", 4 → "eq"). /// Unsigned variants (`lgt`/`llt`/`lge`/`lle`) cover bits 1-3 of the TO /// encoding which `tw`/`td` use for logical-compare conditions. fn trap_cond(to: u32) -> Option<&'static str> { match to { 1 => Some("lgt"), 2 => Some("llt"), 4 => Some("eq"), 5 => Some("lge"), 6 => Some("lle"), 8 => Some("gt"), 12 => Some("ge"), 16 => Some("lt"), 20 => Some("le"), 24 => Some("ne"), 31 => Some(""), // unconditional _ => None, } } /// For non-decrementing conditional branches: returns Some((cond_name, cr_prefix)) /// where cr_prefix is e.g. "" or "cr2, ". fn cond_branch_ext(bo: u32, bi: u32) -> Option<(&'static str, String)> { let cond_true = bo & 0x08 != 0; let no_cond = bo & 0x10 != 0; let decr = bo & 0x04 == 0; if no_cond || decr { return None; } let cr_field = bi / 4; let cr_bit = bi % 4; let cond_name = match (cr_bit, cond_true) { (0, true) => "lt", (0, false) => "ge", (1, true) => "gt", (1, false) => "le", (2, true) => "eq", (2, false) => "ne", (3, true) => "so", (3, false) => "ns", _ => return None, }; let cr = if cr_field == 0 { String::new() } else { format!("cr{cr_field}, ") }; Some((cond_name, cr)) } #[inline] fn rc_dot(instr: &DecodedInstr) -> &'static str { if instr.rc_bit() { "." } else { "" } } // ── Public entrypoints ────────────────────────────────────────────────────── /// Format a decoded instruction into structured disassembly text. pub fn format(instr: &DecodedInstr) -> DisasmText { match instr.opcode { // ── Branch ────────────────────────────────────────────────────────── PpcOpcode::bx => fmt_b(instr), PpcOpcode::bcx => fmt_bc(instr), PpcOpcode::bclrx => fmt_bclr(instr), PpcOpcode::bcctrx => fmt_bcctr(instr), PpcOpcode::sc => base("sc", String::new(), 0), // ── Trap ──────────────────────────────────────────────────────────── PpcOpcode::tdi => fmt_trap_imm(instr, "tdi", "td"), PpcOpcode::twi => fmt_trap_imm(instr, "twi", "tw"), PpcOpcode::td => fmt_trap_reg(instr, "td"), PpcOpcode::tw => fmt_trap_reg(instr, "tw"), // ── D-form ALU/logical ────────────────────────────────────────────── PpcOpcode::addi => fmt_addi(instr), PpcOpcode::addis => fmt_addis(instr), PpcOpcode::addic => fmt_d_add(instr, "addic"), PpcOpcode::addicx => fmt_d_add(instr, "addic."), PpcOpcode::subficx => fmt_d_imm_simple(instr, "subfic"), PpcOpcode::mulli => fmt_d_imm_simple(instr, "mulli"), PpcOpcode::cmpi => fmt_cmp_imm(instr, "cmpi", true), PpcOpcode::cmpli => fmt_cmp_imm(instr, "cmpli", false), PpcOpcode::ori => fmt_ori(instr), PpcOpcode::oris => fmt_d_logic(instr, "oris"), PpcOpcode::xori => fmt_d_logic(instr, "xori"), PpcOpcode::xoris => fmt_d_logic(instr, "xoris"), PpcOpcode::andix => fmt_d_logic(instr, "andi."), PpcOpcode::andisx => fmt_d_logic(instr, "andis."), // ── D-form load/store ─────────────────────────────────────────────── PpcOpcode::lwz => fmt_ld(instr, "lwz", false), PpcOpcode::lwzu => fmt_ld(instr, "lwzu", false), PpcOpcode::lbz => fmt_ld(instr, "lbz", false), PpcOpcode::lbzu => fmt_ld(instr, "lbzu", false), PpcOpcode::lhz => fmt_ld(instr, "lhz", false), PpcOpcode::lhzu => fmt_ld(instr, "lhzu", false), PpcOpcode::lha => fmt_ld(instr, "lha", false), PpcOpcode::lhau => fmt_ld(instr, "lhau", false), PpcOpcode::lmw => fmt_ld(instr, "lmw", false), PpcOpcode::lfs => fmt_ld(instr, "lfs", true), PpcOpcode::lfsu => fmt_ld(instr, "lfsu", true), PpcOpcode::lfd => fmt_ld(instr, "lfd", true), PpcOpcode::lfdu => fmt_ld(instr, "lfdu", true), PpcOpcode::stw => fmt_st(instr, "stw", false), PpcOpcode::stwu => fmt_st(instr, "stwu", false), PpcOpcode::stb => fmt_st(instr, "stb", false), PpcOpcode::stbu => fmt_st(instr, "stbu", false), PpcOpcode::sth => fmt_st(instr, "sth", false), PpcOpcode::sthu => fmt_st(instr, "sthu", false), PpcOpcode::stmw => fmt_st(instr, "stmw", false), PpcOpcode::stfs => fmt_st(instr, "stfs", true), PpcOpcode::stfsu => fmt_st(instr, "stfsu", true), PpcOpcode::stfd => fmt_st(instr, "stfd", true), PpcOpcode::stfdu => fmt_st(instr, "stfdu", true), // ── DS-form load/store ────────────────────────────────────────────── PpcOpcode::ld => fmt_ds(instr, "ld"), PpcOpcode::ldu => fmt_ds(instr, "ldu"), PpcOpcode::lwa => fmt_ds(instr, "lwa"), PpcOpcode::std => fmt_ds(instr, "std"), PpcOpcode::stdu => fmt_ds(instr, "stdu"), // ── Rotate ───────────────────────────────────────────────────────── PpcOpcode::rlwimix => fmt_rlwimi(instr), PpcOpcode::rlwinmx => fmt_rlwinm(instr), PpcOpcode::rlwnmx => fmt_rlwnm(instr), PpcOpcode::rldiclx => fmt_rldicl(instr), PpcOpcode::rldicrx => fmt_rldicr(instr), PpcOpcode::rldicx => fmt_rldic(instr), PpcOpcode::rldimix => fmt_rldimi(instr), PpcOpcode::rldclx => fmt_rldcl(instr), PpcOpcode::rldcrx => fmt_rldcr(instr), // ── Compare (X-form) ─────────────────────────────────────────────── PpcOpcode::cmp => fmt_cmp_reg(instr, "cmp"), PpcOpcode::cmpl => fmt_cmp_reg(instr, "cmpl"), // ── X-form ALU (3-register) with OE/Rc ───────────────────────────── PpcOpcode::addx => fmt_xo_3op(instr, "add"), PpcOpcode::addcx => fmt_xo_3op(instr, "addc"), PpcOpcode::addex => fmt_xo_3op(instr, "adde"), PpcOpcode::addmex => fmt_xo_2op(instr, "addme"), PpcOpcode::addzex => fmt_xo_2op(instr, "addze"), PpcOpcode::subfx => fmt_subf(instr, "subf", "sub"), PpcOpcode::subfcx => fmt_subf(instr, "subfc", "subc"), PpcOpcode::subfex => fmt_xo_3op(instr, "subfe"), PpcOpcode::subfmex => fmt_xo_2op(instr, "subfme"), PpcOpcode::subfzex => fmt_xo_2op(instr, "subfze"), PpcOpcode::negx => fmt_xo_2op(instr, "neg"), PpcOpcode::mullwx => fmt_xo_3op(instr, "mullw"), PpcOpcode::mulhwx => fmt_xo_3op_no_oe(instr, "mulhw"), PpcOpcode::mulhwux => fmt_xo_3op_rc_only(instr, "mulhwu"), PpcOpcode::divwx => fmt_xo_3op(instr, "divw"), PpcOpcode::divwux => fmt_xo_3op(instr, "divwu"), PpcOpcode::mulldx => fmt_xo_3op(instr, "mulld"), PpcOpcode::mulhdx => fmt_xo_3op_rc_only(instr, "mulhd"), PpcOpcode::mulhdux => fmt_xo_3op_rc_only(instr, "mulhdu"), PpcOpcode::divdx => fmt_xo_3op(instr, "divd"), PpcOpcode::divdux => fmt_xo_3op(instr, "divdu"), // ── X-form logical (Rc) ──────────────────────────────────────────── PpcOpcode::andx => fmt_logic_and(instr), PpcOpcode::andcx => fmt_x_logic(instr, "andc"), PpcOpcode::orx => fmt_logic_or(instr), PpcOpcode::orcx => fmt_x_logic(instr, "orc"), PpcOpcode::xorx => fmt_x_logic(instr, "xor"), PpcOpcode::norx => fmt_logic_nor(instr), PpcOpcode::nandx => fmt_x_logic(instr, "nand"), PpcOpcode::eqvx => fmt_x_logic(instr, "eqv"), PpcOpcode::extsbx => fmt_x_unary_rc(instr, "extsb"), PpcOpcode::extshx => fmt_x_unary_rc(instr, "extsh"), PpcOpcode::extswx => fmt_x_unary_rc(instr, "extsw"), PpcOpcode::cntlzwx => fmt_x_unary_rc(instr, "cntlzw"), PpcOpcode::cntlzdx => fmt_x_unary_rc(instr, "cntlzd"), // ── Shift (32 / 64) ───────────────────────────────────────────────── PpcOpcode::slwx => fmt_x_logic(instr, "slw"), PpcOpcode::srwx => fmt_x_logic(instr, "srw"), PpcOpcode::srawx => fmt_x_logic(instr, "sraw"), PpcOpcode::sldx => fmt_x_logic(instr, "sld"), PpcOpcode::srdx => fmt_x_logic(instr, "srd"), PpcOpcode::sradx => fmt_x_logic(instr, "srad"), PpcOpcode::srawix => fmt_srawi(instr), PpcOpcode::sradix => fmt_sradi(instr), // ── Special register moves ───────────────────────────────────────── PpcOpcode::mfspr => fmt_mfspr(instr), PpcOpcode::mtspr => fmt_mtspr(instr), PpcOpcode::mfcr => base("mfcr", gpr(instr.rd()), 8), PpcOpcode::mtcrf => fmt_mtcrf(instr), PpcOpcode::mfmsr => base("mfmsr", gpr(instr.rd()), 8), PpcOpcode::mtmsr => base("mtmsr", gpr(instr.rs()), 8), PpcOpcode::mtmsrd => base("mtmsrd", gpr(instr.rs()), 8), PpcOpcode::mftb => fmt_mftb(instr), PpcOpcode::mcrxr => base("mcrxr", format!("cr{}", instr.crfd()), 8), PpcOpcode::mcrf => base("mcrf", format!("cr{}, cr{}", instr.crfd(), instr.crfs()), 8), // ── X-form indexed load/store ────────────────────────────────────── PpcOpcode::lwzx => fmt_x_load(instr, "lwzx", false), PpcOpcode::lwzux => fmt_x_load(instr, "lwzux", false), PpcOpcode::lbzx => fmt_x_load(instr, "lbzx", false), PpcOpcode::lbzux => fmt_x_load(instr, "lbzux", false), PpcOpcode::lhzx => fmt_x_load(instr, "lhzx", false), PpcOpcode::lhzux => fmt_x_load(instr, "lhzux", false), PpcOpcode::lhax => fmt_x_load(instr, "lhax", false), PpcOpcode::lhaux => fmt_x_load(instr, "lhaux", false), PpcOpcode::lwax => fmt_x_load(instr, "lwax", false), PpcOpcode::lwaux => fmt_x_load(instr, "lwaux", false), PpcOpcode::ldx => fmt_x_load(instr, "ldx", false), PpcOpcode::ldux => fmt_x_load(instr, "ldux", false), PpcOpcode::lwbrx => fmt_x_load(instr, "lwbrx", false), PpcOpcode::lhbrx => fmt_x_load(instr, "lhbrx", false), PpcOpcode::ldbrx => fmt_x_load(instr, "ldbrx", false), PpcOpcode::lwarx => fmt_x_load(instr, "lwarx", false), PpcOpcode::ldarx => fmt_x_load(instr, "ldarx", false), PpcOpcode::lswx => fmt_x_load(instr, "lswx", false), PpcOpcode::lswi => fmt_lswi_stswi(instr, "lswi"), PpcOpcode::lfsx => fmt_x_load(instr, "lfsx", true), PpcOpcode::lfsux => fmt_x_load(instr, "lfsux", true), PpcOpcode::lfdx => fmt_x_load(instr, "lfdx", true), PpcOpcode::lfdux => fmt_x_load(instr, "lfdux", true), PpcOpcode::stwx => fmt_x_store(instr, "stwx", false), PpcOpcode::stwux => fmt_x_store(instr, "stwux", false), PpcOpcode::stbx => fmt_x_store(instr, "stbx", false), PpcOpcode::stbux => fmt_x_store(instr, "stbux", false), PpcOpcode::sthx => fmt_x_store(instr, "sthx", false), PpcOpcode::sthux => fmt_x_store(instr, "sthux", false), PpcOpcode::stdx => fmt_x_store(instr, "stdx", false), PpcOpcode::stdux => fmt_x_store(instr, "stdux", false), PpcOpcode::stwbrx => fmt_x_store(instr, "stwbrx", false), PpcOpcode::sthbrx => fmt_x_store(instr, "sthbrx", false), PpcOpcode::stdbrx => fmt_x_store(instr, "stdbrx", false), PpcOpcode::stwcx => fmt_x_store(instr, "stwcx.", false), PpcOpcode::stdcx => fmt_x_store(instr, "stdcx.", false), PpcOpcode::stswx => fmt_x_store(instr, "stswx", false), PpcOpcode::stswi => fmt_lswi_stswi(instr, "stswi"), PpcOpcode::stfsx => fmt_x_store(instr, "stfsx", true), PpcOpcode::stfsux => fmt_x_store(instr, "stfsux", true), PpcOpcode::stfdx => fmt_x_store(instr, "stfdx", true), PpcOpcode::stfdux => fmt_x_store(instr, "stfdux", true), PpcOpcode::stfiwx => fmt_x_store(instr, "stfiwx", true), // ── Cache / sync ──────────────────────────────────────────────────── PpcOpcode::dcbf => fmt_cache(instr, "dcbf"), PpcOpcode::dcbi => fmt_cache(instr, "dcbi"), PpcOpcode::dcbst => fmt_cache(instr, "dcbst"), PpcOpcode::dcbt => fmt_cache(instr, "dcbt"), PpcOpcode::dcbtst => fmt_cache(instr, "dcbtst"), PpcOpcode::dcbz => fmt_cache(instr, "dcbz"), PpcOpcode::dcbz128 => fmt_cache(instr, "dcbz128"), PpcOpcode::icbi => fmt_cache(instr, "icbi"), PpcOpcode::sync => { // L-field at PPC bit 10 (host bit 21) selects lwsync (L=1), the // acquire barrier in every Xbox 360 spinlock. PPCBUG-641. if (instr.raw >> 21) & 1 == 1 { with_ext("sync", String::new(), 0, "lwsync", String::new(), 0) } else { base("sync", String::new(), 0) } } PpcOpcode::eieio => base("eieio", String::new(), 0), PpcOpcode::isync => base("isync", String::new(), 0), // ── CR logical ────────────────────────────────────────────────────── PpcOpcode::crand => fmt_cr_logic(instr, "crand"), PpcOpcode::crandc => fmt_cr_logic(instr, "crandc"), PpcOpcode::creqv => fmt_creqv(instr), PpcOpcode::crnand => fmt_cr_logic(instr, "crnand"), PpcOpcode::crnor => fmt_crnor(instr), PpcOpcode::cror => fmt_cror(instr), PpcOpcode::crorc => fmt_cr_logic(instr, "crorc"), PpcOpcode::crxor => fmt_crxor(instr), // ── FPU (op59 / op63) ────────────────────────────────────────────── PpcOpcode::fdivsx => fmt_a_3op(instr, "fdivs", false), PpcOpcode::fsubsx => fmt_a_3op(instr, "fsubs", false), PpcOpcode::faddsx => fmt_a_3op(instr, "fadds", false), PpcOpcode::fsqrtsx => fmt_a_unary(instr, "fsqrts"), PpcOpcode::fresx => fmt_a_unary(instr, "fres"), PpcOpcode::fmulsx => fmt_a_3op(instr, "fmuls", true), PpcOpcode::fmsubsx => fmt_a_4op(instr, "fmsubs"), PpcOpcode::fmaddsx => fmt_a_4op(instr, "fmadds"), PpcOpcode::fnmsubsx => fmt_a_4op(instr, "fnmsubs"), PpcOpcode::fnmaddsx => fmt_a_4op(instr, "fnmadds"), PpcOpcode::fdivx => fmt_a_3op(instr, "fdiv", false), PpcOpcode::fsubx => fmt_a_3op(instr, "fsub", false), PpcOpcode::faddx => fmt_a_3op(instr, "fadd", false), PpcOpcode::fsqrtx => fmt_a_unary(instr, "fsqrt"), PpcOpcode::fselx => fmt_a_4op(instr, "fsel"), PpcOpcode::fmulx => fmt_a_3op(instr, "fmul", true), PpcOpcode::frsqrtex => fmt_a_unary(instr, "frsqrte"), PpcOpcode::fmsubx => fmt_a_4op(instr, "fmsub"), PpcOpcode::fmaddx => fmt_a_4op(instr, "fmadd"), PpcOpcode::fnmsubx => fmt_a_4op(instr, "fnmsub"), PpcOpcode::fnmaddx => fmt_a_4op(instr, "fnmadd"), PpcOpcode::fcmpu => fmt_fcmp(instr, "fcmpu"), PpcOpcode::fcmpo => fmt_fcmp(instr, "fcmpo"), PpcOpcode::frspx => fmt_x_fpu_unary(instr, "frsp"), PpcOpcode::fctiwx => fmt_x_fpu_unary(instr, "fctiw"), PpcOpcode::fctiwzx => fmt_x_fpu_unary(instr, "fctiwz"), PpcOpcode::fnegx => fmt_x_fpu_unary(instr, "fneg"), PpcOpcode::fmrx => fmt_x_fpu_unary(instr, "fmr"), PpcOpcode::fnabsx => fmt_x_fpu_unary(instr, "fnabs"), PpcOpcode::fabsx => fmt_x_fpu_unary(instr, "fabs"), PpcOpcode::fctidx => fmt_x_fpu_unary(instr, "fctid"), PpcOpcode::fctidzx => fmt_x_fpu_unary(instr, "fctidz"), PpcOpcode::fcfidx => fmt_x_fpu_unary(instr, "fcfid"), PpcOpcode::mffsx => { let rc = rc_dot(instr); base(&format!("mffs{rc}"), fpr(instr.rd()), 8) } PpcOpcode::mtfsfx => { let rc = rc_dot(instr); let fxm = (instr.raw >> 17) & 0xFF; let frb = (instr.raw >> 11) & 0x1F; base(&format!("mtfsf{rc}"), format!("0x{fxm:02X}, {}", fpr(frb as usize)), 8) } PpcOpcode::mtfsb1x => fmt_mtfsb(instr, "mtfsb1"), PpcOpcode::mtfsb0x => fmt_mtfsb(instr, "mtfsb0"), PpcOpcode::mtfsfix => { let rc = rc_dot(instr); let bf = instr.crfd(); let imm = (instr.raw >> 12) & 0xF; base(&format!("mtfsfi{rc}"), format!("cr{bf}, {imm}"), 8) } PpcOpcode::mcrfs => base("mcrfs", format!("cr{}, cr{}", instr.crfd(), instr.crfs()), 8), // ── Standard VMX (5-bit registers) ──────────────────────────────── // 3-operand VD, VA, VB PpcOpcode::vaddubm | PpcOpcode::vmaxub | PpcOpcode::vrlb | PpcOpcode::vmuloub | PpcOpcode::vaddfp | PpcOpcode::vmrghb | PpcOpcode::vpkuhum | PpcOpcode::vadduhm | PpcOpcode::vmaxuh | PpcOpcode::vrlh | PpcOpcode::vmulouh | PpcOpcode::vsubfp | PpcOpcode::vmrghh | PpcOpcode::vpkuwum | PpcOpcode::vadduwm | PpcOpcode::vmaxuw | PpcOpcode::vrlw | PpcOpcode::vmrghw | PpcOpcode::vpkuhus | PpcOpcode::vpkuwus | PpcOpcode::vmaxsb | PpcOpcode::vslb | PpcOpcode::vmulosb | PpcOpcode::vmrglb | PpcOpcode::vpkshus | PpcOpcode::vmaxsh | PpcOpcode::vslh | PpcOpcode::vmulosh | PpcOpcode::vmrglh | PpcOpcode::vpkswus | PpcOpcode::vaddcuw | PpcOpcode::vmaxsw | PpcOpcode::vslw | PpcOpcode::vmrglw | PpcOpcode::vpkshss | PpcOpcode::vsl | PpcOpcode::vpkswss | PpcOpcode::vaddubs | PpcOpcode::vminub | PpcOpcode::vsrb | PpcOpcode::vmuleub | PpcOpcode::vadduhs | PpcOpcode::vminuh | PpcOpcode::vsrh | PpcOpcode::vmuleuh | PpcOpcode::vadduws | PpcOpcode::vminuw | PpcOpcode::vsrw | PpcOpcode::vsr | PpcOpcode::vaddsbs | PpcOpcode::vminsb | PpcOpcode::vsrab | PpcOpcode::vmulesb | PpcOpcode::vpkpx | PpcOpcode::vaddshs | PpcOpcode::vminsh | PpcOpcode::vsrah | PpcOpcode::vmulesh | PpcOpcode::vaddsws | PpcOpcode::vminsw | PpcOpcode::vsraw | PpcOpcode::vsububm | PpcOpcode::vavgub | PpcOpcode::vand | PpcOpcode::vmaxfp | PpcOpcode::vslo | PpcOpcode::vsubuhm | PpcOpcode::vavguh | PpcOpcode::vandc | PpcOpcode::vminfp | PpcOpcode::vsro | PpcOpcode::vsubuwm | PpcOpcode::vavguw | PpcOpcode::vor | PpcOpcode::vxor | PpcOpcode::vavgsb | PpcOpcode::vnor | PpcOpcode::vavgsh | PpcOpcode::vsubcuw | PpcOpcode::vavgsw | PpcOpcode::vsububs | PpcOpcode::vsum4ubs| PpcOpcode::vsubuhs | PpcOpcode::vsum4shs | PpcOpcode::vsubuws | PpcOpcode::vsum2sws| PpcOpcode::vsubsbs | PpcOpcode::vsum4sbs | PpcOpcode::vsubshs | PpcOpcode::vsubsws | PpcOpcode::vsumsws => { fmt_vmx_3op(instr, opcode_name(instr.opcode)) } // VMX unary VD, VB PpcOpcode::vrefp | PpcOpcode::vrsqrtefp | PpcOpcode::vexptefp | PpcOpcode::vlogefp | PpcOpcode::vrfin | PpcOpcode::vrfiz | PpcOpcode::vrfip | PpcOpcode::vrfim | PpcOpcode::vupkhsb | PpcOpcode::vupkhsh | PpcOpcode::vupklsb | PpcOpcode::vupklsh | PpcOpcode::vupkhpx | PpcOpcode::vupklpx => { fmt_vmx_unary(instr, opcode_name(instr.opcode)) } // VMX VD, VB, UIMM (VA = uimm field) PpcOpcode::vspltb | PpcOpcode::vsplth | PpcOpcode::vspltw | PpcOpcode::vcfux | PpcOpcode::vcfsx | PpcOpcode::vctuxs | PpcOpcode::vctsxs => { fmt_vmx_uimm(instr, opcode_name(instr.opcode)) } // VMX VD, SIMM (VA field as 5-bit signed immediate) PpcOpcode::vspltisb => fmt_vmx_simm(instr, "vspltisb"), PpcOpcode::vspltish => fmt_vmx_simm(instr, "vspltish"), PpcOpcode::vspltisw => fmt_vmx_simm(instr, "vspltisw"), PpcOpcode::mfvscr => base("mfvscr", vr(instr.rd()), 8), PpcOpcode::mtvscr => base("mtvscr", vr(instr.rb()), 8), // VMX compare (Rc bit at bit 21) PpcOpcode::vcmpequb | PpcOpcode::vcmpequh | PpcOpcode::vcmpequw | PpcOpcode::vcmpeqfp | PpcOpcode::vcmpgefp | PpcOpcode::vcmpgtub | PpcOpcode::vcmpgtuh | PpcOpcode::vcmpgtuw | PpcOpcode::vcmpgtfp | PpcOpcode::vcmpgtsb | PpcOpcode::vcmpgtsh | PpcOpcode::vcmpgtsw | PpcOpcode::vcmpbfp => fmt_vmx_cmp(instr, opcode_name(instr.opcode)), // VMX 4-operand VD, VA, VB, VC PpcOpcode::vmhaddshs | PpcOpcode::vmhraddshs | PpcOpcode::vmladduhm | PpcOpcode::vmsumubm | PpcOpcode::vmsummbm | PpcOpcode::vmsumuhm | PpcOpcode::vmsumuhs | PpcOpcode::vmsumshm | PpcOpcode::vmsumshs | PpcOpcode::vsel | PpcOpcode::vperm => { fmt_vmx_4op(instr, opcode_name(instr.opcode)) } PpcOpcode::vsldoi => fmt_vsldoi(instr), PpcOpcode::vmaddfp => fmt_vmx_4op_swap(instr, "vmaddfp"), PpcOpcode::vnmsubfp => fmt_vmx_4op_swap(instr, "vnmsubfp"), // ── VMX128 load/store (uses GPR addressing + vd128 dest) ─────────── PpcOpcode::lvsl128 => fmt_vmx128_ls(instr, "lvsl128"), PpcOpcode::lvsr128 => fmt_vmx128_ls(instr, "lvsr128"), PpcOpcode::lvewx128 => fmt_vmx128_ls(instr, "lvewx128"), PpcOpcode::lvx128 => fmt_vmx128_ls(instr, "lvx128"), PpcOpcode::lvxl128 => fmt_vmx128_ls(instr, "lvxl128"), PpcOpcode::lvlx128 => fmt_vmx128_ls(instr, "lvlx128"), PpcOpcode::lvrx128 => fmt_vmx128_ls(instr, "lvrx128"), PpcOpcode::lvlxl128 => fmt_vmx128_ls(instr, "lvlxl128"), PpcOpcode::lvrxl128 => fmt_vmx128_ls(instr, "lvrxl128"), PpcOpcode::stvewx128 => fmt_vmx128_ls(instr, "stvewx128"), PpcOpcode::stvx128 => fmt_vmx128_ls(instr, "stvx128"), PpcOpcode::stvxl128 => fmt_vmx128_ls(instr, "stvxl128"), PpcOpcode::stvlx128 => fmt_vmx128_ls(instr, "stvlx128"), PpcOpcode::stvrx128 => fmt_vmx128_ls(instr, "stvrx128"), PpcOpcode::stvlxl128 => fmt_vmx128_ls(instr, "stvlxl128"), PpcOpcode::stvrxl128 => fmt_vmx128_ls(instr, "stvrxl128"), // Standard AltiVec load/store indexed (5-bit vr0-vr31) PpcOpcode::lvsl => fmt_vmx_ls(instr, "lvsl"), PpcOpcode::lvsr => fmt_vmx_ls(instr, "lvsr"), PpcOpcode::lvebx => fmt_vmx_ls(instr, "lvebx"), PpcOpcode::lvehx => fmt_vmx_ls(instr, "lvehx"), PpcOpcode::lvewx => fmt_vmx_ls(instr, "lvewx"), PpcOpcode::lvx => fmt_vmx_ls(instr, "lvx"), PpcOpcode::lvxl => fmt_vmx_ls(instr, "lvxl"), PpcOpcode::lvlx => fmt_vmx_ls(instr, "lvlx"), PpcOpcode::lvrx => fmt_vmx_ls(instr, "lvrx"), PpcOpcode::lvlxl => fmt_vmx_ls(instr, "lvlxl"), PpcOpcode::lvrxl => fmt_vmx_ls(instr, "lvrxl"), PpcOpcode::stvebx => fmt_vmx_ls(instr, "stvebx"), PpcOpcode::stvehx => fmt_vmx_ls(instr, "stvehx"), PpcOpcode::stvewx => fmt_vmx_ls(instr, "stvewx"), PpcOpcode::stvx => fmt_vmx_ls(instr, "stvx"), PpcOpcode::stvxl => fmt_vmx_ls(instr, "stvxl"), PpcOpcode::stvlx => fmt_vmx_ls(instr, "stvlx"), PpcOpcode::stvrx => fmt_vmx_ls(instr, "stvrx"), PpcOpcode::stvlxl => fmt_vmx_ls(instr, "stvlxl"), PpcOpcode::stvrxl => fmt_vmx_ls(instr, "stvrxl"), // ── VMX128 op5 (3-op and 4-op fp/pack/logic) ─────────────────────── PpcOpcode::vaddfp128 => fmt_vmx128_3op(instr, "vaddfp128"), PpcOpcode::vsubfp128 => fmt_vmx128_3op(instr, "vsubfp128"), PpcOpcode::vmulfp128 => fmt_vmx128_3op(instr, "vmulfp128"), PpcOpcode::vmsum3fp128 => fmt_vmx128_3op(instr, "vmsum3fp128"), PpcOpcode::vmsum4fp128 => fmt_vmx128_3op(instr, "vmsum4fp128"), PpcOpcode::vpkshss128 => fmt_vmx128_3op(instr, "vpkshss128"), PpcOpcode::vpkshus128 => fmt_vmx128_3op(instr, "vpkshus128"), PpcOpcode::vpkswss128 => fmt_vmx128_3op(instr, "vpkswss128"), PpcOpcode::vpkswus128 => fmt_vmx128_3op(instr, "vpkswus128"), PpcOpcode::vpkuhum128 => fmt_vmx128_3op(instr, "vpkuhum128"), PpcOpcode::vpkuhus128 => fmt_vmx128_3op(instr, "vpkuhus128"), PpcOpcode::vpkuwum128 => fmt_vmx128_3op(instr, "vpkuwum128"), PpcOpcode::vpkuwus128 => fmt_vmx128_3op(instr, "vpkuwus128"), PpcOpcode::vand128 => fmt_vmx128_3op(instr, "vand128"), PpcOpcode::vandc128 => fmt_vmx128_3op(instr, "vandc128"), PpcOpcode::vnor128 => fmt_vmx128_3op(instr, "vnor128"), PpcOpcode::vor128 => fmt_vmx128_3op(instr, "vor128"), PpcOpcode::vxor128 => fmt_vmx128_3op(instr, "vxor128"), PpcOpcode::vsel128 => fmt_vmx128_3op(instr, "vsel128"), PpcOpcode::vslo128 => fmt_vmx128_3op(instr, "vslo128"), PpcOpcode::vsro128 => fmt_vmx128_3op(instr, "vsro128"), PpcOpcode::vmaddfp128 => fmt_vmaddfp128(instr), PpcOpcode::vmaddcfp128 => fmt_vmx128_madd_vd_vb(instr, "vmaddcfp128"), PpcOpcode::vnmsubfp128 => fmt_vmx128_madd_vd_vb(instr, "vnmsubfp128"), PpcOpcode::vperm128 => fmt_vperm128(instr), PpcOpcode::vsldoi128 => fmt_vsldoi128(instr), PpcOpcode::vpermwi128 => fmt_vpermwi128(instr), // ── VMX128 op6 special ───────────────────────────────────────────── PpcOpcode::vpkd3d128 => fmt_vmx128_pack_d3d(instr, "vpkd3d128"), PpcOpcode::vrlimi128 => fmt_vmx128_pack_d3d(instr, "vrlimi128"), PpcOpcode::vrfim128 => fmt_vmx128_unary(instr, "vrfim128"), PpcOpcode::vrfin128 => fmt_vmx128_unary(instr, "vrfin128"), PpcOpcode::vrfip128 => fmt_vmx128_unary(instr, "vrfip128"), PpcOpcode::vrfiz128 => fmt_vmx128_unary(instr, "vrfiz128"), PpcOpcode::vrefp128 => fmt_vmx128_unary(instr, "vrefp128"), PpcOpcode::vrsqrtefp128 => fmt_vmx128_unary(instr, "vrsqrtefp128"), PpcOpcode::vexptefp128 => fmt_vmx128_unary(instr, "vexptefp128"), PpcOpcode::vlogefp128 => fmt_vmx128_unary(instr, "vlogefp128"), PpcOpcode::vcfpsxws128 => fmt_vmx128_uimm(instr, "vcfpsxws128"), PpcOpcode::vcfpuxws128 => fmt_vmx128_uimm(instr, "vcfpuxws128"), PpcOpcode::vcsxwfp128 => fmt_vmx128_uimm(instr, "vcsxwfp128"), PpcOpcode::vcuxwfp128 => fmt_vmx128_uimm(instr, "vcuxwfp128"), PpcOpcode::vspltw128 => fmt_vmx128_uimm(instr, "vspltw128"), PpcOpcode::vupkd3d128 => fmt_vmx128_uimm(instr, "vupkd3d128"), PpcOpcode::vspltisw128 => { let vd = instr.vd128(); let simm = sign_ext(extract_vx128_uimm5(instr.raw), 5); base("vspltisw128", format!("{}, {simm}", vr(vd)), 14) } PpcOpcode::vcmpeqfp128 => fmt_vmx128_cmp(instr, "vcmpeqfp128"), PpcOpcode::vcmpgefp128 => fmt_vmx128_cmp(instr, "vcmpgefp128"), PpcOpcode::vcmpgtfp128 => fmt_vmx128_cmp(instr, "vcmpgtfp128"), PpcOpcode::vcmpbfp128 => fmt_vmx128_cmp(instr, "vcmpbfp128"), PpcOpcode::vcmpequw128 => fmt_vmx128_cmp(instr, "vcmpequw128"), PpcOpcode::vrlw128 => fmt_vmx128_3op(instr, "vrlw128"), PpcOpcode::vslw128 => fmt_vmx128_3op(instr, "vslw128"), PpcOpcode::vsraw128 => fmt_vmx128_3op(instr, "vsraw128"), PpcOpcode::vsrw128 => fmt_vmx128_3op(instr, "vsrw128"), PpcOpcode::vmaxfp128 => fmt_vmx128_3op(instr, "vmaxfp128"), PpcOpcode::vminfp128 => fmt_vmx128_3op(instr, "vminfp128"), PpcOpcode::vmrghw128 => fmt_vmx128_3op(instr, "vmrghw128"), PpcOpcode::vmrglw128 => fmt_vmx128_3op(instr, "vmrglw128"), PpcOpcode::vupkhsb128 => fmt_vmx128_3op(instr, "vupkhsb128"), PpcOpcode::vupklsb128 => fmt_vmx128_3op(instr, "vupklsb128"), PpcOpcode::Invalid => long_word(instr.raw), } } /// Disassemble a decoded instruction into PPC assembly text. /// /// Back-compat entry point: returns the same single-string the legacy /// formatter produced, preferring the extended form when present. pub fn disassemble(instr: &DecodedInstr) -> String { format(instr).display().to_string() } /// Disassemble a range of instructions from a byte slice. pub fn disassemble_block(data: &[u8], base_addr: u32, count: usize) -> Vec<(u32, String)> { let mut result = Vec::new(); for i in 0..count { let offset = i * 4; if offset + 4 > data.len() { break; } let raw = u32::from_be_bytes([ data[offset], data[offset + 1], data[offset + 2], data[offset + 3], ]); let addr = base_addr + offset as u32; let instr = crate::decoder::decode(raw, addr); let text = disassemble(&instr); result.push((addr, text)); } result } /// One yielded instruction from [`iter_disasm`]. Carries the absolute VA, /// raw word, decoded opcode and the formatted text — everything a sink /// needs to render or persist a single row without re-parsing. #[derive(Debug, Clone)] pub struct DisasmItem { pub addr: u32, pub raw: u32, pub opcode: PpcOpcode, pub text: DisasmText, } /// Iterate over instructions in the VA range `[va_start, va_end)` of an /// image-mapped byte slice. `image[rva]` must hold the byte at absolute VA /// `image_base + rva` (the layout produced by [`xenia_xex::loader`]). /// /// Stops on a truncated tail (less than 4 bytes remaining at the cursor). /// Yields nothing if `va_start >= va_end` or the start RVA is beyond the /// image. pub fn iter_disasm( image: &[u8], image_base: u32, va_start: u32, va_end: u32, ) -> impl Iterator + '_ { DisasmIter { image, image_base, va: va_start, end: va_end } } struct DisasmIter<'a> { image: &'a [u8], image_base: u32, va: u32, end: u32, } impl Iterator for DisasmIter<'_> { type Item = DisasmItem; #[inline] fn next(&mut self) -> Option { if self.va >= self.end { return None; } let rva = self.va.wrapping_sub(self.image_base) as usize; if rva + 4 > self.image.len() { return None; } let raw = u32::from_be_bytes([ self.image[rva], self.image[rva + 1], self.image[rva + 2], self.image[rva + 3], ]); let abs = self.va; let decoded = crate::decoder::decode(raw, abs); let text = format(&decoded); self.va = self.va.wrapping_add(4); Some(DisasmItem { addr: abs, raw, opcode: decoded.opcode, text }) } } // ── Per-class formatters ─────────────────────────────────────────────────── fn opcode_name(op: PpcOpcode) -> &'static str { // Used for VMX where the enum variant name matches the canonical mnemonic. // For ALU/FPU variants ending in "x", use hardcoded strings instead. match op { PpcOpcode::vaddubm => "vaddubm", PpcOpcode::vmaxub => "vmaxub", PpcOpcode::vrlb => "vrlb", PpcOpcode::vmuloub => "vmuloub", PpcOpcode::vaddfp => "vaddfp", PpcOpcode::vmrghb => "vmrghb", PpcOpcode::vpkuhum => "vpkuhum", PpcOpcode::vadduhm => "vadduhm", PpcOpcode::vmaxuh => "vmaxuh", PpcOpcode::vrlh => "vrlh", PpcOpcode::vmulouh => "vmulouh", PpcOpcode::vsubfp => "vsubfp", PpcOpcode::vmrghh => "vmrghh", PpcOpcode::vpkuwum => "vpkuwum", PpcOpcode::vadduwm => "vadduwm", PpcOpcode::vmaxuw => "vmaxuw", PpcOpcode::vrlw => "vrlw", PpcOpcode::vmrghw => "vmrghw", PpcOpcode::vpkuhus => "vpkuhus", PpcOpcode::vpkuwus => "vpkuwus", PpcOpcode::vmaxsb => "vmaxsb", PpcOpcode::vslb => "vslb", PpcOpcode::vmulosb => "vmulosb", PpcOpcode::vmrglb => "vmrglb", PpcOpcode::vpkshus => "vpkshus", PpcOpcode::vmaxsh => "vmaxsh", PpcOpcode::vslh => "vslh", PpcOpcode::vmulosh => "vmulosh", PpcOpcode::vmrglh => "vmrglh", PpcOpcode::vpkswus => "vpkswus", PpcOpcode::vaddcuw => "vaddcuw", PpcOpcode::vmaxsw => "vmaxsw", PpcOpcode::vslw => "vslw", PpcOpcode::vmrglw => "vmrglw", PpcOpcode::vpkshss => "vpkshss", PpcOpcode::vsl => "vsl", PpcOpcode::vpkswss => "vpkswss", PpcOpcode::vaddubs => "vaddubs", PpcOpcode::vminub => "vminub", PpcOpcode::vsrb => "vsrb", PpcOpcode::vmuleub => "vmuleub", PpcOpcode::vadduhs => "vadduhs", PpcOpcode::vminuh => "vminuh", PpcOpcode::vsrh => "vsrh", PpcOpcode::vmuleuh => "vmuleuh", PpcOpcode::vadduws => "vadduws", PpcOpcode::vminuw => "vminuw", PpcOpcode::vsrw => "vsrw", PpcOpcode::vsr => "vsr", PpcOpcode::vaddsbs => "vaddsbs", PpcOpcode::vminsb => "vminsb", PpcOpcode::vsrab => "vsrab", PpcOpcode::vmulesb => "vmulesb", PpcOpcode::vpkpx => "vpkpx", PpcOpcode::vaddshs => "vaddshs", PpcOpcode::vminsh => "vminsh", PpcOpcode::vsrah => "vsrah", PpcOpcode::vmulesh => "vmulesh", PpcOpcode::vaddsws => "vaddsws", PpcOpcode::vminsw => "vminsw", PpcOpcode::vsraw => "vsraw", PpcOpcode::vsububm => "vsububm", PpcOpcode::vavgub => "vavgub", PpcOpcode::vand => "vand", PpcOpcode::vmaxfp => "vmaxfp", PpcOpcode::vslo => "vslo", PpcOpcode::vsubuhm => "vsubuhm", PpcOpcode::vavguh => "vavguh", PpcOpcode::vandc => "vandc", PpcOpcode::vminfp => "vminfp", PpcOpcode::vsro => "vsro", PpcOpcode::vsubuwm => "vsubuwm", PpcOpcode::vavguw => "vavguw", PpcOpcode::vor => "vor", PpcOpcode::vxor => "vxor", PpcOpcode::vavgsb => "vavgsb", PpcOpcode::vnor => "vnor", PpcOpcode::vavgsh => "vavgsh", PpcOpcode::vsubcuw => "vsubcuw", PpcOpcode::vavgsw => "vavgsw", PpcOpcode::vsububs => "vsububs", PpcOpcode::vsum4ubs => "vsum4ubs", PpcOpcode::vsubuhs => "vsubuhs", PpcOpcode::vsum4shs => "vsum4shs", PpcOpcode::vsubuws => "vsubuws", PpcOpcode::vsum2sws => "vsum2sws", PpcOpcode::vsubsbs => "vsubsbs", PpcOpcode::vsum4sbs => "vsum4sbs", PpcOpcode::vsubshs => "vsubshs", PpcOpcode::vsubsws => "vsubsws", PpcOpcode::vsumsws => "vsumsws", PpcOpcode::vrefp => "vrefp", PpcOpcode::vrsqrtefp => "vrsqrtefp", PpcOpcode::vexptefp => "vexptefp", PpcOpcode::vlogefp => "vlogefp", PpcOpcode::vrfin => "vrfin", PpcOpcode::vrfiz => "vrfiz", PpcOpcode::vrfip => "vrfip", PpcOpcode::vrfim => "vrfim", PpcOpcode::vupkhsb => "vupkhsb", PpcOpcode::vupkhsh => "vupkhsh", PpcOpcode::vupklsb => "vupklsb", PpcOpcode::vupklsh => "vupklsh", PpcOpcode::vupkhpx => "vupkhpx", PpcOpcode::vupklpx => "vupklpx", PpcOpcode::vspltb => "vspltb", PpcOpcode::vsplth => "vsplth", PpcOpcode::vspltw => "vspltw", PpcOpcode::vcfux => "vcfux", PpcOpcode::vcfsx => "vcfsx", PpcOpcode::vctuxs => "vctuxs", PpcOpcode::vctsxs => "vctsxs", PpcOpcode::vcmpequb => "vcmpequb", PpcOpcode::vcmpequh => "vcmpequh", PpcOpcode::vcmpequw => "vcmpequw", PpcOpcode::vcmpeqfp => "vcmpeqfp", PpcOpcode::vcmpgefp => "vcmpgefp", PpcOpcode::vcmpgtub => "vcmpgtub", PpcOpcode::vcmpgtuh => "vcmpgtuh", PpcOpcode::vcmpgtuw => "vcmpgtuw", PpcOpcode::vcmpgtfp => "vcmpgtfp", PpcOpcode::vcmpgtsb => "vcmpgtsb", PpcOpcode::vcmpgtsh => "vcmpgtsh", PpcOpcode::vcmpgtsw => "vcmpgtsw", PpcOpcode::vcmpbfp => "vcmpbfp", PpcOpcode::vmhaddshs => "vmhaddshs", PpcOpcode::vmhraddshs => "vmhraddshs", PpcOpcode::vmladduhm => "vmladduhm", PpcOpcode::vmsumubm => "vmsumubm", PpcOpcode::vmsummbm => "vmsummbm", PpcOpcode::vmsumuhm => "vmsumuhm", PpcOpcode::vmsumuhs => "vmsumuhs", PpcOpcode::vmsumshm => "vmsumshm", PpcOpcode::vmsumshs => "vmsumshs", PpcOpcode::vsel => "vsel", PpcOpcode::vperm => "vperm", _ => "?", } } // Branches (I-form: b/bl/ba/bla) — produces base + extended forms. fn fmt_b(instr: &DecodedInstr) -> DisasmText { let aa = instr.aa(); let lk = instr.lk(); let target = if aa { instr.li() as u32 } else { instr.addr.wrapping_add(instr.li() as u32) }; let mnem = match (aa, lk) { (false, false) => "b", (false, true) => "bl", (true, false) => "ba", (true, true) => "bla", }; let ops = format!("0x{target:08X}"); with_target(base(mnem, ops, 8), target) } fn fmt_bc(instr: &DecodedInstr) -> DisasmText { let bo = instr.bo(); let bi = instr.bi(); let aa = instr.aa(); let lk = instr.lk(); let target = if aa { instr.bd() as u32 } else { instr.addr.wrapping_add(instr.bd() as u32) }; let a = if aa { "a" } else { "" }; let l = if lk { "l" } else { "" }; let base_mnem = format!("bc{a}{l}"); let base_ops = format!("{bo}, {}, 0x{target:08X}", crb(bi)); // Extended forms. let cr_field = bi / 4; let cr_bit = bi % 4; let decr = bo & 0x04 == 0; let uncond = bo & 0x10 != 0; let result = if uncond && !decr { // Unconditional branch. let ext_mnem = format!("b{a}{l}"); let ext_ops = format!("0x{target:08X}"); with_ext(&base_mnem, base_ops, 8, &ext_mnem, ext_ops, 8) } else { let cond_true = bo & 0x08 != 0; let cond_name_opt: Option<&'static str> = match (cr_bit, cond_true) { (0, true) => Some("lt"), (0, false) => Some("ge"), (1, true) => Some("gt"), (1, false) => Some("le"), (2, true) => Some("eq"), (2, false) => Some("ne"), (3, true) => Some("so"), (3, false) => Some("ns"), _ => None, }; let cr = if cr_field == 0 { String::new() } else { format!("cr{cr_field}, ") }; if decr { let z = if bo & 0x02 != 0 { "z" } else { "nz" }; // BO bit 4 (uncond) means CR is ignored — pure CTR-decrement branch. // Without this guard, bdnz/bdz would emit a spurious `ge` suffix derived // from the don't-care BI=0 / cond_true=false pair (PPCBUG-640). let cond_str = if uncond { "" } else { cond_name_opt.unwrap_or("") }; let ext_mnem = format!("bd{z}{cond_str}{a}{l}"); let ext_ops = format!("{cr}0x{target:08X}"); with_ext(&base_mnem, base_ops, 8, &ext_mnem, ext_ops, 8) } else if let Some(cond_name) = cond_name_opt { let ext_mnem = format!("b{cond_name}{a}{l}"); let ext_ops = format!("{cr}0x{target:08X}"); with_ext(&base_mnem, base_ops, 8, &ext_mnem, ext_ops, 8) } else { base(&base_mnem, base_ops, 8) } }; with_target(result, target) } fn fmt_bclr(instr: &DecodedInstr) -> DisasmText { let bo = instr.bo(); let bi = instr.bi(); let lk = instr.lk(); let l = if lk { "l" } else { "" }; let base_mnem = format!("bclr{l}"); let base_ops = format!("{bo}, {}", crb(bi)); // BO=20 (binary 10100) sets both "ignore CTR" and "ignore CR" bits, making // the branch unconditional regardless of BI. BI is don't-care by spec, so // the simplified `blr`/`blrl` form applies for any BI value. if bo == 20 { let ext = if lk { "blrl" } else { "blr" }; return with_ext(&base_mnem, base_ops, 8, ext, String::new(), 0); } if let Some((cond, cr)) = cond_branch_ext(bo, bi) { let cr_no_comma = cr.trim_end_matches(", "); let ext_mnem = format!("b{cond}lr{l}"); if cr_no_comma.is_empty() { return with_ext(&base_mnem, base_ops, 8, &ext_mnem, String::new(), 0); } else { return with_ext(&base_mnem, base_ops, 8, &ext_mnem, cr_no_comma.to_string(), 8); } } let decr = bo & 0x04 == 0; let uncond = bo & 0x10 != 0; if decr && uncond { let z = if bo & 0x02 != 0 { "z" } else { "nz" }; let ext_mnem = format!("bd{z}lr{l}"); return with_ext(&base_mnem, base_ops, 8, &ext_mnem, String::new(), 0); } base(&base_mnem, base_ops, 8) } fn fmt_bcctr(instr: &DecodedInstr) -> DisasmText { let bo = instr.bo(); let bi = instr.bi(); let lk = instr.lk(); let l = if lk { "l" } else { "" }; let base_mnem = format!("bcctr{l}"); let base_ops = format!("{bo}, {}", crb(bi)); // BO=20 unconditional pattern: BI is don't-care (see fmt_bclr). if bo == 20 { let ext = if lk { "bctrl" } else { "bctr" }; return with_ext(&base_mnem, base_ops, 8, ext, String::new(), 0); } if let Some((cond, cr)) = cond_branch_ext(bo, bi) { let cr_no_comma = cr.trim_end_matches(", "); let ext_mnem = format!("b{cond}ctr{l}"); if cr_no_comma.is_empty() { return with_ext(&base_mnem, base_ops, 8, &ext_mnem, String::new(), 0); } else { return with_ext(&base_mnem, base_ops, 8, &ext_mnem, cr_no_comma.to_string(), 8); } } base(&base_mnem, base_ops, 8) } // Trap immediate / register fn fmt_trap_imm(instr: &DecodedInstr, mnem: &str, simplified_prefix: &str) -> DisasmText { let to = instr.to(); let ra = instr.ra(); let imm = instr.simm16() as i32; let base_ops = format!("{to}, {}, {imm}", gpr(ra)); if let Some(cond) = trap_cond(to) { if cond.is_empty() { base(mnem, base_ops, 8) } else { let ext_mnem = format!("{simplified_prefix}{cond}i"); let ext_ops = format!("{}, {imm}", gpr(ra)); with_ext(mnem, base_ops, 8, &ext_mnem, ext_ops, 8) } } else { base(mnem, base_ops, 8) } } fn fmt_trap_reg(instr: &DecodedInstr, mnem: &str) -> DisasmText { let to = instr.to(); let ra = instr.ra(); let rb = instr.rb(); let base_ops = format!("{to}, {}, {}", gpr(ra), gpr(rb)); if to == 31 && ra == 0 && rb == 0 { return with_ext(mnem, base_ops, 8, "trap", String::new(), 0); } if let Some(cond) = trap_cond(to) && !cond.is_empty() { let ext_mnem = format!("{mnem}{cond}"); let ext_ops = format!("{}, {}", gpr(ra), gpr(rb)); return with_ext(mnem, base_ops, 8, &ext_mnem, ext_ops, 8); } base(mnem, base_ops, 8) } // D-form ALU fn fmt_addi(instr: &DecodedInstr) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let imm = instr.simm16() as i32; let base_ops = format!("{}, {}, {imm}", gpr(rt), gpr(ra)); if ra == 0 { with_ext("addi", base_ops, 8, "li", format!("{}, {imm}", gpr(rt)), 8) } else if imm < 0 { with_ext("addi", base_ops, 8, "subi", format!("{}, {}, {}", gpr(rt), gpr(ra), -imm), 8) } else { base("addi", base_ops, 8) } } fn fmt_addis(instr: &DecodedInstr) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let imm = instr.simm16() as i32; let imm_u = imm as u16 as u32; let base_ops = format!("{}, {}, 0x{imm_u:X}", gpr(rt), gpr(ra)); if ra == 0 { with_ext("addis", base_ops, 8, "lis", format!("{}, 0x{imm_u:X}", gpr(rt)), 8) } else if imm < 0 { let neg = (-imm) as u16 as u32; with_ext("addis", base_ops, 8, "subis", format!("{}, {}, 0x{neg:X}", gpr(rt), gpr(ra)), 8) } else { base("addis", base_ops, 8) } } fn fmt_d_add(instr: &DecodedInstr, mnem: &str) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let imm = instr.simm16() as i32; let base_ops = format!("{}, {}, {imm}", gpr(rt), gpr(ra)); if imm < 0 { let ext_mnem = mnem.replace("addic", "subic"); with_ext(mnem, base_ops, 8, &ext_mnem, format!("{}, {}, {}", gpr(rt), gpr(ra), -imm), 8) } else { base(mnem, base_ops, 8) } } fn fmt_d_imm_simple(instr: &DecodedInstr, mnem: &str) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let imm = instr.simm16() as i32; base(mnem, format!("{}, {}, {imm}", gpr(rt), gpr(ra)), 8) } fn fmt_cmp_imm(instr: &DecodedInstr, mnem: &str, signed: bool) -> DisasmText { let bf = instr.crfd(); let l_bit = if instr.l() { 1 } else { 0 }; let ra = instr.ra(); let imm_str = if signed { format!("{}", instr.simm16() as i32) } else { format!("0x{:X}", instr.uimm16()) }; let cr = if bf == 0 { String::new() } else { format!("cr{bf}, ") }; let base_ops = format!("{cr}{l_bit}, {}, {imm_str}", gpr(ra)); let size = if l_bit == 0 { "w" } else { "d" }; let ext_mnem = if mnem == "cmpi" { format!("cmp{size}i") } else { format!("cmpl{size}i") }; let ext_ops = format!("{cr}{}, {imm_str}", gpr(ra)); with_ext(mnem, base_ops, 8, &ext_mnem, ext_ops, 8) } fn fmt_cmp_reg(instr: &DecodedInstr, mnem: &str) -> DisasmText { let bf = instr.crfd(); let l_bit = if instr.l() { 1 } else { 0 }; let ra = instr.ra(); let rb = instr.rb(); let cr = if bf == 0 { String::new() } else { format!("cr{bf}, ") }; let base_ops = format!("{cr}{l_bit}, {}, {}", gpr(ra), gpr(rb)); let size = if l_bit == 0 { "w" } else { "d" }; let ext_mnem = format!("{mnem}{size}"); let ext_ops = format!("{cr}{}, {}", gpr(ra), gpr(rb)); with_ext(mnem, base_ops, 8, &ext_mnem, ext_ops, 8) } fn fmt_ori(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let uimm = instr.uimm16() as u32; let base_ops = format!("{}, {}, 0x{uimm:X}", gpr(ra), gpr(rs)); if rs == 0 && ra == 0 && uimm == 0 { with_ext("ori", base_ops, 8, "nop", String::new(), 0) } else { base("ori", base_ops, 8) } } fn fmt_d_logic(instr: &DecodedInstr, mnem: &str) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let uimm = instr.uimm16() as u32; base(mnem, format!("{}, {}, 0x{uimm:X}", gpr(ra), gpr(rs)), 8) } // D-form load/store. `is_fpr` selects between fX and rX for the data register. fn fmt_ld(instr: &DecodedInstr, mnem: &str, is_fpr: bool) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let d = instr.d(); let rn = if is_fpr { fpr(rt) } else { gpr(rt) }; base(mnem, format!("{rn}, {d}({})", gpr(ra)), 8) } fn fmt_st(instr: &DecodedInstr, mnem: &str, is_fpr: bool) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let d = instr.d(); let rn = if is_fpr { fpr(rs) } else { gpr(rs) }; base(mnem, format!("{rn}, {d}({})", gpr(ra)), 8) } // DS-form load/store. fn fmt_ds(instr: &DecodedInstr, mnem: &str) -> DisasmText { let r = instr.rd(); let ra = instr.ra(); let ds = instr.ds(); base(mnem, format!("{}, {ds}({})", gpr(r), gpr(ra)), 8) } // Rotate (32-bit). fn fmt_rlwimi(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let sh = instr.sh(); let mb = instr.mb(); let me = instr.me(); let rc = rc_dot(instr); let mnem = format!("rlwimi{rc}"); let base_ops = format!("{}, {}, {sh}, {mb}, {me}", gpr(ra), gpr(rs)); // inslwi rA, rS, n, b = rlwimi rA, rS, 32-b, b, b+n-1 if mb <= me && sh == (32u32.wrapping_sub(mb)) % 32 && sh != 31u32.wrapping_sub(me) { let n = me - mb + 1; let b = mb; let ext_mnem = format!("inslwi{rc}"); return with_ext(&mnem, base_ops, 8, &ext_mnem, format!("{}, {}, {n}, {b}", gpr(ra), gpr(rs)), 8); } // insrwi rA, rS, n, b = rlwimi rA, rS, 32-(b+n), b, b+n-1 if mb <= me && sh == 31u32.wrapping_sub(me) % 32 { let n = me - mb + 1; let b = mb; let ext_mnem = format!("insrwi{rc}"); return with_ext(&mnem, base_ops, 8, &ext_mnem, format!("{}, {}, {n}, {b}", gpr(ra), gpr(rs)), 8); } base(&mnem, base_ops, 8) } fn fmt_rlwinm(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let sh = instr.sh(); let mb = instr.mb(); let me = instr.me(); let rc = rc_dot(instr); let mnem = format!("rlwinm{rc}"); let base_ops = format!("{}, {}, {sh}, {mb}, {me}", gpr(ra), gpr(rs)); // Priority-ordered simplified forms. if sh > 0 && mb == 0 && me == 31 - sh { let ext = format!("slwi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8); } if sh > 0 && me == 31 && sh + mb == 32 { let ext = format!("srwi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {}", gpr(ra), gpr(rs), 32 - sh), 8); } if sh > 0 && mb == 0 && me == 31 { let ext = format!("rotlwi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8); } if sh == 0 && me == 31 && mb > 0 { let ext = format!("clrlwi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {mb}", gpr(ra), gpr(rs)), 8); } if sh == 0 && mb == 0 && me < 31 { let ext = format!("clrrwi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {}", gpr(ra), gpr(rs), 31 - me), 8); } if mb == 0 && sh > 0 && me < 31 { let n = me + 1; let ext = format!("extlwi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {n}, {sh}", gpr(ra), gpr(rs)), 8); } if me == 31 && mb > 0 && sh > 0 { let n = 32 - mb; let b = sh.wrapping_sub(n) % 32; let ext = format!("extrwi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {n}, {b}", gpr(ra), gpr(rs)), 8); } base(&mnem, base_ops, 8) } fn fmt_rlwnm(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rb = instr.rb(); let mb = instr.mb(); let me = instr.me(); let rc = rc_dot(instr); let mnem = format!("rlwnm{rc}"); let base_ops = format!("{}, {}, {}, {mb}, {me}", gpr(ra), gpr(rs), gpr(rb)); if mb == 0 && me == 31 { let ext = format!("rotlw{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb)), 8); } base(&mnem, base_ops, 8) } // 64-bit MD/MDS-form rotate. fn fmt_rldicl(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rc = rc_dot(instr); let sh = instr.sh64(); let mb = mb_md(instr.raw); let mnem = format!("rldicl{rc}"); let base_ops = format!("{}, {}, {sh}, {mb}", gpr(ra), gpr(rs)); if sh == 0 && mb > 0 { let ext = format!("clrldi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {mb}", gpr(ra), gpr(rs)), 8); } if mb > 0 && sh == (64u32.wrapping_sub(mb)) & 63 { let ext = format!("srdi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {mb}", gpr(ra), gpr(rs)), 8); } if sh > 0 && mb == 0 { let ext = format!("rotldi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8); } base(&mnem, base_ops, 8) } fn fmt_rldicr(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rc = rc_dot(instr); let sh = instr.sh64(); let me = mb_md(instr.raw); let mnem = format!("rldicr{rc}"); let base_ops = format!("{}, {}, {sh}, {me}", gpr(ra), gpr(rs)); if sh > 0 && me == (63u32.wrapping_sub(sh)) & 63 { let ext = format!("sldi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8); } if sh == 0 && me < 63 { let ext = format!("clrrdi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {}", gpr(ra), gpr(rs), 63 - me), 8); } base(&mnem, base_ops, 8) } fn fmt_rldic(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rc = rc_dot(instr); let sh = instr.sh64(); let mb = mb_md(instr.raw); base(&format!("rldic{rc}"), format!("{}, {}, {sh}, {mb}", gpr(ra), gpr(rs)), 8) } fn fmt_rldimi(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rc = rc_dot(instr); let sh = instr.sh64(); let mb = mb_md(instr.raw); let mnem = format!("rldimi{rc}"); let base_ops = format!("{}, {}, {sh}, {mb}", gpr(ra), gpr(rs)); if mb > 0 { let n = (64u32.wrapping_sub(sh).wrapping_sub(mb)) & 63; if n > 0 { let ext = format!("insrdi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {n}, {mb}", gpr(ra), gpr(rs)), 8); } } base(&mnem, base_ops, 8) } fn fmt_rldcl(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rb = instr.rb(); let rc = rc_dot(instr); let mb = mb_md(instr.raw); let mnem = format!("rldcl{rc}"); let base_ops = format!("{}, {}, {}, {mb}", gpr(ra), gpr(rs), gpr(rb)); if mb == 0 { let ext = format!("rotld{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb)), 8); } base(&mnem, base_ops, 8) } fn fmt_rldcr(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rb = instr.rb(); let rc = rc_dot(instr); let me = mb_md(instr.raw); base(&format!("rldcr{rc}"), format!("{}, {}, {}, {me}", gpr(ra), gpr(rs), gpr(rb)), 8) } /// MD/MDS-form mb/me field: 6 bits packed as bits 21-25 + bit 26 (low bit). #[inline] fn mb_md(raw: u32) -> u32 { let lo5 = (raw >> 6) & 0x1F; // bits 21-25 let hi = (raw >> 5) & 0x1; // bit 26 lo5 | (hi << 5) } // XO-form ALU fn fmt_xo_3op(instr: &DecodedInstr, mnem: &str) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let rb = instr.rb(); let rc = rc_dot(instr); let oe = if instr.oe() { "o" } else { "" }; let full = format!("{mnem}{oe}{rc}"); base(&full, format!("{}, {}, {}", gpr(rt), gpr(ra), gpr(rb)), 8) } fn fmt_xo_2op(instr: &DecodedInstr, mnem: &str) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let rc = rc_dot(instr); let oe = if instr.oe() { "o" } else { "" }; let full = format!("{mnem}{oe}{rc}"); base(&full, format!("{}, {}", gpr(rt), gpr(ra)), 8) } fn fmt_xo_3op_no_oe(instr: &DecodedInstr, mnem: &str) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let rb = instr.rb(); let rc = rc_dot(instr); let oe = if instr.oe() { "o" } else { "" }; let full = format!("{mnem}{oe}{rc}"); base(&full, format!("{}, {}, {}", gpr(rt), gpr(ra), gpr(rb)), 8) } fn fmt_xo_3op_rc_only(instr: &DecodedInstr, mnem: &str) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let rb = instr.rb(); let rc = rc_dot(instr); let full = format!("{mnem}{rc}"); base(&full, format!("{}, {}, {}", gpr(rt), gpr(ra), gpr(rb)), 8) } fn fmt_subf(instr: &DecodedInstr, base_mnem: &str, ext_mnem: &str) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let rb = instr.rb(); let rc = rc_dot(instr); let oe = if instr.oe() { "o" } else { "" }; let bm = format!("{base_mnem}{oe}{rc}"); let em = format!("{ext_mnem}{oe}{rc}"); let bo = format!("{}, {}, {}", gpr(rt), gpr(ra), gpr(rb)); let eo = format!("{}, {}, {}", gpr(rt), gpr(rb), gpr(ra)); with_ext(&bm, bo, 8, &em, eo, 8) } // X-form logical fn fmt_x_logic(instr: &DecodedInstr, mnem: &str) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rb = instr.rb(); let rc = rc_dot(instr); let full = format!("{mnem}{rc}"); base(&full, format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb)), 8) } fn fmt_x_unary_rc(instr: &DecodedInstr, mnem: &str) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rc = rc_dot(instr); let full = format!("{mnem}{rc}"); base(&full, format!("{}, {}", gpr(ra), gpr(rs)), 8) } fn fmt_logic_and(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rb = instr.rb(); let rc = rc_dot(instr); let bm = format!("and{rc}"); let bo = format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb)); if rs == rb { let em = format!("mr{rc}"); with_ext(&bm, bo, 8, &em, format!("{}, {}", gpr(ra), gpr(rs)), 8) } else { base(&bm, bo, 8) } } fn fmt_logic_or(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rb = instr.rb(); let rc = rc_dot(instr); let bm = format!("or{rc}"); let bo = format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb)); if rs == rb { let em = format!("mr{rc}"); with_ext(&bm, bo, 8, &em, format!("{}, {}", gpr(ra), gpr(rs)), 8) } else { base(&bm, bo, 8) } } fn fmt_logic_nor(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rb = instr.rb(); let rc = rc_dot(instr); let bm = format!("nor{rc}"); let bo = format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb)); if rs == rb { let em = format!("not{rc}"); with_ext(&bm, bo, 8, &em, format!("{}, {}", gpr(ra), gpr(rs)), 8) } else { base(&bm, bo, 8) } } // Shift immediate fn fmt_srawi(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let sh = instr.sh(); let rc = rc_dot(instr); base(&format!("srawi{rc}"), format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8) } fn fmt_sradi(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let sh = instr.sh64(); let rc = rc_dot(instr); base(&format!("sradi{rc}"), format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8) } // Special-purpose register moves fn fmt_mfspr(instr: &DecodedInstr) -> DisasmText { let rd = instr.rd(); let spr = instr.spr(); let base_ops = format!("{}, {}", gpr(rd), spr_name(spr)); let ext = match spr { 8 => Some(("mflr", format!("{}", gpr(rd)))), 9 => Some(("mfctr", format!("{}", gpr(rd)))), 1 => Some(("mfxer", format!("{}", gpr(rd)))), _ => None, }; match ext { Some((em, eo)) => with_ext("mfspr", base_ops, 8, em, eo, 8), None => base("mfspr", base_ops, 8), } } fn fmt_mtspr(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let spr = instr.spr(); let base_ops = format!("{}, {}", spr_name(spr), gpr(rs)); let ext = match spr { 8 => Some(("mtlr", format!("{}", gpr(rs)))), 9 => Some(("mtctr", format!("{}", gpr(rs)))), 1 => Some(("mtxer", format!("{}", gpr(rs)))), _ => None, }; match ext { Some((em, eo)) => with_ext("mtspr", base_ops, 8, em, eo, 8), None => base("mtspr", base_ops, 8), } } fn fmt_mtcrf(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let fxm = (instr.raw >> 12) & 0xFF; let bo = format!("0x{fxm:02X}, {}", gpr(rs)); if fxm == 0xFF { with_ext("mtcrf", bo, 8, "mtcr", gpr(rs), 8) } else { base("mtcrf", bo, 8) } } fn fmt_mftb(instr: &DecodedInstr) -> DisasmText { let rd = instr.rd(); let tbr = instr.spr(); let base_ops = format!("{}, {tbr}", gpr(rd)); match tbr { 268 => with_ext("mftb", base_ops, 8, "mftb", gpr(rd), 8), 269 => with_ext("mftb", base_ops, 8, "mftbu", gpr(rd), 8), _ => base("mftb", base_ops, 8), } } // X-form indexed load/store. fn fmt_x_load(instr: &DecodedInstr, mnem: &str, is_fpr: bool) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let rb = instr.rb(); let rn = if is_fpr { fpr(rt) } else { gpr(rt) }; base(mnem, format!("{rn}, {}, {}", gpr(ra), gpr(rb)), 8) } fn fmt_x_store(instr: &DecodedInstr, mnem: &str, is_fpr: bool) -> DisasmText { let rs = instr.rs(); let ra = instr.ra(); let rb = instr.rb(); let rn = if is_fpr { fpr(rs) } else { gpr(rs) }; base(mnem, format!("{rn}, {}, {}", gpr(ra), gpr(rb)), 8) } fn fmt_lswi_stswi(instr: &DecodedInstr, mnem: &str) -> DisasmText { let rt = instr.rd(); let ra = instr.ra(); let nb = instr.nb(); base(mnem, format!("{}, {}, {nb}", gpr(rt), gpr(ra)), 8) } fn fmt_cache(instr: &DecodedInstr, mnem: &str) -> DisasmText { let ra = instr.ra(); let rb = instr.rb(); base(mnem, format!("{}, {}", gpr(ra), gpr(rb)), 8) } // CR logical fn fmt_cr_logic(instr: &DecodedInstr, mnem: &str) -> DisasmText { let bt = instr.crbd(); let ba = instr.crba(); let bb = instr.crbb(); base(mnem, format!("{}, {}, {}", crb(bt), crb(ba), crb(bb)), 8) } fn fmt_crnor(instr: &DecodedInstr) -> DisasmText { let bt = instr.crbd(); let ba = instr.crba(); let bb = instr.crbb(); let bo = format!("{}, {}, {}", crb(bt), crb(ba), crb(bb)); if ba == bb { with_ext("crnor", bo, 8, "crnot", format!("{}, {}", crb(bt), crb(ba)), 8) } else { base("crnor", bo, 8) } } fn fmt_crxor(instr: &DecodedInstr) -> DisasmText { let bt = instr.crbd(); let ba = instr.crba(); let bb = instr.crbb(); let bo = format!("{}, {}, {}", crb(bt), crb(ba), crb(bb)); if bt == ba && ba == bb { with_ext("crxor", bo, 8, "crclr", crb(bt), 8) } else { base("crxor", bo, 8) } } fn fmt_creqv(instr: &DecodedInstr) -> DisasmText { let bt = instr.crbd(); let ba = instr.crba(); let bb = instr.crbb(); let bo = format!("{}, {}, {}", crb(bt), crb(ba), crb(bb)); if bt == ba && ba == bb { with_ext("creqv", bo, 8, "crset", crb(bt), 8) } else { base("creqv", bo, 8) } } fn fmt_cror(instr: &DecodedInstr) -> DisasmText { let bt = instr.crbd(); let ba = instr.crba(); let bb = instr.crbb(); let bo = format!("{}, {}, {}", crb(bt), crb(ba), crb(bb)); if ba == bb { with_ext("cror", bo, 8, "crmove", format!("{}, {}", crb(bt), crb(ba)), 8) } else { base("cror", bo, 8) } } // FPU fn fmt_a_3op(instr: &DecodedInstr, mnem: &str, use_frc: bool) -> DisasmText { let frt = instr.rd(); let fra = instr.ra(); let frb = instr.rb(); let frc = instr.rc(); let rc = rc_dot(instr); let full = format!("{mnem}{rc}"); let ops = if use_frc { format!("{}, {}, {}", fpr(frt), fpr(fra), fpr(frc)) } else { format!("{}, {}, {}", fpr(frt), fpr(fra), fpr(frb)) }; base(&full, ops, 8) } fn fmt_a_unary(instr: &DecodedInstr, mnem: &str) -> DisasmText { let frt = instr.rd(); let frb = instr.rb(); let rc = rc_dot(instr); base(&format!("{mnem}{rc}"), format!("{}, {}", fpr(frt), fpr(frb)), 8) } fn fmt_a_4op(instr: &DecodedInstr, mnem: &str) -> DisasmText { let frt = instr.rd(); let fra = instr.ra(); let frb = instr.rb(); let frc = instr.rc(); let rc = rc_dot(instr); base(&format!("{mnem}{rc}"), format!("{}, {}, {}, {}", fpr(frt), fpr(fra), fpr(frc), fpr(frb)), 8) } fn fmt_fcmp(instr: &DecodedInstr, mnem: &str) -> DisasmText { let bf = instr.crfd(); let fra = instr.ra(); let frb = instr.rb(); base(mnem, format!("cr{bf}, {}, {}", fpr(fra), fpr(frb)), 8) } fn fmt_x_fpu_unary(instr: &DecodedInstr, mnem: &str) -> DisasmText { let frt = instr.rd(); let frb = instr.rb(); let rc = rc_dot(instr); base(&format!("{mnem}{rc}"), format!("{}, {}", fpr(frt), fpr(frb)), 8) } fn fmt_mtfsb(instr: &DecodedInstr, mnem: &str) -> DisasmText { let bt = instr.crbd(); let rc = rc_dot(instr); base(&format!("{mnem}{rc}"), format!("{bt}"), 8) } // VMX (5-bit registers). fn fmt_vmx_3op(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.rd(); let va = instr.ra(); let vb = instr.rb(); base(mnem, format!("{}, {}, {}", vr(vd), vr(va), vr(vb)), 8) } fn fmt_vmx_unary(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.rd(); let vb = instr.rb(); base(mnem, format!("{}, {}", vr(vd), vr(vb)), 8) } fn fmt_vmx_uimm(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.rd(); let vb = instr.rb(); let uimm = instr.ra() as u32; base(mnem, format!("{}, {}, {uimm}", vr(vd), vr(vb)), 8) } fn fmt_vmx_simm(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.rd(); let simm = sign_ext(instr.ra() as u32, 5); base(mnem, format!("{}, {simm}", vr(vd)), 9) } fn fmt_vmx_cmp(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.rd(); let va = instr.ra(); let vb = instr.rb(); // Rc bit at position 22 (0-indexed from MSB) let rc = if (instr.raw >> 10) & 1 != 0 { "." } else { "" }; let full = format!("{mnem}{rc}"); base(&full, format!("{}, {}, {}", vr(vd), vr(va), vr(vb)), 12) } fn fmt_vmx_4op(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.rd(); let va = instr.ra(); let vb = instr.rb(); let vc = instr.rc(); base(mnem, format!("{}, {}, {}, {}", vr(vd), vr(va), vr(vb), vr(vc)), 12) } fn fmt_vmx_4op_swap(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.rd(); let va = instr.ra(); let vb = instr.rb(); let vc = instr.rc(); base(mnem, format!("{}, {}, {}, {}", vr(vd), vr(va), vr(vc), vr(vb)), 9) } fn fmt_vsldoi(instr: &DecodedInstr) -> DisasmText { let vd = instr.rd(); let va = instr.ra(); let vb = instr.rb(); let sh = (instr.raw >> 6) & 0xF; base("vsldoi", format!("{}, {}, {}, {sh}", vr(vd), vr(va), vr(vb)), 8) } fn fmt_vmx_ls(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.rd(); let ra = instr.ra(); let rb = instr.rb(); base(mnem, format!("{}, {}, {}", vr(vd), gpr(ra), gpr(rb)), 8) } // VMX128 — uses canonical va128/vb128/vd128 accessors from decoder.rs. // (Silently fixes the prior ppc.rs bug where these used wrong bit positions.) fn fmt_vmx128_ls(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.vd128(); let ra = instr.ra(); let rb = instr.rb(); base(mnem, format!("{}, {}, {}", vr(vd), gpr(ra), gpr(rb)), 12) } fn fmt_vmx128_3op(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.vd128(); let va = instr.va128(); let vb = instr.vb128(); base(mnem, format!("{}, {}, {}", vr(vd), vr(va), vr(vb)), 12) } // VMX128 multiply-add forms (VX128_2): the addend is the VD register // re-used, not a separate VC field. Operand order differs between // `vmaddfp128` (VD, VA, VB, VD) and the `vmaddcfp128`/`vnmsubfp128` // pair (VD, VA, VD, VB), per canary's authoritative formatters in // xenia-canary/src/xenia/cpu/ppc/ppc_opcode_disasm_gen.cc. fn fmt_vmaddfp128(instr: &DecodedInstr) -> DisasmText { let vd = instr.vd128(); let va = instr.va128(); let vb = instr.vb128(); base( "vmaddfp128", format!("{}, {}, {}, {}", vr(vd), vr(va), vr(vb), vr(vd)), 12, ) } fn fmt_vmx128_madd_vd_vb(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.vd128(); let va = instr.va128(); let vb = instr.vb128(); base( mnem, format!("{}, {}, {}, {}", vr(vd), vr(va), vr(vd), vr(vb)), 12, ) } fn fmt_vperm128(instr: &DecodedInstr) -> DisasmText { let vd = instr.vd128(); let va = instr.va128(); let vb = instr.vb128(); let vc = (instr.raw >> 6) & 0x7; base("vperm128", format!("{}, {}, {}, {vc}", vr(vd), vr(va), vr(vb)), 9) } fn fmt_vsldoi128(instr: &DecodedInstr) -> DisasmText { let vd = instr.vd128(); let va = instr.va128(); let vb = instr.vb128(); let sh = (instr.raw >> 6) & 0xF; base("vsldoi128", format!("{}, {}, {}, {sh}", vr(vd), vr(va), vr(vb)), 10) } fn fmt_vpermwi128(instr: &DecodedInstr) -> DisasmText { let vd = instr.vd128(); let vb = instr.vb128(); // UIMM combines bits 11-15 (low 5) with bits 23-25 (upper 3). let lo = (instr.raw >> 16) & 0x1F; let hi = (instr.raw >> 6) & 0x7; let uimm = lo | (hi << 5); base("vpermwi128", format!("{}, {}, 0x{uimm:X}", vr(vd), vr(vb)), 11) } fn fmt_vmx128_pack_d3d(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.vd128(); let vb = instr.vb128(); let imm = (instr.raw >> 16) & 0x1F; let z = (instr.raw >> 6) & 0x3; base(mnem, format!("{}, {}, {imm}, {z}", vr(vd), vr(vb)), 10) } fn fmt_vmx128_unary(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.vd128(); let vb = instr.vb128(); base(mnem, format!("{}, {}", vr(vd), vr(vb)), 12) } fn fmt_vmx128_uimm(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.vd128(); let vb = instr.vb128(); let uimm = extract_vx128_uimm5(instr.raw); base(mnem, format!("{}, {}, {uimm}", vr(vd), vr(vb)), 12) } fn fmt_vmx128_cmp(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.vd128(); let va = instr.va128(); let vb = instr.vb128(); // Rc bit at position 25 in VMX128 cmp form. let rc = if (instr.raw >> 6) & 1 != 0 { "." } else { "" }; let full = format!("{mnem}{rc}"); base(&full, format!("{}, {}, {}", vr(vd), vr(va), vr(vb)), 14) } #[cfg(test)] mod tests { use super::*; use crate::decoder::decode; #[test] fn nop_collapses_via_extended() { let instr = decode(0x60000000, 0); let t = format(&instr); assert_eq!(t.mnemonic, "ori"); assert_eq!(t.ext_mnemonic.as_deref(), Some("nop")); assert_eq!(t.display(), "nop"); } #[test] fn addi_to_li_when_ra_zero() { // addi r3, r0, 16 let raw = (14u32 << 26) | (3 << 21) | (0 << 16) | 16; let instr = decode(raw, 0); let t = format(&instr); assert_eq!(t.mnemonic, "addi"); assert_eq!(t.ext_mnemonic.as_deref(), Some("li")); assert_eq!(t.ext_operands.as_deref(), Some("r3, 16")); } #[test] fn rlwinm_dot_preserves_record_bit() { // Same pattern as the Sylpheed graphics-callback test: // rlwinm. r11, r11, 0, 31, 31 with Rc=1 let raw = (21u32 << 26) | (11 << 21) | (11 << 16) | (0 << 11) | (31 << 6) | (31 << 1) | 1; let instr = decode(raw, 0); let t = format(&instr); assert!(t.disasm.starts_with("rlwinm."), "got: {}", t.disasm); } #[test] fn rlwinm_no_dot_when_rc_unset() { let raw = (21u32 << 26) | (11 << 21) | (11 << 16) | (0 << 11) | (31 << 6) | (31 << 1); let instr = decode(raw, 0); let t = format(&instr); assert_eq!(t.mnemonic, "rlwinm"); assert!(!t.mnemonic.ends_with('.')); } #[test] fn or_with_same_source_is_mr() { // or r3, r4, r4 → mr r3, r4 let raw = (31u32 << 26) | (4 << 21) | (3 << 16) | (4 << 11) | (444 << 1); let instr = decode(raw, 0); let t = format(&instr); assert_eq!(t.ext_mnemonic.as_deref(), Some("mr")); assert_eq!(t.ext_operands.as_deref(), Some("r3, r4")); } #[test] fn unconditional_branch_resolves_target() { // b +0x100 with addr=0x82000000 let raw = (18u32 << 26) | (0x40 << 2); let instr = decode(raw, 0x82000000); let t = format(&instr); assert_eq!(t.mnemonic, "b"); assert_eq!(t.branch_target, Some(0x82000100)); assert_eq!(t.operands, "0x82000100"); } #[test] fn bclr_unconditional_is_blr() { // bclr 20, 0 let raw = (19u32 << 26) | (20 << 21) | (0 << 16) | (16 << 1); let instr = decode(raw, 0); let t = format(&instr); assert_eq!(t.ext_mnemonic.as_deref(), Some("blr")); } #[test] fn back_compat_disassemble_returns_display() { let instr = decode(0x60000000, 0); assert_eq!(disassemble(&instr), "nop"); } #[test] fn iter_disasm_walks_byte_slice_in_order() { // Three instructions at 0x82000000: nop, addi r3,r0,16, b +0x100. let mut bytes = Vec::new(); bytes.extend_from_slice(&0x60000000u32.to_be_bytes()); // nop bytes.extend_from_slice(&((14u32 << 26) | (3 << 21) | (0 << 16) | 16).to_be_bytes()); // addi bytes.extend_from_slice(&((18u32 << 26) | (0x40 << 2)).to_be_bytes()); // b +0x100 let items: Vec<_> = super::iter_disasm(&bytes, 0x82000000, 0x82000000, 0x82000000 + 12) .collect(); assert_eq!(items.len(), 3); assert_eq!(items[0].addr, 0x82000000); assert_eq!(items[0].text.ext_mnemonic.as_deref(), Some("nop")); assert_eq!(items[1].addr, 0x82000004); assert_eq!(items[1].text.ext_mnemonic.as_deref(), Some("li")); assert_eq!(items[2].addr, 0x82000008); assert_eq!(items[2].text.branch_target, Some(0x82000108)); } #[test] fn iter_disasm_stops_on_truncated_tail() { // 6 bytes — one full instruction + 2 dangling. Iterator must yield exactly 1. let mut bytes = Vec::new(); bytes.extend_from_slice(&0x60000000u32.to_be_bytes()); bytes.push(0x60); bytes.push(0x00); let items: Vec<_> = super::iter_disasm(&bytes, 0, 0, 6).collect(); assert_eq!(items.len(), 1); } }