disasm: ISA-conformance fixes found by differential review
Reviewed the PPC disassembler against two independent oracles: xenia-canary's
authoritative encoding table, and capstone 5.0.7 (PPC64 big-endian) run over
all 1,859,397 instructions in the reference title.
Decoding was already sound — our tables reproduce all 455 entries of canary's
`ppc_opcode_table_gen.cc` exactly, and every word we render as `.long` is
genuinely not an instruction (9,332 zero padding + 388 with reserved primary
opcode 0). The defects were all in the text layer:
- `mfocrf`/`mtocrf` were printed as `mfcr`/`mtcrf`. They share XO with the
wide forms and differ only in bit 11; the one-field form also carries an FXM
operand naming which CR field is touched. 163 of 165 `mfcr` sites in this
title are really `mfocrf`, so the disassembly was dropping that operand
entirely. (Canary folds both into one handler because the wide read is a safe
superset at runtime — a disassembler cannot.)
- `rlwinm rA,rS,0,0,31` produced no simplified form: every branch in
`fmt_rlwinm` was gated on `sh > 0`, but rotate-by-zero under a full mask is
still `slwi rA,rS,0`. It is the single most common `rlwinm` encoding here —
3,720 sites.
- `vor vD,vA,vA` and `vnor vD,vA,vA` are the vector move and complement
(`vmr`/`vnot`); 1,544 sites were left in base form. The guarded arms have to
precede the catch-all VX group or they are unreachable.
- Combined CTR+condition branches emitted `bdnzne`, a mnemonic assembled by
nothing. PowerISA names these `bdnzt`/`bdnzf`/`bdzt`/`bdzf` with the CR bit
as an operand.
- Static branch-prediction hints were discarded. The `at` bits in `BO`
(`001at`, `011at`, `1a00t`, `1a01t`) are the only record of the compiler's
prediction, and are now rendered as the ISA's `+`/`-` suffix.
- `twi`/`tdi` with TO=31 had no simplified form; the register form already had
`trap`, the immediate form's counterpart is `twui`/`tdui`. Also adds the
missing TO=3 (`lne`) row.
Capstone disagreements fall from 5,596 to 135, and every survivor is ours
being right for the target or a free choice of name:
- `dcbz128` vs `dcbzl` (55) — same encoding, Xenon name vs POWER name
- `lvx128`, `vsldoi128` (70) — primary opcode 4 is the VMX128 space on
Xenon; capstone applies POWER9, which reuses it for
`vcmpequd`/`maddhd`/`maddld`
- `slwi.` vs `rotlwi.` (10) — both name rotate-0/full-mask; capstone is
itself inconsistent here, using `slwi` when Rc=0
Adds `xenia-cpu --example decode_table_check`, which replays canary's table
through our decoder (455/455), and 9 regression tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
37
crates/xenia-cpu/examples/decode_table_check.rs
Normal file
37
crates/xenia-cpu/examples/decode_table_check.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! Cross-check our decoder against xenia-canary's authoritative encoding table.
|
||||
//!
|
||||
//! Canary's `ppc_opcode_table_gen.cc` lists, for every opcode it knows, a
|
||||
//! representative instruction word with the operand fields zeroed. Feeding each
|
||||
//! word to our decoder must yield the matching opcode — anything else is a hole
|
||||
//! or a mis-decode in our tables.
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run --release -p xenia-cpu --example decode_table_check -- <table.txt>
|
||||
//! ```
|
||||
//! where each line is `0xWORD name`.
|
||||
use std::io::BufRead;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = std::env::args().nth(1).ok_or("usage: decode_table_check <table>")?;
|
||||
let f = std::io::BufReader::new(std::fs::File::open(path)?);
|
||||
let (mut ok, mut bad, mut invalid) = (0u32, 0u32, 0u32);
|
||||
for line in f.lines() {
|
||||
let line = line?;
|
||||
let mut it = line.split_whitespace();
|
||||
let (Some(w), Some(name)) = (it.next(), it.next()) else { continue };
|
||||
let word = u32::from_str_radix(w.trim_start_matches("0x"), 16)?;
|
||||
let d = xenia_cpu::decoder::decode(word, 0x8200_0000);
|
||||
let got = format!("{:?}", d.opcode);
|
||||
if got == name {
|
||||
ok += 1;
|
||||
} else if got == "Invalid" {
|
||||
invalid += 1;
|
||||
println!("MISSING {w} {name:<14} -> Invalid");
|
||||
} else {
|
||||
bad += 1;
|
||||
println!("MISMATCH {w} {name:<14} -> {got}");
|
||||
}
|
||||
}
|
||||
println!("\nmatched {ok}, mismatched {bad}, missing {invalid}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -127,6 +127,7 @@ 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"),
|
||||
@@ -298,7 +299,7 @@ pub fn format(instr: &DecodedInstr) -> DisasmText {
|
||||
// ── Special register moves ─────────────────────────────────────────
|
||||
PpcOpcode::mfspr => fmt_mfspr(instr),
|
||||
PpcOpcode::mtspr => fmt_mtspr(instr),
|
||||
PpcOpcode::mfcr => base("mfcr", gpr(instr.rd()), 8),
|
||||
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),
|
||||
@@ -441,6 +442,16 @@ pub fn format(instr: &DecodedInstr) -> DisasmText {
|
||||
|
||||
// ── 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 |
|
||||
@@ -468,6 +479,7 @@ pub fn format(instr: &DecodedInstr) -> DisasmText {
|
||||
fmt_vmx_3op(instr, opcode_name(instr.opcode))
|
||||
}
|
||||
|
||||
|
||||
// VMX unary VD, VB
|
||||
PpcOpcode::vrefp | PpcOpcode::vrsqrtefp | PpcOpcode::vexptefp |
|
||||
PpcOpcode::vlogefp | PpcOpcode::vrfin | PpcOpcode::vrfiz |
|
||||
@@ -799,6 +811,33 @@ fn fmt_b(instr: &DecodedInstr) -> DisasmText {
|
||||
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();
|
||||
@@ -818,6 +857,7 @@ fn fmt_bc(instr: &DecodedInstr) -> DisasmText {
|
||||
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}");
|
||||
@@ -836,15 +876,26 @@ fn fmt_bc(instr: &DecodedInstr) -> DisasmText {
|
||||
|
||||
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)
|
||||
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}");
|
||||
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 {
|
||||
@@ -861,6 +912,7 @@ fn fmt_bclr(instr: &DecodedInstr) -> DisasmText {
|
||||
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
|
||||
@@ -871,7 +923,7 @@ fn fmt_bclr(instr: &DecodedInstr) -> DisasmText {
|
||||
}
|
||||
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}");
|
||||
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 {
|
||||
@@ -882,7 +934,7 @@ fn fmt_bclr(instr: &DecodedInstr) -> DisasmText {
|
||||
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}");
|
||||
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)
|
||||
@@ -895,6 +947,7 @@ fn fmt_bcctr(instr: &DecodedInstr) -> DisasmText {
|
||||
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 {
|
||||
@@ -903,7 +956,7 @@ fn fmt_bcctr(instr: &DecodedInstr) -> DisasmText {
|
||||
}
|
||||
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}");
|
||||
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 {
|
||||
@@ -921,7 +974,12 @@ fn fmt_trap_imm(instr: &DecodedInstr, mnem: &str, simplified_prefix: &str) -> Di
|
||||
let base_ops = format!("{to}, {}, {imm}", gpr(ra));
|
||||
if let Some(cond) = trap_cond(to) {
|
||||
if cond.is_empty() {
|
||||
base(mnem, base_ops, 8)
|
||||
// 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));
|
||||
@@ -1118,7 +1176,13 @@ fn fmt_rlwinm(instr: &DecodedInstr) -> DisasmText {
|
||||
let base_ops = format!("{}, {}, {sh}, {mb}, {me}", gpr(ra), gpr(rs));
|
||||
|
||||
// Priority-ordered simplified forms.
|
||||
if sh > 0 && mb == 0 && me == 31 - sh {
|
||||
//
|
||||
// `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);
|
||||
}
|
||||
@@ -1435,9 +1499,32 @@ fn fmt_mtspr(instr: &DecodedInstr) -> DisasmText {
|
||||
}
|
||||
}
|
||||
|
||||
/// `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)
|
||||
@@ -1597,6 +1684,20 @@ fn fmt_mtfsb(instr: &DecodedInstr, mnem: &str) -> DisasmText {
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -1875,4 +1976,153 @@ mod tests {
|
||||
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 | (0 << 11) | (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) | 0;
|
||||
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) | 0;
|
||||
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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user