fix(cpu): PPCBUG-034+035+036+037 extsbx/extshx writeback + CR0 (coupled)

Phase 4 batch 2: extsbx and extshx writeback truncation + CR0 view fix.
Coupled per audit — must land together because the writeback fix would
silently break CR0 sign classification if the CR0 fix didn't ship in
the same commit.

Before:
- extsbx: `as i8 as i64 as u64` — every negative byte poisoned upper
  32 bits (active poisoning, not latent). 0x80 → 0xFFFFFFFF_FFFFFF80.
- extshx: same shape for halfwords.
- CR0: `as i64` view — accidentally correct on the buggy 64-bit form
  because the high bits matched the byte's sign bit.

After:
- extsbx: `as i8 as i32 as u32 as u64` — sign-extend to i32 then
  zero-extend to u64. 0x80 → 0x00000000_FFFFFF80.
- extshx: same for halfwords.
- CR0: `as u32 as i32 as i64` — i32 view, so a result with bit 31 set
  is correctly classified as negative under the 32-bit ABI.

Tests:
- extsbx_negative_byte_zero_extends_upper: 0x80 input → 0x00000000_FFFFFF80
  with CR0.LT set.
- extshx_negative_halfword_zero_extends_upper: same shape for 0x8000.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-02 11:38:22 +02:00
parent e18a0a40b8
commit 145a7a4019

View File

@@ -556,13 +556,17 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
// ===== Extend/Count =====
PpcOpcode::extsbx => {
ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i8 as i64 as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); }
// PPCBUG-034: 32-bit ABI — sign-extend byte to i32, write zero-extended.
// PPCBUG-036 (coupled): CR0 must view result as i32, not i64.
ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i8 as i32 as u32 as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4;
}
PpcOpcode::extshx => {
ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i16 as i64 as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); }
// PPCBUG-035: same shape as extsbx for halfwords.
// PPCBUG-037 (coupled): CR0 i32 view.
ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] as i16 as i32 as u32 as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4;
}
PpcOpcode::extswx => {
@@ -5182,6 +5186,39 @@ mod tests {
assert_eq!(ctx.xer_ca, 1, "rb>=ra → CA=1 (10 > 5)");
}
#[test]
fn extsbx_negative_byte_zero_extends_upper() {
// PPCBUG-034+036 coupled: extsb of 0x80 (negative byte) must produce
// 0x00000000_FFFFFF80, NOT 0xFFFFFFFF_FFFFFF80. CR0.LT must still fire
// (i32 view of 0xFFFFFF80 is negative).
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 0x80;
// extsbx. r5, r3 (XO=954, Rc=1)
let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (954 << 1) | 1;
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FF80);
assert!(ctx.cr[0].lt, "CR0.LT must fire for negative i32");
assert!(!ctx.cr[0].gt);
}
#[test]
fn extshx_negative_halfword_zero_extends_upper() {
// PPCBUG-035+037 coupled: extsh of 0x8000 must produce 0x00000000_FFFF8000.
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 0x8000;
// extshx. r5, r3 (XO=922, Rc=1)
let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (922 << 1) | 1;
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_8000);
assert!(ctx.cr[0].lt);
}
#[test]
fn subfmex_ra_max_ca_zero_clears_ca() {
// PPCBUG-019: `subfme` with RA=u32::MAX and CA=0 should set CA=0