diff --git a/crates/xenia-cpu/src/interpreter.rs b/crates/xenia-cpu/src/interpreter.rs index 19fa865..aad2485 100644 --- a/crates/xenia-cpu/src/interpreter.rs +++ b/crates/xenia-cpu/src/interpreter.rs @@ -112,8 +112,10 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - match instr.opcode { // ===== ALU: Immediate ===== PpcOpcode::addi => { + // PPCBUG-001: 32-bit ABI. `li rT, -1` (= addi rT, r0, -1) must produce + // 0x00000000_FFFFFFFF, not 0xFFFFFFFF_FFFFFFFF (sign-extended simm16). let ra_val = if instr.ra() == 0 { 0 } else { ctx.gpr[instr.ra()] }; - ctx.gpr[instr.rd()] = ra_val.wrapping_add(instr.simm16() as i64 as u64); + ctx.gpr[instr.rd()] = ra_val.wrapping_add(instr.simm16() as i64 as u64) as u32 as u64; ctx.pc += 4; } PpcOpcode::addis => { @@ -131,256 +133,285 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } PpcOpcode::addic => { - let ra = ctx.gpr[instr.ra()]; - let imm = instr.simm16() as i64 as u64; - let result = ra.wrapping_add(imm); - ctx.xer_ca = if result < ra { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-002: 32-bit ABI. CA must be from a 32-bit unsigned compare; + // canary's `AddDidCarry` truncates both operands to int32 first. + let ra32 = ctx.gpr[instr.ra()] as u32; + let imm32 = instr.simm16() as i32 as u32; + let result32 = ra32.wrapping_add(imm32); + ctx.xer_ca = if result32 < ra32 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; ctx.pc += 4; } PpcOpcode::addicx => { - let ra = ctx.gpr[instr.ra()]; - let imm = instr.simm16() as i64 as u64; - let result = ra.wrapping_add(imm); - ctx.xer_ca = if result < ra { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; - // Update CR0 - ctx.update_cr_signed(0, result as i64); + // PPCBUG-003: same fix as addic plus CR0 i32 view. + let ra32 = ctx.gpr[instr.ra()] as u32; + let imm32 = instr.simm16() as i32 as u32; + let result32 = ra32.wrapping_add(imm32); + ctx.xer_ca = if result32 < ra32 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; + ctx.update_cr_signed(0, result32 as i32 as i64); ctx.pc += 4; } PpcOpcode::subficx => { - let ra = ctx.gpr[instr.ra()]; - let imm = instr.simm16() as i64 as u64; - let result = imm.wrapping_sub(ra); - ctx.xer_ca = if imm >= ra { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-005: 32-bit ABI. Sign-extended imm has bits 32-63 set for + // negative SIMM, poisoning the writeback. Canary uses 32-bit form. + let ra32 = ctx.gpr[instr.ra()] as u32; + let imm32 = instr.simm16() as i32 as u32; + let result32 = imm32.wrapping_sub(ra32); + ctx.xer_ca = if imm32 >= ra32 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; ctx.pc += 4; } PpcOpcode::mulli => { - let ra = ctx.gpr[instr.ra()] as i64; + // PPCBUG-004: 32-bit ABI. Read RA as i32 (low 32, sign-extended for + // multiply), product fits in 32 bits per ISA (overflow wraps). + let ra = ctx.gpr[instr.ra()] as i32 as i64; let imm = instr.simm16() as i64; - ctx.gpr[instr.rd()] = ra.wrapping_mul(imm) as u64; + ctx.gpr[instr.rd()] = (ra.wrapping_mul(imm) as u32) as u64; ctx.pc += 4; } // ===== ALU: Register ===== PpcOpcode::addx => { - let ra = ctx.gpr[instr.ra()]; - let rb = ctx.gpr[instr.rb()]; - let result = ra.wrapping_add(rb); - ctx.gpr[instr.rd()] = result; + // PPCBUG-012+020: 32-bit ABI writeback truncation + CR0 i32 view. + let ra32 = ctx.gpr[instr.ra()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + let result32 = ra32.wrapping_add(rb32); + ctx.gpr[instr.rd()] = result32 as u64; 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() { - ctx.update_cr_signed(0, result as i64); + ctx.update_cr_signed(0, result32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::addcx => { - let ra = ctx.gpr[instr.ra()]; - let rb = ctx.gpr[instr.rb()]; - let result = ra.wrapping_add(rb); - ctx.xer_ca = if result < ra { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-013+020: 32-bit truncation; CA from u32 unsigned compare. + let ra32 = ctx.gpr[instr.ra()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + let result32 = ra32.wrapping_add(rb32); + ctx.xer_ca = if result32 < ra32 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; 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() { - ctx.update_cr_signed(0, result as i64); + ctx.update_cr_signed(0, result32 as i32 as i64); } ctx.pc += 4; } PpcOpcode::addex => { - let ra = ctx.gpr[instr.ra()]; - let rb = ctx.gpr[instr.rb()]; - let ca = ctx.xer_ca as u64; - let result = ra.wrapping_add(rb).wrapping_add(ca); - ctx.xer_ca = if result < ra || (ca != 0 && result == ra) { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-014+020: 32-bit truncation; CA from u32 unsigned compare. + let ra32 = ctx.gpr[instr.ra()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + let ca = ctx.xer_ca as u32; + let result32 = ra32.wrapping_add(rb32).wrapping_add(ca); + ctx.xer_ca = if result32 < ra32 || (ca != 0 && result32 == ra32) { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - let true_sum = (ra as i64 as i128) + (rb as i64 as i128) + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + let true_sum = (ra32 as i32 as i128) + (rb32 as i32 as i128) + (ca as i128); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } 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; } PpcOpcode::addzex => { - let ra = ctx.gpr[instr.ra()]; - let ca = ctx.xer_ca as u64; - let result = ra.wrapping_add(ca); - ctx.xer_ca = if result < ra { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-015+020: 32-bit truncation. + let ra32 = ctx.gpr[instr.ra()] as u32; + let ca = ctx.xer_ca as u32; + let result32 = ra32.wrapping_add(ca); + ctx.xer_ca = if result32 < ra32 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - let true_sum = (ra as i64 as i128) + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + let true_sum = (ra32 as i32 as i128) + (ca as i128); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } 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; } PpcOpcode::addmex => { - let ra = ctx.gpr[instr.ra()]; - let ca = ctx.xer_ca as u64; - let result = ra.wrapping_add(ca).wrapping_sub(1); - ctx.xer_ca = if ra != 0 || ca != 0 { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-016+020: 32-bit truncation. RT = RA + CA - 1. + let ra32 = ctx.gpr[instr.ra()] as u32; + let ca = ctx.xer_ca as u32; + let result32 = ra32.wrapping_add(ca).wrapping_sub(1); + ctx.xer_ca = if ra32 != 0 || ca != 0 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - // RT <- RA + CA + (-1) - let true_sum = (ra as i64 as i128) + (ca as i128) - 1; - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + let true_sum = (ra32 as i32 as i128) + (ca as i128) - 1; + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } 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; } PpcOpcode::subfx => { - let ra = ctx.gpr[instr.ra()]; - let rb = ctx.gpr[instr.rb()]; - let result = rb.wrapping_sub(ra); - ctx.gpr[instr.rd()] = result; + // PPCBUG-017+020: 32-bit truncation. + let ra32 = ctx.gpr[instr.ra()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + let result32 = rb32.wrapping_sub(ra32); + ctx.gpr[instr.rd()] = result32 as u64; 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, true_diff != (result32 as i32) as i128); } 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; } PpcOpcode::subfcx => { - let ra = ctx.gpr[instr.ra()]; - let rb = ctx.gpr[instr.rb()]; - let result = rb.wrapping_sub(ra); - ctx.xer_ca = if rb >= ra { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-007: 32-bit ABI. The `rb >= ra` u64 unsigned compare is + // exactly the shape that broke addis. Defensive 32-bit truncation + // is required for correct CA even after upstream cleanup. + let ra32 = ctx.gpr[instr.ra()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + let result32 = rb32.wrapping_sub(ra32); + ctx.xer_ca = if rb32 >= ra32 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; 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, true_diff != (result32 as i32) as i128); } 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; } PpcOpcode::subfex => { - let ra = ctx.gpr[instr.ra()]; - let rb = ctx.gpr[instr.rb()]; - let ca = ctx.xer_ca as u64; - let result = (!ra).wrapping_add(rb).wrapping_add(ca); - ctx.xer_ca = if rb > ra || (rb == ra && ca != 0) { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-008: 32-bit ABI. Compute in u32 space — `!ra` on u64 always + // pollutes the upper 32 bits, making this an active poisoner. + let ra32 = ctx.gpr[instr.ra()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + let ca = ctx.xer_ca as u32; + let result32 = (!ra32).wrapping_add(rb32).wrapping_add(ca); + ctx.xer_ca = if rb32 > ra32 || (rb32 == ra32 && ca != 0) { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - // RT <- !RA + RB + CA == RB - RA - 1 + CA - let true_sum = (rb as i64 as i128) - (ra as i64 as i128) - 1 + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + // 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); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } 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; } PpcOpcode::subfzex => { - let ra = ctx.gpr[instr.ra()]; - let ca = ctx.xer_ca as u64; - let result = (!ra).wrapping_add(ca); - // RT <- !RA + CA (no -1 term). 64-bit carry-out only when - // !RA = u64::MAX (i.e. RA = 0) AND CA = 1. - ctx.xer_ca = if ra == 0 && ca != 0 { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-018: same active-poisoning shape as subfex; operate in u32. + let ra32 = ctx.gpr[instr.ra()] as u32; + let ca = ctx.xer_ca as u32; + let result32 = (!ra32).wrapping_add(ca); + // RT <- !RA + CA (no -1 term). 32-bit carry-out only when + // !ra32 = u32::MAX (i.e. ra32 = 0) AND ca = 1. + ctx.xer_ca = if ra32 == 0 && ca != 0 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - // RT <- !RA + CA == -RA - 1 + CA - let true_sum = -(ra as i64 as i128) - 1 + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + let true_sum = -(ra32 as i32 as i128) - 1 + (ca as i128); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } 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; } PpcOpcode::subfmex => { - let ra = ctx.gpr[instr.ra()]; - let ca = ctx.xer_ca as u64; - let result = (!ra).wrapping_add(ca).wrapping_sub(1); - ctx.xer_ca = if (!ra) != 0 || ca != 0 { 1 } else { 0 }; - ctx.gpr[instr.rd()] = result; + // PPCBUG-019: also fixes the always-true CA edge — `!ra` on u64 + // is non-zero when ra32==0xFFFFFFFF and ca==0, so CA was stuck at 1. + let ra32 = ctx.gpr[instr.ra()] as u32; + let ca = ctx.xer_ca as u32; + let result32 = (!ra32).wrapping_add(ca).wrapping_sub(1); + ctx.xer_ca = if (!ra32) != 0 || ca != 0 { 1 } else { 0 }; + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - // RT <- !RA + CA + (-1) == -RA - 2 + CA - let true_sum = -(ra as i64 as i128) - 2 + (ca as i128); - overflow::apply(ctx, overflow::sum_overflow_64(true_sum, result)); + let true_sum = -(ra32 as i32 as i128) - 2 + (ca as i128); + overflow::apply(ctx, true_sum != (result32 as i32) as i128); } 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; } PpcOpcode::negx => { - let ra = ctx.gpr[instr.ra()]; - let result = (!ra).wrapping_add(1); - ctx.gpr[instr.rd()] = result; + // PPCBUG-006: 32-bit ABI. `(!ra).wrapping_add(1)` on u64 always + // sets upper 32 bits — every neg poisoned the GPR. neg_ov also + // checks at 64-bit INT_MIN; should be 32-bit INT_MIN. + let ra32 = ctx.gpr[instr.ra()] as u32; + let result32 = (!ra32).wrapping_add(1); + ctx.gpr[instr.rd()] = result32 as u64; if instr.oe() { - overflow::apply(ctx, overflow::neg_ov_64(ra)); + overflow::apply(ctx, ra32 == 0x8000_0000); } 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; } 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; } PpcOpcode::mulhwx => { + // PPCBUG-020: 32-bit ABI CR0 view. let ra = ctx.gpr[instr.ra()] as i32 as i64; let rb = ctx.gpr[instr.rb()] as i32 as i64; 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() { - 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; } PpcOpcode::mulhwux => { + // PPCBUG-020: 32-bit ABI CR0 view. let ra = ctx.gpr[instr.ra()] as u32 as u64; let rb = ctx.gpr[instr.rb()] as u32 as u64; let result = ra.wrapping_mul(rb); ctx.gpr[instr.rd()] = (result >> 32) & 0xFFFF_FFFF; 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; } 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; } PpcOpcode::divwux => { + // PPCBUG-020: 32-bit ABI CR0 view. let ra = ctx.gpr[instr.ra()] as u32; let rb = ctx.gpr[instr.rb()] as u32; let ov = overflow::divw_ov_unsigned(rb); @@ -393,7 +424,7 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - 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; } @@ -466,13 +497,16 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - // ===== Logical ===== PpcOpcode::andix => { + // PPCBUG-020: 32-bit ABI CR0 view. 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; } 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.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; } PpcOpcode::ori => { @@ -492,55 +526,78 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } 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()]; - 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::andcx => { - 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); } + // PPCBUG-033: !rb on u64 flips upper 32 bits — active poisoning. + let rs32 = ctx.gpr[instr.rs()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = (rs32 & !rb32) 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::orx => { + // PPCBUG-032+020: 32-bit ABI CR0 view. 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; } PpcOpcode::orcx => { - 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); } + // PPCBUG-028: same shape as andcx — operate in u32. + let rs32 = ctx.gpr[instr.rs()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = (rs32 | !rb32) 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::xorx => { + // PPCBUG-032+020: 32-bit ABI CR0 view. 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; } PpcOpcode::norx => { - 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); } + // PPCBUG-029: `not` simplified mnemonic — every `not` poisoned the GPR. + let rs32 = ctx.gpr[instr.rs()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = (!(rs32 | rb32)) 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::nandx => { - 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); } + // PPCBUG-030: same shape — operate in u32. + let rs32 = ctx.gpr[instr.rs()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = (!(rs32 & rb32)) 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::eqvx => { - 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); } + // PPCBUG-031: `eqv rA, rA, rA` is a common "set to all-ones" idiom; + // 64-bit form gave 0xFFFFFFFFFFFFFFFF but 32-bit ABI expects 0x00000000FFFFFFFF. + let rs32 = ctx.gpr[instr.rs()] as u32; + let rb32 = ctx.gpr[instr.rb()] as u32; + ctx.gpr[instr.ra()] = (!(rs32 ^ rb32)) as u64; + if instr.rc_bit() { ctx.update_cr_signed(0, ctx.gpr[instr.ra()] as u32 as i32 as i64); } ctx.pc += 4; } // ===== 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 => { @@ -549,8 +606,10 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - ctx.pc += 4; } 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; - 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::cntlzdx => { @@ -561,50 +620,57 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - // ===== Shift ===== 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; ctx.gpr[instr.ra()] = if sh < 32 { ((ctx.gpr[instr.rs()] as u32) << sh) as u64 } 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::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; ctx.gpr[instr.ra()] = if sh < 32 { ((ctx.gpr[instr.rs()] as u32) >> sh) as u64 } 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::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 => { @@ -664,7 +730,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - let rotated = rs.rotate_left(sh); let mask = rlw_mask(mb, me); 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; } PpcOpcode::rlwimix => { @@ -676,7 +743,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - let mask = rlw_mask(mb, me); let ra = ctx.gpr[instr.ra()] as u32; 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; } PpcOpcode::rlwnmx => { @@ -687,7 +755,8 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - let rotated = rs.rotate_left(sh); let mask = rlw_mask(mb, me); 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; } PpcOpcode::rldiclx => { @@ -987,13 +1056,13 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - PpcOpcode::lha => { let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; let ea = ea.wrapping_add(instr.d() as i64 as u64) as u32; - ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i32 as u32 as u64; ctx.pc += 4; } PpcOpcode::lhax => { let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; - ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i32 as u32 as u64; ctx.pc += 4; } PpcOpcode::lhzux => { @@ -1004,13 +1073,13 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - } PpcOpcode::lhau => { let ea = ctx.gpr[instr.ra()].wrapping_add(instr.d() as i64 as u64) as u32; - ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i32 as u32 as u64; ctx.gpr[instr.ra()] = ea as u64; ctx.pc += 4; } PpcOpcode::lhaux => { let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; - ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u16(ea) as i16 as i32 as u32 as u64; ctx.gpr[instr.ra()] = ea as u64; ctx.pc += 4; } @@ -1029,18 +1098,18 @@ fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) - PpcOpcode::lwa => { let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; let ea = ea.wrapping_add(instr.ds() as i64 as u64) as u32; - ctx.gpr[instr.rd()] = mem.read_u32(ea) as i32 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; ctx.pc += 4; } PpcOpcode::lwax => { let ea = if instr.ra() == 0 { 0u64 } else { ctx.gpr[instr.ra()] }; let ea = ea.wrapping_add(ctx.gpr[instr.rb()]) as u32; - ctx.gpr[instr.rd()] = mem.read_u32(ea) as i32 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; ctx.pc += 4; } PpcOpcode::lwaux => { let ea = ctx.gpr[instr.ra()].wrapping_add(ctx.gpr[instr.rb()]) as u32; - ctx.gpr[instr.rd()] = mem.read_u32(ea) as i32 as i64 as u64; + ctx.gpr[instr.rd()] = mem.read_u32(ea) as u64; ctx.gpr[instr.ra()] = ea as u64; ctx.pc += 4; } @@ -4961,14 +5030,15 @@ mod tests { #[test] 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 mem = TestMem::new(); - ctx.gpr[3] = i64::MAX as u64; + ctx.gpr[3] = i32::MAX as u32 as u64; ctx.gpr[4] = 1; write_instr(&mut mem, 0, addx_raw(5, 3, 4, true, false)); ctx.pc = 0; 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_so, 1, "SO must be stickied from OV"); } @@ -5003,9 +5073,10 @@ mod tests { } #[test] - fn addx_rc_uses_64bit_compare_not_32bit() { - // r3 = 0x0000_0000_FFFF_FFFF, r4 = 0 → result = 0x0000_0000_FFFF_FFFF. - // As i32 this is -1 (lt). As i64 this is positive (gt). Spec says 64-bit. + fn addx_rc_uses_32bit_compare_in_xbox_abi() { + // PPCBUG-012+020: 32-bit ABI. r3 + r4 = 0xFFFFFFFF (low 32). As i32 + // 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 mem = TestMem::new(); ctx.gpr[3] = 0x0000_0000_FFFF_FFFF; @@ -5014,27 +5085,64 @@ mod tests { ctx.pc = 0; step(&mut ctx, &mut mem); 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].gt); + assert!(ctx.cr[0].lt, "32-bit ABI: 0xFFFFFFFF as i32 is -1, CR0.LT"); + assert!(!ctx.cr[0].gt); assert!(!ctx.cr[0].eq); } #[test] - fn subfo_sets_xer_ov_on_min_minus_one() { + fn subfo_sets_xer_ov_on_int32_min_minus_one() { + // PPCBUG-017: 32-bit ABI subfo overflow detection. r4=INT32_MIN, r3=1 + // → result = INT32_MIN - 1 → wraps to INT32_MAX with OV=1. let mut ctx = PpcContext::new(); let mut mem = TestMem::new(); - // subfo r5, r3, r4 -> r5 = r4 - r3 - // r4 = INT64_MIN, r3 = 1 -> result overflows ctx.gpr[3] = 1; - ctx.gpr[4] = i64::MIN as u64; + ctx.gpr[4] = 0x8000_0000u64; let raw = (31 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (1 << 10) | (40 << 1); write_instr(&mut mem, 0, raw); ctx.pc = 0; step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 0x7FFF_FFFFu64); assert_eq!(ctx.xer_ov, 1); assert_eq!(ctx.xer_so, 1); } + #[test] + fn subfo_no_spurious_ov_when_result_has_bit31_set() { + // PPCBUG-017 review-fix regression: subfo r5, r3, r4 with r3=1, r4=0x80000001 + // → result = 0x80000000. This is i32::MIN — a legitimate negative value + // with no 32-bit overflow (true_diff = -2147483648, fits in i32). + // The legacy `sum_overflow_64` predicate compared against the u64 view + // of result (= +2147483648), spuriously flagging OV=1. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 1; + ctx.gpr[4] = 0x8000_0001u64; + // subfo r5, r3, r4 + let raw = (31 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (1 << 10) | (40 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 0x8000_0000u64); + assert_eq!(ctx.xer_ov, 0, "legitimate i32::MIN result must NOT trigger OV"); + } + + #[test] + fn subfco_no_spurious_ov_when_result_has_bit31_set() { + // PPCBUG-007 same review-fix: subfcx OE handler must use 32-bit predicate. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 1; + ctx.gpr[4] = 0x8000_0001u64; + // subfco r5, r3, r4 (XO=8, OE=1) + let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (1 << 10) | (8 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 0x8000_0000u64); + assert_eq!(ctx.xer_ov, 0, "legitimate i32::MIN result must NOT trigger OV"); + } + #[test] fn mullwo_sets_xer_ov_when_product_overflows_32_bits() { let mut ctx = PpcContext::new(); @@ -5067,17 +5175,385 @@ mod tests { #[test] fn nego_sets_ov_only_on_int_min() { + // PPCBUG-006: 32-bit ABI. INT_MIN is 0x80000000 (low 32), not 0x8000000000000000. let mut ctx = PpcContext::new(); let mut mem = TestMem::new(); // nego r5, r3 (XO=104, OE=1) - ctx.gpr[3] = i64::MIN as u64; + ctx.gpr[3] = 0x8000_0000; let raw = (31 << 26) | (5 << 21) | (3 << 16) | (1 << 10) | (104 << 1); write_instr(&mut mem, 0, raw); ctx.pc = 0; step(&mut ctx, &mut mem); assert_eq!(ctx.xer_ov, 1); - // -INT_MIN wraps to INT_MIN - assert_eq!(ctx.gpr[5], i64::MIN as u64); + // -INT_MIN wraps to INT_MIN (low 32 bits) with upper 32 bits zero. + assert_eq!(ctx.gpr[5], 0x0000_0000_8000_0000); + } + + #[test] + fn neg_clean_input_no_upper_bits() { + // PPCBUG-006 regression: neg r3=5 must produce 0x00000000_FFFFFFFB, + // not 0xFFFFFFFF_FFFFFFFB (the 64-bit !ra-then-add-1 result). + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 5; + let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (104 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFFB); + } + + #[test] + fn norx_not_simplified_keeps_upper_bits_clean() { + // PPCBUG-029: `not rA, rB` (norx with rs==rb) is the canonical not + // simplified mnemonic. 64-bit !val poisons upper 32 bits of every + // execution; under the 32-bit ABI we must truncate. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0x0000_0000_0000_00FF; + // norx r5, r3, r3 (XO=124) + let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (3 << 11) | (124 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FF00, "upper 32 bits must be zero"); + } + + #[test] + fn eqvx_self_self_self_sets_low32_to_all_ones() { + // PPCBUG-031: `eqv rA, rA, rA` is a common "set-to-all-ones" idiom. + // 64-bit !(0^0) gives u64::MAX (0xFFFFFFFF_FFFFFFFF); 32-bit ABI + // expects 0x00000000_FFFFFFFF. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0; + // eqvx r3, r3, r3 (XO=284) + let raw = (31u32 << 26) | (3 << 21) | (3 << 16) | (3 << 11) | (284 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[3], 0x0000_0000_FFFF_FFFF); + } + + #[test] + fn andcx_bit_clear_keeps_upper_clean() { + // PPCBUG-033: `andc rA, rS, rB` = rS & !rB. 64-bit !rB poisons. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0xFFFF_FFFF; // rS + ctx.gpr[4] = 0x000F; // rB (low bits to clear) + // andcx r5, r3, r4 (XO=60) + let raw = (31u32 << 26) | (3 << 21) | (5 << 16) | (4 << 11) | (60 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFF0); + } + + #[test] + fn subfex_clean_inputs_no_upper_bits() { + // PPCBUG-008: 32-bit ABI. RT = !RA + RB + CA. RA=5, RB=10, CA=1 + // → !5u32 = 0xFFFFFFFA, +10 = 0x100000004, +1 = 0x100000005, low32 = 5. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 5; + ctx.gpr[4] = 10; + ctx.xer_ca = 1; + // subfex r5, r3, r4 (XO=136) + let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (136 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[5], 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] + fn lha_negative_halfword_zero_extends_upper() { + // PPCBUG-095: memory 0x8000 must yield gpr[rD] = 0x00000000_FFFF8000. + let mut ctx = PpcContext::new(); + let mem = TestMem::new(); + mem.write_u16(0x100, 0x8000); + ctx.gpr[3] = 0x100; + // lha r5, 0(r3): opcode 42 + let raw = (42u32 << 26) | (5 << 21) | (3 << 16) | 0; + write_instr(&mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_8000u64); + } + + #[test] + fn lhaux_negative_halfword_clean_writeback() { + // PPCBUG-098: indexed update form. Memory 0xFFFF → rD = 0x00000000_FFFFFFFF; + // rA must update to the EA. + let mut ctx = PpcContext::new(); + let mem = TestMem::new(); + mem.write_u16(0x200, 0xFFFF); + ctx.gpr[3] = 0x100; // ra + ctx.gpr[4] = 0x100; // rb + // lhaux r5, r3, r4 (XO=375) + let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (375 << 1); + write_instr(&mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_FFFF_FFFFu64); + assert_eq!(ctx.gpr[3], 0x200, "rA updated to EA"); + } + + #[test] + fn lwa_high_bit_set_zero_extends_upper() { + // PPCBUG-105: memory 0x80000000 must yield rD = 0x00000000_80000000 + // under 32-bit ABI (no sign extension to bits 32-63). + let mut ctx = PpcContext::new(); + let mem = TestMem::new(); + mem.write_u32(0x100, 0x8000_0000); + ctx.gpr[3] = 0x100; + // lwa r5, 0(r3): opcode 58, XO=2 (DS-form, ds=0) + let raw = (58u32 << 26) | (5 << 21) | (3 << 16) | 2; + write_instr(&mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mem); + assert_eq!(ctx.gpr[5], 0x0000_0000_8000_0000u64); + } + + #[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 + // 0x00000000_FFFFFFFF, not 0xFFFFFFFF_FFFFFFFF. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // addi r3, r0, -1: opcode 14, simm16 = 0xFFFF + let raw = (14u32 << 26) | (3 << 21) | (0 << 16) | 0xFFFF; + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[3], 0x0000_0000_FFFF_FFFFu64); + } + + #[test] + fn addic_carry_uses_32bit_compare() { + // PPCBUG-002: addic ra=0xFFFFFFFF_00000001, simm=-1 (0xFFFF). + // 32-bit: 0x00000001 + 0xFFFFFFFF = 0x00000000 with CA=1. + // 64-bit (buggy): result < ra → since 64-bit ra has high bits set, + // the buggy form would compare against the polluted u64 and could + // give wrong CA. Truncated form ignores upper 32 bits. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0xFFFFFFFF_00000001u64; + // addic r4, r3, -1: opcode 12 + let raw = (12u32 << 26) | (4 << 21) | (3 << 16) | 0xFFFF; + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + // Result low 32: 0x00000001 + 0xFFFFFFFF = 0x00000000 with carry. + assert_eq!(ctx.gpr[4], 0); + assert_eq!(ctx.xer_ca, 1, "32-bit compare must see CA=1"); + } + + #[test] + fn mulli_overflow_wraps_to_32() { + // PPCBUG-004: mulli must truncate to 32 bits even when the upper 32 bits + // of RA are polluted (e.g. by upstream bugs). Pre-fix: ra = u64::MAX as + // i64 = -1, * 2 = -2, written to GPR as `0xFFFFFFFF_FFFFFFFE`. Post-fix: + // truncated to `0xFFFFFFFE`. Discriminating regression test. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = u64::MAX; + // mulli r4, r3, 2: opcode 7 + let raw = (7u32 << 26) | (4 << 21) | (3 << 16) | 2; + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[4], 0xFFFF_FFFEu64, "low 32 bits = -2 in i32; upper 32 zero"); + } + + #[test] + fn subficx_neg_simm_zero_extends() { + // PPCBUG-005: subfic r4, r3, -1 with r3=5: imm-ra = 0xFFFFFFFF - 5 = 0xFFFFFFFA. + // Buggy form: imm sign-extended to u64 0xFFFFFFFFFFFFFFFF - 5 = poisoned. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 5; + // subfic r4, r3, -1: opcode 8, simm = 0xFFFF + let raw = (8u32 << 26) | (4 << 21) | (3 << 16) | 0xFFFF; + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.gpr[4], 0x0000_0000_FFFF_FFFAu64); + assert_eq!(ctx.xer_ca, 1, "0xFFFFFFFF >= 5 → CA=1"); + } + + #[test] + fn subfcx_addis_incident_case() { + // PPCBUG-007: regression for the exact case that revealed the addis bug. + // After P1's addis fix this works coincidentally; P4 batch 3 makes + // subfcx itself robust to 64-bit GPR pollution. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + // ra polluted in upper 32 bits, low 32 = 0x828F3F98 + ctx.gpr[3] = 0xFFFF_FFFF_828F_3F98u64; + // rb clean low 32 = 0x828F3F68 + ctx.gpr[4] = 0x0000_0000_828F_3F68u64; + // subfcx r5, r3, r4 (XO=8): result = rb - ra = 0xFFFFFFD0 (low 32) + let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (4 << 11) | (8 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + // 32-bit unsigned: 0x828F3F68 < 0x828F3F98 → CA=0 + assert_eq!(ctx.xer_ca, 0, "32-bit unsigned: rb < ra → CA=0"); + // result = 0x828F3F68 - 0x828F3F98 = 0xFFFFFFD0 (low 32, upper 32 zero) + assert_eq!(ctx.gpr[5], 0xFFFF_FFD0u64); + } + + #[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 + // (because !u32::MAX = 0). The buggy code's `!ra != 0` predicate + // on u64 was always true (because !u64-cast-of-u32::MAX has high + // bits flipped non-zero), wrongly setting CA=1. + let mut ctx = PpcContext::new(); + let mut mem = TestMem::new(); + ctx.gpr[3] = 0xFFFF_FFFFu64; + ctx.xer_ca = 0; + // subfmex r5, r3 (XO=232) + let raw = (31u32 << 26) | (5 << 21) | (3 << 16) | (232 << 1); + write_instr(&mut mem, 0, raw); + ctx.pc = 0; + step(&mut ctx, &mut mem); + assert_eq!(ctx.xer_ca, 0, "RA=u32::MAX, CA=0 → !RA32==0, CA=0"); } // ---------- Phase 2 fixes: trap TO-field ---------- @@ -6086,7 +6562,8 @@ mod tests { ctx.xer_ca = 0; step(&mut ctx, &mem); assert_eq!(ctx.xer_ca, 0, "ra=0, ca=0 should produce CA=0"); - assert_eq!(ctx.gpr[3], u64::MAX, "result = !0 + 0 = u64::MAX"); + // PPCBUG-018: 32-bit ABI. !0u32 + 0 = u32::MAX, with upper 32 bits zero. + assert_eq!(ctx.gpr[3], 0xFFFF_FFFFu64, "result = !0u32 + 0 = u32::MAX"); } // Case 3: ra=1, ca=0 → CA=0 (old buggy code reported CA=1) { @@ -6098,21 +6575,20 @@ mod tests { ctx.xer_ca = 0; step(&mut ctx, &mem); assert_eq!(ctx.xer_ca, 0, "ra=1, ca=0 should produce CA=0"); - assert_eq!(ctx.gpr[3], u64::MAX - 1, "result = !1 + 0 = u64::MAX - 1"); + // PPCBUG-018: 32-bit ABI. !1u32 + 0 = u32::MAX - 1, with upper 32 bits zero. + assert_eq!(ctx.gpr[3], 0xFFFF_FFFEu64, "result = !1u32 + 0 = u32::MAX - 1"); } - // Case 4: ra=u64::MAX, ca=0 → CA=0 (old buggy code reported CA=1 - // because !ra == 0 only here, which the buggy `!ra != 0` predicate - // happened to handle right; flip ca=1 to exercise the other arm) + // Case 4: ra=u32::MAX, ca=1 → CA=0; result = !u32::MAX + 1 = 1. { let mut ctx = PpcContext::new(); let mem = TestMem::new(); write_instr(&mem, 0, raw); ctx.pc = 0; - ctx.gpr[4] = u64::MAX; + ctx.gpr[4] = 0xFFFF_FFFFu64; ctx.xer_ca = 1; step(&mut ctx, &mem); - assert_eq!(ctx.xer_ca, 0, "ra=u64::MAX, ca=1 should produce CA=0"); - assert_eq!(ctx.gpr[3], 1, "result = !u64::MAX + 1 = 1"); + assert_eq!(ctx.xer_ca, 0, "ra=u32::MAX, ca=1 should produce CA=0"); + assert_eq!(ctx.gpr[3], 1, "result = !u32::MAX + 1 = 1"); } }