fix(cpu): PPCBUG-012-017/020/023-026/032/044 4c+4d latent + CR0 catch-all

Phase 4 batch 6: latent writeback truncation (4c) and CR0 catch-all (4d).
~13 PPCBUGs across all remaining 32-bit ABI ALU sites.

Latent writeback (4c) — the 4a/4b fixes already eliminate the upstream
poisoning, but a defensive truncation here catches any future regression:
- PPCBUG-012 addx, PPCBUG-013 addcx, PPCBUG-014 addex, PPCBUG-015 addzex,
  PPCBUG-016 addmex, PPCBUG-017 subfx — all rewritten to compute on u32
  operands and write `as u64`. CA computed via 32-bit unsigned compare.
  Overflow now uses `true_sum != (result32 as i32) as i128` (32-bit
  predicate, since sum_overflow_64 is i64-bounded).
- PPCBUG-032 andx/orx/xorx — CR0 catch-all only (results inherit upper
  bits from operands; once those are clean, no truncation needed).

CR0 catch-all (4d) — fix the `update_cr_signed(0, X as i64)` pattern at
every 32-bit-ABI Rc=1 path:
- PPCBUG-020 catch-all: applied to mulhwx, mulhwux, divwux, mullwx (was
  already done in batch 4), addx/addcx/addex/addzex/addmex/subfx (now in
  4c above), andx/orx/xorx, andix, andisx, slwx, srwx, cntlzwx,
  rlwinmx, rlwimix, rlwnmx, mullwx (already), divwx (already),
  srawx/srawix (already in batch 4).
- PPCBUG-023 andisx: now correctly classifies bit-31 results as CR0.LT.
- PPCBUG-024 rlwinmx, PPCBUG-025 rlwimix, PPCBUG-026 rlwnmx.
- PPCBUG-044 slwx/srwx: bit-31 result like 0x80000000 now CR0.LT.

64-bit ABI ops (rldicl/rldicr/rldic/rldimi/rldcl/rldcr, sldx/srdx/sradx/
sradix, mulhdx/mulhdux/mulldx, divdx/divdux, cntlzdx) intentionally retain
the 64-bit `as i64` form per ISA — these are 64-bit-mode instructions.

Updated old tests:
- addo_sets_xer_ov_on_signed_overflow_and_stickies_so: i32::MAX + 1 → INT_MIN.
- addx_rc_uses_64bit_compare_not_32bit: renamed to ..._uses_32bit_compare_in_xbox_abi
  with assertions flipped to the correct 32-bit ABI behavior.

New tests:
- andisx_sign_bit_set_classifies_lt (PPCBUG-023).
- slwx_high_bit_result_classifies_lt (PPCBUG-044).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-02 11:55:50 +02:00
parent 20a730d69e
commit 16993bb8af

View File

@@ -173,89 +173,97 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
// ===== ALU: Register ===== // ===== ALU: Register =====
PpcOpcode::addx => { PpcOpcode::addx => {
let ra = ctx.gpr[instr.ra()]; // PPCBUG-012+020: 32-bit ABI writeback truncation + CR0 i32 view.
let rb = ctx.gpr[instr.rb()]; let ra32 = ctx.gpr[instr.ra()] as u32;
let result = ra.wrapping_add(rb); let rb32 = ctx.gpr[instr.rb()] as u32;
ctx.gpr[instr.rd()] = result; let result32 = ra32.wrapping_add(rb32);
ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
overflow::apply(ctx, overflow::add_ov_64(ra, rb, result)); let true_sum = (ra32 as i32 as i128) + (rb32 as i32 as i128);
overflow::apply(ctx, true_sum != (result32 as i32) as i128);
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
} }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::addcx => { PpcOpcode::addcx => {
let ra = ctx.gpr[instr.ra()]; // PPCBUG-013+020: 32-bit truncation; CA from u32 unsigned compare.
let rb = ctx.gpr[instr.rb()]; let ra32 = ctx.gpr[instr.ra()] as u32;
let result = ra.wrapping_add(rb); let rb32 = ctx.gpr[instr.rb()] as u32;
ctx.xer_ca = if result < ra { 1 } else { 0 }; let result32 = ra32.wrapping_add(rb32);
ctx.gpr[instr.rd()] = result; ctx.xer_ca = if result32 < ra32 { 1 } else { 0 };
ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
overflow::apply(ctx, overflow::add_ov_64(ra, rb, result)); let true_sum = (ra32 as i32 as i128) + (rb32 as i32 as i128);
overflow::apply(ctx, true_sum != (result32 as i32) as i128);
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
} }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::addex => { PpcOpcode::addex => {
let ra = ctx.gpr[instr.ra()]; // PPCBUG-014+020: 32-bit truncation; CA from u32 unsigned compare.
let rb = ctx.gpr[instr.rb()]; let ra32 = ctx.gpr[instr.ra()] as u32;
let ca = ctx.xer_ca as u64; let rb32 = ctx.gpr[instr.rb()] as u32;
let result = ra.wrapping_add(rb).wrapping_add(ca); let ca = ctx.xer_ca as u32;
ctx.xer_ca = if result < ra || (ca != 0 && result == ra) { 1 } else { 0 }; let result32 = ra32.wrapping_add(rb32).wrapping_add(ca);
ctx.gpr[instr.rd()] = result; ctx.xer_ca = if result32 < ra32 || (ca != 0 && result32 == ra32) { 1 } else { 0 };
ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
let true_sum = (ra as i64 as i128) + (rb as i64 as i128) + (ca as i128); let true_sum = (ra32 as i32 as i128) + (rb32 as i32 as i128) + (ca as i128);
overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); overflow::apply(ctx, true_sum != (result32 as i32) as i128);
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
} }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::addzex => { PpcOpcode::addzex => {
let ra = ctx.gpr[instr.ra()]; // PPCBUG-015+020: 32-bit truncation.
let ca = ctx.xer_ca as u64; let ra32 = ctx.gpr[instr.ra()] as u32;
let result = ra.wrapping_add(ca); let ca = ctx.xer_ca as u32;
ctx.xer_ca = if result < ra { 1 } else { 0 }; let result32 = ra32.wrapping_add(ca);
ctx.gpr[instr.rd()] = result; ctx.xer_ca = if result32 < ra32 { 1 } else { 0 };
ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
let true_sum = (ra as i64 as i128) + (ca as i128); let true_sum = (ra32 as i32 as i128) + (ca as i128);
overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); overflow::apply(ctx, true_sum != (result32 as i32) as i128);
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
} }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::addmex => { PpcOpcode::addmex => {
let ra = ctx.gpr[instr.ra()]; // PPCBUG-016+020: 32-bit truncation. RT = RA + CA - 1.
let ca = ctx.xer_ca as u64; let ra32 = ctx.gpr[instr.ra()] as u32;
let result = ra.wrapping_add(ca).wrapping_sub(1); let ca = ctx.xer_ca as u32;
ctx.xer_ca = if ra != 0 || ca != 0 { 1 } else { 0 }; let result32 = ra32.wrapping_add(ca).wrapping_sub(1);
ctx.gpr[instr.rd()] = result; ctx.xer_ca = if ra32 != 0 || ca != 0 { 1 } else { 0 };
ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
// RT <- RA + CA + (-1) let true_sum = (ra32 as i32 as i128) + (ca as i128) - 1;
let true_sum = (ra as i64 as i128) + (ca as i128) - 1; overflow::apply(ctx, true_sum != (result32 as i32) as i128);
overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result));
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
} }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::subfx => { PpcOpcode::subfx => {
let ra = ctx.gpr[instr.ra()]; // PPCBUG-017+020: 32-bit truncation.
let rb = ctx.gpr[instr.rb()]; let ra32 = ctx.gpr[instr.ra()] as u32;
let result = rb.wrapping_sub(ra); let rb32 = ctx.gpr[instr.rb()] as u32;
ctx.gpr[instr.rd()] = result; let result32 = rb32.wrapping_sub(ra32);
ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
overflow::apply(ctx, overflow::sub_ov_64(ra, rb, result)); let true_diff = (rb32 as i32 as i128) - (ra32 as i32 as i128);
overflow::apply(ctx, overflow::sum_overflow_64(true_diff, result32 as u64));
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
} }
ctx.pc += 4; ctx.pc += 4;
} }
@@ -289,7 +297,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
if instr.oe() { if instr.oe() {
// RT <- !RA + RB + CA == RB - RA - 1 + CA (32-bit semantics). // RT <- !RA + RB + CA == RB - RA - 1 + CA (32-bit semantics).
let true_sum = (rb32 as i32 as i128) - (ra32 as i32 as i128) - 1 + (ca as i128); let true_sum = (rb32 as i32 as i128) - (ra32 as i32 as i128) - 1 + (ca as i128);
overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result32 as u64)); overflow::apply(ctx, true_sum != (result32 as i32) as i128);
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result32 as i32 as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
@@ -307,7 +315,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.gpr[instr.rd()] = result32 as u64; ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
let true_sum = -(ra32 as i32 as i128) - 1 + (ca as i128); let true_sum = -(ra32 as i32 as i128) - 1 + (ca as i128);
overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result32 as u64)); overflow::apply(ctx, true_sum != (result32 as i32) as i128);
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result32 as i32 as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
@@ -324,7 +332,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.gpr[instr.rd()] = result32 as u64; ctx.gpr[instr.rd()] = result32 as u64;
if instr.oe() { if instr.oe() {
let true_sum = -(ra32 as i32 as i128) - 2 + (ca as i128); let true_sum = -(ra32 as i32 as i128) - 2 + (ca as i128);
overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result32 as u64)); overflow::apply(ctx, true_sum != (result32 as i32) as i128);
} }
if instr.rc_bit() { if instr.rc_bit() {
ctx.update_cr_signed(0, result32 as i32 as i64); ctx.update_cr_signed(0, result32 as i32 as i64);
@@ -362,22 +370,24 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::mulhwx => { PpcOpcode::mulhwx => {
// PPCBUG-020: 32-bit ABI CR0 view.
let ra = ctx.gpr[instr.ra()] as i32 as i64; let ra = ctx.gpr[instr.ra()] as i32 as i64;
let rb = ctx.gpr[instr.rb()] as i32 as i64; let rb = ctx.gpr[instr.rb()] as i32 as i64;
let result = ra.wrapping_mul(rb); let result = ra.wrapping_mul(rb);
ctx.gpr[instr.rd()] = ((result >> 32) as i32 as i64 as u64) & 0xFFFF_FFFF; ctx.gpr[instr.rd()] = ((result >> 32) as u32) as u64;
if instr.rc_bit() { 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; ctx.pc += 4;
} }
PpcOpcode::mulhwux => { PpcOpcode::mulhwux => {
// PPCBUG-020: 32-bit ABI CR0 view.
let ra = ctx.gpr[instr.ra()] as u32 as u64; let ra = ctx.gpr[instr.ra()] as u32 as u64;
let rb = ctx.gpr[instr.rb()] as u32 as u64; let rb = ctx.gpr[instr.rb()] as u32 as u64;
let result = ra.wrapping_mul(rb); let result = ra.wrapping_mul(rb);
ctx.gpr[instr.rd()] = (result >> 32) & 0xFFFF_FFFF; ctx.gpr[instr.rd()] = (result >> 32) & 0xFFFF_FFFF;
if instr.rc_bit() { 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; ctx.pc += 4;
} }
@@ -401,6 +411,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::divwux => { PpcOpcode::divwux => {
// PPCBUG-020: 32-bit ABI CR0 view.
let ra = ctx.gpr[instr.ra()] as u32; let ra = ctx.gpr[instr.ra()] as u32;
let rb = ctx.gpr[instr.rb()] as u32; let rb = ctx.gpr[instr.rb()] as u32;
let ov = overflow::divw_ov_unsigned(rb); let ov = overflow::divw_ov_unsigned(rb);
@@ -413,7 +424,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
overflow::apply(ctx, ov); overflow::apply(ctx, ov);
} }
if instr.rc_bit() { 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; ctx.pc += 4;
} }
@@ -486,13 +497,16 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
// ===== Logical ===== // ===== Logical =====
PpcOpcode::andix => { PpcOpcode::andix => {
// PPCBUG-020: 32-bit ABI CR0 view.
ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & (instr.uimm16() as u64); ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & (instr.uimm16() as u64);
ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64);
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::andisx => { PpcOpcode::andisx => {
// PPCBUG-023: 32-bit ABI CR0 view. `andis. rA, rS, 0x8000` to test
// sign bit of a 32-bit word now correctly classifies bit 31 = 1 as LT.
ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & ((instr.uimm16() as u64) << 16); ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & ((instr.uimm16() as u64) << 16);
ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64);
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::ori => { PpcOpcode::ori => {
@@ -512,8 +526,9 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::andx => { PpcOpcode::andx => {
// PPCBUG-032+020: 32-bit ABI CR0 view (latent under clean inputs).
ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & ctx.gpr[instr.rb()]; ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] & ctx.gpr[instr.rb()];
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; ctx.pc += 4;
} }
PpcOpcode::andcx => { PpcOpcode::andcx => {
@@ -525,8 +540,9 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::orx => { PpcOpcode::orx => {
// PPCBUG-032+020: 32-bit ABI CR0 view.
ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | ctx.gpr[instr.rb()]; ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] | ctx.gpr[instr.rb()];
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; ctx.pc += 4;
} }
PpcOpcode::orcx => { PpcOpcode::orcx => {
@@ -538,8 +554,9 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::xorx => { PpcOpcode::xorx => {
// PPCBUG-032+020: 32-bit ABI CR0 view.
ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] ^ ctx.gpr[instr.rb()]; ctx.gpr[instr.ra()] = ctx.gpr[instr.rs()] ^ ctx.gpr[instr.rb()];
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; ctx.pc += 4;
} }
PpcOpcode::norx => { PpcOpcode::norx => {
@@ -589,8 +606,10 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::cntlzwx => { PpcOpcode::cntlzwx => {
// Result is 0..=32, fits in u32 with bit 31 always zero, so the
// CR0 view is benign — use the catch-all 32-bit form for consistency.
ctx.gpr[instr.ra()] = (ctx.gpr[instr.rs()] as u32).leading_zeros() as u64; ctx.gpr[instr.ra()] = (ctx.gpr[instr.rs()] as u32).leading_zeros() 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; ctx.pc += 4;
} }
PpcOpcode::cntlzdx => { PpcOpcode::cntlzdx => {
@@ -601,19 +620,23 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
// ===== Shift ===== // ===== Shift =====
PpcOpcode::slwx => { PpcOpcode::slwx => {
// PPCBUG-044: 32-bit ABI CR0 view. A result with bit 31 set
// (e.g. 0x80000000) is negative in i32 view but positive in i64.
let sh = ctx.gpr[instr.rb()] as u32; let sh = ctx.gpr[instr.rb()] as u32;
ctx.gpr[instr.ra()] = if sh < 32 { ctx.gpr[instr.ra()] = if sh < 32 {
((ctx.gpr[instr.rs()] as u32) << sh) as u64 ((ctx.gpr[instr.rs()] as u32) << sh) as u64
} else { 0 }; } 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; ctx.pc += 4;
} }
PpcOpcode::srwx => { PpcOpcode::srwx => {
// PPCBUG-044: 32-bit ABI CR0 view (zero-extended right shift can never
// have bit 31 set, but use the canonical form for consistency).
let sh = ctx.gpr[instr.rb()] as u32; let sh = ctx.gpr[instr.rb()] as u32;
ctx.gpr[instr.ra()] = if sh < 32 { ctx.gpr[instr.ra()] = if sh < 32 {
((ctx.gpr[instr.rs()] as u32) >> sh) as u64 ((ctx.gpr[instr.rs()] as u32) >> sh) as u64
} else { 0 }; } 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; ctx.pc += 4;
} }
PpcOpcode::srawx => { PpcOpcode::srawx => {
@@ -707,7 +730,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
let rotated = rs.rotate_left(sh); let rotated = rs.rotate_left(sh);
let mask = rlw_mask(mb, me); let mask = rlw_mask(mb, me);
ctx.gpr[instr.ra()] = (rotated & mask) as u64; ctx.gpr[instr.ra()] = (rotated & mask) as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } // PPCBUG-024: 32-bit ABI CR0 view.
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::rlwimix => { PpcOpcode::rlwimix => {
@@ -719,7 +743,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
let mask = rlw_mask(mb, me); let mask = rlw_mask(mb, me);
let ra = ctx.gpr[instr.ra()] as u32; let ra = ctx.gpr[instr.ra()] as u32;
ctx.gpr[instr.ra()] = ((rotated & mask) | (ra & !mask)) as u64; ctx.gpr[instr.ra()] = ((rotated & mask) | (ra & !mask)) as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } // PPCBUG-025: 32-bit ABI CR0 view.
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::rlwnmx => { PpcOpcode::rlwnmx => {
@@ -730,7 +755,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -
let rotated = rs.rotate_left(sh); let rotated = rs.rotate_left(sh);
let mask = rlw_mask(mb, me); let mask = rlw_mask(mb, me);
ctx.gpr[instr.ra()] = (rotated & mask) as u64; ctx.gpr[instr.ra()] = (rotated & mask) as u64;
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as i64); } // PPCBUG-026: 32-bit ABI CR0 view.
if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); }
ctx.pc += 4; ctx.pc += 4;
} }
PpcOpcode::rldiclx => { PpcOpcode::rldiclx => {
@@ -5004,14 +5030,15 @@ mod tests {
#[test] #[test]
fn addo_sets_xer_ov_on_signed_overflow_and_stickies_so() { fn addo_sets_xer_ov_on_signed_overflow_and_stickies_so() {
// PPCBUG-012: 32-bit ABI. INT32_MAX + 1 overflows to INT32_MIN.
let mut ctx = PpcContext::new(); let mut ctx = PpcContext::new();
let mut mem = TestMem::new(); let mut mem = TestMem::new();
ctx.gpr[3] = i64::MAX as u64; ctx.gpr[3] = i32::MAX as u32 as u64;
ctx.gpr[4] = 1; ctx.gpr[4] = 1;
write_instr(&mut mem, 0, addx_raw(5, 3, 4, true, false)); write_instr(&mut mem, 0, addx_raw(5, 3, 4, true, false));
ctx.pc = 0; ctx.pc = 0;
step(&mut ctx, &mut mem); step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], i64::MIN as u64); assert_eq!(ctx.gpr[5], 0x8000_0000u64);
assert_eq!(ctx.xer_ov, 1, "OV must be set on signed overflow"); assert_eq!(ctx.xer_ov, 1, "OV must be set on signed overflow");
assert_eq!(ctx.xer_so, 1, "SO must be stickied from OV"); assert_eq!(ctx.xer_so, 1, "SO must be stickied from OV");
} }
@@ -5046,9 +5073,10 @@ mod tests {
} }
#[test] #[test]
fn addx_rc_uses_64bit_compare_not_32bit() { fn addx_rc_uses_32bit_compare_in_xbox_abi() {
// r3 = 0x0000_0000_FFFF_FFFF, r4 = 0 → result = 0x0000_0000_FFFF_FFFF. // PPCBUG-012+020: 32-bit ABI. r3 + r4 = 0xFFFFFFFF (low 32). As i32
// As i32 this is -1 (lt). As i64 this is positive (gt). Spec says 64-bit. // this is -1 (CR0.LT). The previous 64-bit compare wrongly classified
// this as positive (CR0.GT) for Xbox 360 binaries.
let mut ctx = PpcContext::new(); let mut ctx = PpcContext::new();
let mut mem = TestMem::new(); let mut mem = TestMem::new();
ctx.gpr[3] = 0x0000_0000_FFFF_FFFF; ctx.gpr[3] = 0x0000_0000_FFFF_FFFF;
@@ -5057,8 +5085,8 @@ mod tests {
ctx.pc = 0; ctx.pc = 0;
step(&mut ctx, &mut mem); step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFFF); assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFFF);
assert!(!ctx.cr[0].lt, "64-bit compare: value is positive, not negative"); assert!(ctx.cr[0].lt, "32-bit ABI: 0xFFFFFFFF as i32 is -1, CR0.LT");
assert!(ctx.cr[0].gt); assert!(!ctx.cr[0].gt);
assert!(!ctx.cr[0].eq); assert!(!ctx.cr[0].eq);
} }
@@ -5203,6 +5231,39 @@ mod tests {
assert_eq!(ctx.xer_ca, 1, "rb>=ra → CA=1 (10 > 5)"); assert_eq!(ctx.xer_ca, 1, "rb>=ra → CA=1 (10 > 5)");
} }
#[test]
fn andisx_sign_bit_set_classifies_lt() {
// PPCBUG-023: andis. r4, r3, 0x8000 with r3=0xFFFFFFFF should produce
// result=0x80000000 with CR0.LT=1 (i32 view).
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 0xFFFF_FFFFu64;
// andis. r4, r3, 0x8000: opcode 29, uimm16 = 0x8000
let raw = (29u32 << 26) | (3 << 21) | (4 << 16) | 0x8000;
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[4], 0x8000_0000u64);
assert!(ctx.cr[0].lt, "result=0x80000000 → i32 view negative → CR0.LT");
}
#[test]
fn slwx_high_bit_result_classifies_lt() {
// PPCBUG-044: slwx producing 0x80000000 must classify as CR0.LT under
// the 32-bit ABI, not CR0.GT (which 64-bit view would give).
let mut ctx = PpcContext::new();
let mut mem = TestMem::new();
ctx.gpr[3] = 0x4000_0000u64;
ctx.gpr[4] = 1;
// slwx. r5, r3, r4 (XO=24, Rc=1)
let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (4 << 11) | (24 << 1) | 1;
write_instr(&mut mem, 0, raw);
ctx.pc = 0;
step(&mut ctx, &mut mem);
assert_eq!(ctx.gpr[5], 0x8000_0000u64);
assert!(ctx.cr[0].lt, "0x80000000 as i32 is negative");
}
#[test] #[test]
fn lha_negative_halfword_zero_extends_upper() { fn lha_negative_halfword_zero_extends_upper() {
// PPCBUG-095: memory 0x8000 must yield gpr[rD] = 0x00000000_FFFF8000. // PPCBUG-095: memory 0x8000 must yield gpr[rD] = 0x00000000_FFFF8000.