fix(cpu): PPCBUG-009/010+011/041+042+043 mul/div + srawx truncation

Phase 4 batch 4: mulwx, divwx (coupled +CR0), srawx/srawix (coupled +CR0).

- PPCBUG-009 mullwx: 32-bit ABI. Product truncated to u32 before write.
  OE handler still uses full i64 product to detect overflow.
- PPCBUG-010+011 divwx (coupled): quotient zero-extended (canary uses
  ZeroExtend(v, INT64_TYPE)). CR0 view via i32 — without this, a negative
  i32 quotient (e.g. -3 from -10/3) would be classified as positive in
  i64 view of the now-zero-extended writeback.
- PPCBUG-041+042+043 srawx/srawix (coupled): writeback uses `as u32 as u64`
  (was `as i64 as u64`). All-ones case (sh>=32 with negative input) writes
  0x00000000_FFFFFFFF instead of u64::MAX. CR0 view via i32. CA logic
  preserved unchanged (audit-verified independently correct).

Tests:
- mullwx_overflow_truncates_to_32 (PPCBUG-009).
- divwx_negative_quotient_zero_extends (PPCBUG-010+011).
- srawx_negative_value_zero_extends_upper (PPCBUG-041+043).
- srawix_high_count_negative_input_yields_low32_all_ones (PPCBUG-042+043).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-02 11:44:34 +02:00
parent bf8208e88c
commit 82a9bff934

View File

@@ -347,16 +347,17 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4;
}
PpcOpcode::mullwx => {
// PPCBUG-009: 32-bit ABI. Truncate product to u32 — overflow detection
// (mullw_ov) still uses the full i64 product to catch the overflow.
let ra = ctx.gpr[instr.ra()] as i32 as i64;
let rb = ctx.gpr[instr.rb()] as i32 as i64;
let product = ra.wrapping_mul(rb);
ctx.gpr[instr.rd()] = product as u64;
ctx.gpr[instr.rd()] = product as u32 as u64;
if instr.oe() {
// OV iff the 64-bit product can't fit into 32-bit signed.
overflow::apply(ctx, overflow::mullw_ov(product));
}
if instr.rc_bit() {
ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i64);
ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as u32 as i32 as i64);
}
ctx.pc += 4;
}
@@ -381,20 +382,21 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4;
}
PpcOpcode::divwx => {
// PPCBUG-010+011 coupled: 32-bit ABI. Quotient zero-extended to u64
// (canary explicitly uses ZeroExtend(v, INT64_TYPE)). CR0 view via i32.
let ra = ctx.gpr[instr.ra()] as i32;
let rb = ctx.gpr[instr.rb()] as i32;
let ov = overflow::divw_ov_signed(ra, rb);
if ov {
// PPC: RT undefined on div-by-zero / INT_MIN/-1. Canary uses 0.
ctx.gpr[instr.rd()] = 0;
} else {
ctx.gpr[instr.rd()] = (ra / rb) as i64 as u64;
ctx.gpr[instr.rd()] = (ra / rb) as u32 as u64;
}
if instr.oe() {
overflow::apply(ctx, ov);
}
if instr.rc_bit() {
ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as i64);
ctx.update_cr_signed(0, ctx.gpr[instr.rd()] as u32 as i32 as i64);
}
ctx.pc += 4;
}
@@ -615,34 +617,37 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4;
}
PpcOpcode::srawx => {
// PPCBUG-041+043 coupled: 32-bit ABI writeback truncation + CR0 i32.
// CA logic is independently correct (uses u32 shifted-out test).
let rs = ctx.gpr[instr.rs()] as i32;
let sh = ctx.gpr[instr.rb()] as u32 & 0x3F;
if sh == 0 {
ctx.gpr[instr.ra()] = rs as i64 as u64;
ctx.gpr[instr.ra()] = rs as u32 as u64;
ctx.xer_ca = 0;
} else if sh < 32 {
let result = rs >> sh;
ctx.xer_ca = if rs < 0 && (rs as u32) << (32 - sh) != 0 { 1 } else { 0 };
ctx.gpr[instr.ra()] = result as i64 as u64;
ctx.gpr[instr.ra()] = result as u32 as u64;
} else {
ctx.gpr[instr.ra()] = if rs < 0 { u64::MAX } else { 0 };
ctx.gpr[instr.ra()] = if rs < 0 { 0xFFFF_FFFFu64 } else { 0 };
ctx.xer_ca = if rs < 0 { 1 } else { 0 };
}
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); }
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4;
}
PpcOpcode::srawix => {
// PPCBUG-042+043 coupled: same shape as srawx for the sh-immediate form.
let rs = ctx.gpr[instr.rs()] as i32;
let sh = instr.sh();
if sh == 0 {
ctx.gpr[instr.ra()] = rs as i64 as u64;
ctx.gpr[instr.ra()] = rs as u32 as u64;
ctx.xer_ca = 0;
} else {
let result = rs >> sh;
ctx.xer_ca = if rs < 0 && (rs as u32) << (32 - sh) != 0 { 1 } else { 0 };
ctx.gpr[instr.ra()] = result as i64 as u64;
ctx.gpr[instr.ra()] = result as u32 as u64;
}
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); }
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4;
}
PpcOpcode::sldx => {
@@ -5198,6 +5203,72 @@ mod tests {
assert_eq!(ctx.xer_ca, 1, "rb>=ra → CA=1 (10 > 5)");
}
#[test]
fn mullwx_overflow_truncates_to_32() {
// PPCBUG-009: mullwo r5, r3, r4 with ra=0x10000, rb=0x10000 → product
// 0x100000000 (overflow). Low 32 = 0; OE must fire.
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 0x10000;
ctx.gpr[4] = 0x10000;
// mullwo r5, r3, r4 (XO=235, OE=1)
let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (1 << 10) | (235 << 1);
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 0, "low 32 bits = 0");
assert_eq!(ctx.xer_ov, 1, "overflow detected");
}
#[test]
fn divwx_negative_quotient_zero_extends() {
// PPCBUG-010+011: -10 / 3 = -3 must produce 0x00000000_FFFFFFFD,
// not 0xFFFFFFFF_FFFFFFFD. CR0.LT must still fire (i32 view of FFFFFFFD is negative).
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = (-10i32) as u32 as u64;
ctx.gpr[4] = 3;
// divwx. r5, r3, r4 (XO=491, Rc=1)
let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (491 << 1) | 1;
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFFDu64);
assert!(ctx.cr[0].lt, "CR0.LT must fire for negative i32 quotient");
}
#[test]
fn srawx_negative_value_zero_extends_upper() {
// PPCBUG-041+043: srawx of negative i32 by 1 produces a negative i32;
// writeback must zero-extend to u64 (not sign-extend).
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 0x8000_0000u64; // i32::MIN
ctx.gpr[4] = 1;
// srawx. r5, r3, r4 (XO=792, Rc=1)
let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (4 << 11) | (792 << 1) | 1;
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 0x0000_0000_C000_0000u64);
assert!(ctx.cr[0].lt);
}
#[test]
fn srawix_high_count_negative_input_yields_low32_all_ones() {
// PPCBUG-042+043: srawi with count=31 on negative input → low 32 bits
// all ones (0xFFFFFFFF), upper 32 zero (was u64::MAX before fix).
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 0x8000_0000u64;
// srawix r5, r3, 31 (XO=824)
let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (31 << 11) | (824 << 1);
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFFFu64);
}
#[test]
fn addi_li_neg_one_zero_extends_upper() {
// PPCBUG-001: `li r3, -1` (= addi r3, r0, -1) must produce