//! 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"),
        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<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)
}

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);
    }
}
