Files
Sylpheed/crates/sylpheed-ppc/src/disasm.rs
MechaCat02 9c48c340bd fix(xexdb): clear the lint gate on the imported crates
rustfmt, then clippy -D warnings across the three new crates. Mechanical,
except three decisions that are stated rather than silently allowed:

  * lzx.rs gets file-scoped needless_range_loop/explicit_counter_loop allows.
    Index arithmetic IS the algorithm -- LZX is defined over symbol indices,
    Huffman slots and window positions, and a decompressor that is merely
    idiomatic is worth nothing if it is not bit-exact.
  * sylpheed-xexdb gets crate-scoped allows for needless_range_loop (nine
    sites index reg[r] where r is the PowerPC register number -- the index is
    the meaning), too_many_arguments and type_complexity. This code arrived
    whole from a retired repository; a refactor here would be an unreviewed
    edit dressed as a lint fix.
  * Everything else clippy asked for is FIXED, including all 14 doc-indent
    sites, the let-else, and a Prepared type alias in the binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:25:44 +02:00

2599 lines
91 KiB
Rust

//! 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<String>,
pub ext_operands: Option<String>,
pub ext_disasm: Option<String>,
pub branch_target: Option<u32>,
}
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!("{:<width$}{}", mnem, operands, width = width)
}
}
fn base(mnem: &str, operands: String, pad: usize) -> 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"),
3 => Some("lne"),
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 => fmt_mfcr(instr),
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
// `vor vD,vA,vA` is the canonical vector register move, and
// `vnor vD,vA,vA` the canonical vector complement. Both are extremely
// common (1,535 and 9 sites here) and both read as noise in base form.
PpcOpcode::vor if instr.ra() == instr.rb() => fmt_vmx_move(instr, "vor", "vmr"),
PpcOpcode::vnor if instr.ra() == instr.rb() => fmt_vmx_move(instr, "vnor", "vnot"),
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 [`sylpheed_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<Item = DisasmItem> + '_ {
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<DisasmItem> {
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)
}
/// Static branch-prediction hint suffix for a `BO` field.
///
/// PowerISA gives several `BO` encodings an `at` pair — `001at`, `011at`,
/// `1a00t`, `1a01t` — where `at=0b10` means "unlikely" (`-`) and `0b11` means
/// "likely" (`+`); `0b00` is "no hint" and `0b01` is reserved. The forms whose
/// low bit is the reserved `z` (`0000z`, `0001z`, `0100z`, `0101z`) carry no
/// hint at all. Dropping the suffix loses the compiler's static prediction,
/// which is the only place it is recorded.
fn hint_suffix(bo: u32) -> &'static str {
let b = |i: u32| (bo >> (4 - i)) & 1; // b(0) is the MSB of the 5-bit field
let at = match (b(0), b(2), b(3)) {
// 1z1zz — branch always, no hint.
(1, 1, _) => return "",
// 1a00t / 1a01t — the `a` bit is b1.
(1, 0, _) => (b(1) << 1) | b(4),
// 001at / 011at — the `a` bit is b3.
(0, 1, _) => (b(3) << 1) | b(4),
// 0000z / 0001z / 0100z / 0101z — low bit reserved, no hint.
_ => return "",
};
match at {
0b10 => "-",
0b11 => "+",
_ => "",
}
}
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 hint = hint_suffix(bo);
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" };
if uncond {
// BO bit 4 set means CR is ignored — a 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 ext_mnem = format!("bd{z}{a}{l}{hint}");
let ext_ops = format!("0x{target:08X}");
with_ext(&base_mnem, base_ops, 8, &ext_mnem, ext_ops, 8)
} else {
// Combined CTR + condition. PowerISA names these `bdnzt` /
// `bdnzf` / `bdzt` / `bdzf` with the CR bit as an operand —
// not a condition-suffixed `bdnzne`, which is an invention no
// assembler accepts.
let t = if cond_true { "t" } else { "f" };
let ext_mnem = format!("bd{z}{t}{a}{l}{hint}");
let ext_ops = format!("{}, 0x{target:08X}", crb(bi));
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}{hint}");
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));
let hint = hint_suffix(bo);
// 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}{hint}");
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}{hint}");
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));
let hint = hint_suffix(bo);
// 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}{hint}");
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() {
// TO=31 traps unconditionally. The register form has `trap`; the
// immediate form's counterpart is `twui`/`tdui` (binutils), which
// is what every other disassembler prints for these 16 sites.
let ext_mnem = format!("{simplified_prefix}ui");
let ext_ops = format!("{}, {imm}", gpr(ra));
with_ext(mnem, base_ops, 8, &ext_mnem, ext_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.
//
// `slwi` is deliberately not gated on `sh > 0`: `rlwinm rA,rS,0,0,31` is a
// rotate-by-zero under a full mask, which the ISA's table still names
// `slwi rA,rS,0` (and which LLVM/capstone print that way). It is the single
// most common `rlwinm` encoding in this binary — 3,720 sites — so gating it
// away left the largest simplified-mnemonic gap we had.
if 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", gpr(rd).to_string())),
9 => Some(("mfctr", gpr(rd).to_string())),
1 => Some(("mfxer", gpr(rd).to_string())),
_ => 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", gpr(rs).to_string())),
9 => Some(("mtctr", gpr(rs).to_string())),
1 => Some(("mtxer", gpr(rs).to_string())),
_ => None,
};
match ext {
Some((em, eo)) => with_ext("mtspr", base_ops, 8, em, eo, 8),
None => base("mtspr", base_ops, 8),
}
}
/// `mfcr` and `mfocrf` share XO=19 and are told apart by bit 11.
///
/// With bit 11 clear the instruction copies the whole CR into `rD`; with it set
/// this is `mfocrf`, which copies only the single CR field named by `FXM` and
/// leaves the rest of `rD` undefined. Printing the latter as a bare `mfcr rD`
/// loses which field was read — and on this title 163 of 165 sites are the
/// one-field form. (The reference emulator folds both into one handler because
/// the wide read is a safe superset at runtime; a disassembler cannot.)
fn fmt_mfcr(instr: &DecodedInstr) -> DisasmText {
let rd = instr.rd();
if instr.raw & (1 << 20) != 0 {
let fxm = (instr.raw >> 12) & 0xFF;
base("mfocrf", format!("{}, 0x{fxm:02X}", gpr(rd)), 8)
} else {
base("mfcr", gpr(rd), 8)
}
}
/// `mtcrf` and `mtocrf` share XO=144, told apart by bit 11 exactly as
/// [`fmt_mfcr`] describes.
fn fmt_mtcrf(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let fxm = (instr.raw >> 12) & 0xFF;
if instr.raw & (1 << 20) != 0 {
return base("mtocrf", format!("0x{fxm:02X}, {}", gpr(rs)), 8);
}
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).
/// A VX-form op whose two sources are the same register, so it degenerates to
/// a move/complement: emit the base form plus the two-operand simplified one.
fn fmt_vmx_move(instr: &DecodedInstr, base_mnem: &str, ext_mnem: &str) -> DisasmText {
let vd = instr.rd();
let va = instr.ra();
let vb = instr.rb();
with_ext(
base_mnem,
format!("{}, {}, {}", vr(vd), vr(va), vr(vb)),
8,
ext_mnem,
format!("{}, {}", vr(vd), vr(va)),
8,
)
}
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)) | 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)) | (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)) | (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)) | (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)) | 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);
}
/// `mfocrf` shares XO=19 with `mfcr`, differing only in bit 11. Printing it
/// as `mfcr` drops the FXM field naming which CR field is actually read —
/// 163 of 165 sites in the reference title are this form.
#[test]
fn mfocrf_is_distinguished_from_mfcr() {
// 0x7d502026: mfocrf r10, 0x02 (bit 11 set)
let d = crate::decoder::decode(0x7d50_2026, 0x8200_0000);
let t = format(&d);
assert_eq!(t.mnemonic, "mfocrf");
assert_eq!(t.operands, "r10, 0x02");
// Same encoding with bit 11 clear is a plain whole-CR read.
let d = crate::decoder::decode(0x7d50_2026 & !(1 << 20), 0x8200_0000);
let t = format(&d);
assert_eq!(t.mnemonic, "mfcr");
assert_eq!(t.operands, "r10");
}
/// The mirror case on the write side.
#[test]
fn mtocrf_is_distinguished_from_mtcrf() {
let base_word = 0x7c10_1120u32; // mtcrf-form, XO=144
let d = crate::decoder::decode(base_word | (1 << 20), 0x8200_0000);
assert_eq!(format(&d).mnemonic, "mtocrf");
let d = crate::decoder::decode(base_word & !(1 << 20), 0x8200_0000);
assert_eq!(format(&d).mnemonic, "mtcrf");
}
/// `rlwinm rA,rS,0,0,31` is a rotate-by-zero under a full mask. It is the
/// most common `rlwinm` encoding in the reference title (3,720 sites) and
/// was falling through to the base form because every simplified branch was
/// gated on `sh > 0`.
#[test]
fn rlwinm_shift_zero_still_simplifies() {
// 0x5548003e: rlwinm r8, r10, 0, 0, 31
let d = crate::decoder::decode(0x5548_003e, 0x8200_0000);
let t = format(&d);
assert_eq!(t.mnemonic, "rlwinm");
assert_eq!(t.ext_mnemonic.as_deref(), Some("slwi"));
assert_eq!(t.ext_operands.as_deref(), Some("r8, r10, 0"));
// The record-bit form keeps its dot.
let d = crate::decoder::decode(0x5569_003f, 0x8200_0000);
assert_eq!(format(&d).ext_mnemonic.as_deref(), Some("slwi."));
}
/// A genuine bit-extraction has no short name and must stay in base form.
#[test]
fn rlwinm_bit_extract_has_no_simplified_form() {
// rlwinm rA,rS,0,30,30 — extracts one bit; not a shift or clear.
let word = 0x5548_0000 | (30 << 6) | (30 << 1);
let t = format(&crate::decoder::decode(word, 0x8200_0000));
assert_eq!(t.mnemonic, "rlwinm");
assert_eq!(t.ext_mnemonic, None);
}
/// `vor vD,vA,vA` is the vector register move; `vnor vD,vA,vA` the vector
/// complement. Both only apply when the two sources are the same register.
#[test]
fn vor_and_vnor_simplify_only_when_sources_match() {
// 0x11800484: vor v12, v0, v0
let t = format(&crate::decoder::decode(0x1180_0484, 0x8200_0000));
assert_eq!(t.mnemonic, "vor");
assert_eq!(t.ext_mnemonic.as_deref(), Some("vmr"));
assert_eq!(t.ext_operands.as_deref(), Some("v12, v0"));
// 0x10000504: vnor v0, v0, v0
let t = format(&crate::decoder::decode(0x1000_0504, 0x8200_0000));
assert_eq!(t.ext_mnemonic.as_deref(), Some("vnot"));
// Distinct sources: a real bitwise OR, no simplification.
let t = format(&crate::decoder::decode(0x1180_1484, 0x8200_0000));
assert_eq!(t.mnemonic, "vor");
assert_eq!(t.ext_mnemonic, None);
}
/// PowerISA names the combined CTR+condition branches `bdnzt`/`bdnzf`
/// (and `bdzt`/`bdzf`) with the CR bit as an operand. We used to synthesise
/// `bdnzne` by gluing on a condition suffix — readable, but not a mnemonic
/// any assembler accepts.
#[test]
fn bdnz_with_condition_uses_the_isa_t_f_form() {
// 0x4002fff8: BO=00000 (dec CTR, branch if CTR!=0 and CR[BI]==0), BI=eq
let t = format(&crate::decoder::decode(0x4002_fff8, 0x8200_0000));
assert_eq!(t.ext_mnemonic.as_deref(), Some("bdnzf"));
assert!(t.ext_operands.as_deref().unwrap().starts_with("eq,"));
}
/// The `at` hint bits are the only record of the compiler's static branch
/// prediction, so they must survive into the text.
#[test]
fn branch_prediction_hints_are_preserved() {
// 0x4320fff0: bdnz with at=0b11 -> "+"
assert_eq!(
format(&crate::decoder::decode(0x4320_fff0, 0x8200_0000))
.ext_mnemonic
.as_deref(),
Some("bdnz+")
);
// 0x41c20024: beq with at=0b10 -> "-"
assert_eq!(
format(&crate::decoder::decode(0x41c2_0024, 0x8200_0000))
.ext_mnemonic
.as_deref(),
Some("beq-")
);
// 0x4de20020: beqlr with at=0b11 -> "+"
assert_eq!(
format(&crate::decoder::decode(0x4de2_0020, 0x8200_0000))
.ext_mnemonic
.as_deref(),
Some("beqlr+")
);
}
/// A branch with no hint bits set must stay unsuffixed, and `blr` (BO=20,
/// the branch-always form) never takes a hint at all.
#[test]
fn unhinted_branches_gain_no_suffix() {
// 0x4182000c: beq, at=0b00
let t = format(&crate::decoder::decode(0x4182_000c, 0x8200_0000));
assert_eq!(t.ext_mnemonic.as_deref(), Some("beq"));
// 0x4e800020: blr
let t = format(&crate::decoder::decode(0x4e80_0020, 0x8200_0000));
assert_eq!(t.ext_mnemonic.as_deref(), Some("blr"));
}
/// The whole trap family, checked against the reference table: TO=31 is
/// unconditional, which the register form calls `trap` and the immediate
/// form `twui` — the latter was the one gap.
#[test]
fn trap_extended_mnemonics_cover_the_table() {
// 0x0fe00016: twi 31, r0, 22 -> unconditional
let t = format(&crate::decoder::decode(0x0fe0_0016, 0x8200_0000));
assert_eq!(t.mnemonic, "twi");
assert_eq!(t.ext_mnemonic.as_deref(), Some("twui"));
// TO=6 is "logically less than or equal" — the divide-by-zero guard
// MSVC emits, and the most common trap in the reference title.
let word = 0x0c00_0000 | (6 << 21) | (3 << 16);
assert_eq!(
format(&crate::decoder::decode(word, 0x8200_0000))
.ext_mnemonic
.as_deref(),
Some("twllei")
);
// TO=5 is "logically greater than or equal".
let word = 0x0c00_0000 | (5 << 21) | (3 << 16);
assert_eq!(
format(&crate::decoder::decode(word, 0x8200_0000))
.ext_mnemonic
.as_deref(),
Some("twlgei")
);
// tw 31,0,0 stays the register-form `trap`.
let t = format(&crate::decoder::decode(0x7fe0_0008, 0x8200_0000));
assert_eq!(t.ext_mnemonic.as_deref(), Some("trap"));
}
}