Files
xenia-rs/crates/xenia-cpu/src/overflow.rs
MechaCat02 99e7814836 test(cpu): PPCBUG-022 verify mulld_ov INT_MIN*-1 + auto-resolved markers
Phase 6 batch 4 — overflow/cleanup verification.

- PPCBUG-022 mulld_ov INT_MIN * -1: the audit-claimed missing edge case
  is actually handled by `i64::checked_mul()` (returns None when the
  result would be -i64::MIN = i64::MAX+1, which doesn't fit). New
  regression tests in overflow.rs confirm: INT_MIN * -1 overflows;
  INT_MIN * 1 doesn't; (INT_MIN+1) * -1 = INT_MAX, no overflow.
  Audit's claim was incorrect; documented by the new tests.
- PPCBUG-021 (overflow.rs OE checks at bit 63): largely auto-resolved
  by P4 batch 6 (16993bb), which switched all 32-bit ABI ops to inline
  `true_sum != (result32 as i32) as i128`. Helpers like add_ov_64 are
  now only called from 64-bit ABI ops where bit-63 is correct.
- PPCBUG-027 (rlwimix upper-32 zeros): auto-resolved by P4 (rlwimix
  now writes via `as u32 as u64` truncation).
- PPCBUG-039 (cntlzdx 32-bit-ABI): wontfix per audit — only matters
  if a 32-bit-ABI binary emits cntlzd, which compilers don't.

Remaining low-impact items (PPCBUG-642 ISA-undefined fmt_bcctr decr,
PPCBUG-643/644 SIMM/D-form hex display, PPCBUG-367/368 vupkhpx/vpkpx
channel ordering, PPCBUG-487/495 vsum operand naming, PPCBUG-515/516
lvebx/lvsr documentation, PPCBUG-601 decode_op6 invariant doc) are
left for a P9 or follow-up batch — they're cosmetic/test-coverage
items rather than correctness bugs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 13:51:43 +02:00

179 lines
5.1 KiB
Rust

//! OE / XER[OV] / XER[SO] handling for integer arithmetic.
//!
//! PPC integer ops with the OE bit set update XER[OV] (overflow) and sticky-set
//! XER[SO]. When OE is clear the instruction leaves XER untouched. Signed
//! overflow is predicated on the operation width and operand signs per the
//! PowerISA pseudocode. For 32-bit-word operations (`addw`, `mullw`, `divw`,
//! `neg`, etc. — on PPC these all have `w` in the mnemonic in spec
//! descriptions even when the assembler spells them without) the predicate
//! uses the low 32 bits. For 64-bit operations (`add`, `mulld`, `divd`) the
//! predicate uses the full 64 bits.
use crate::context::PpcContext;
#[inline]
pub fn apply(ctx: &mut PpcContext, overflowed: bool) {
if overflowed {
ctx.xer_ov = 1;
ctx.xer_so = 1;
} else {
ctx.xer_ov = 0;
}
}
/// Signed addition overflow at width-64 (plain `add`, `addc`, `subf`, `subfc`).
///
/// Predicate: same-sign inputs with opposite-sign result.
/// For sub callers, rewrite as `a + b'` first (see `_sub`).
#[inline]
pub fn add_ov_64(a: u64, b: u64, result: u64) -> bool {
((!(a ^ b)) & (a ^ result)) >> 63 != 0
}
/// Universal signed-overflow predicate for 64-bit arithmetic.
///
/// Caller computes the mathematical (infinite-precision) signed sum as i128,
/// plus the stored 64-bit result. Overflow iff the two disagree — i.e. the
/// true value doesn't fit in i64.
///
/// Use this for multi-term chains (`adde`, `addme`, `addze`, `subfe`, `subfme`,
/// `subfze`) where the carry-in makes the bit-predicate above awkward.
#[inline]
pub fn sum_overflow_64(true_sum: i128, result: u64) -> bool {
true_sum != (result as i64) as i128
}
/// Signed subtraction: RT = b - a. Overflow iff opposite-sign inputs with
/// result sign != b's sign. Equivalently, reduce to addition with `!a + 1`.
#[inline]
pub fn sub_ov_64(a: u64, b: u64, result: u64) -> bool {
((a ^ b) & (b ^ result)) >> 63 != 0
}
/// Signed `addc`/`adde` chain overflow. Same rule as `add_ov_64` — the carry
/// in doesn't alter the sign predicate directly because it's already folded
/// into the stored result.
#[inline]
pub fn adde_ov_64(a: u64, b: u64, result: u64) -> bool {
add_ov_64(a, b, result)
}
/// Signed 32-bit multiply overflow (`mullwo`): result fits in 32 bits signed
/// iff bit 32 equals bits 33..63 of the 64-bit product.
#[inline]
pub fn mullw_ov(product: i64) -> bool {
let lo = product as i32 as i64;
lo != product
}
/// Signed 64-bit multiply overflow (`mulldo`). Detected via checked_mul.
#[inline]
pub fn mulld_ov(a: i64, b: i64) -> bool {
a.checked_mul(b).is_none()
}
/// `divwo` / `divwuo` / `divdo` / `divduo` raise OV in two cases:
/// * divisor is zero, or
/// * signed division of `INT_MIN / -1` (quotient doesn't fit).
#[inline]
pub fn divw_ov_signed(ra: i32, rb: i32) -> bool {
rb == 0 || (ra == i32::MIN && rb == -1)
}
#[inline]
pub fn divw_ov_unsigned(rb: u32) -> bool {
rb == 0
}
#[inline]
pub fn divd_ov_signed(ra: i64, rb: i64) -> bool {
rb == 0 || (ra == i64::MIN && rb == -1)
}
#[inline]
pub fn divd_ov_unsigned(rb: u64) -> bool {
rb == 0
}
/// `negx`: RT = -(RA). Overflow only when RA = INT_MIN (the negation doesn't fit).
#[inline]
pub fn neg_ov_64(ra: u64) -> bool {
ra == 0x8000_0000_0000_0000
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_no_overflow() {
assert!(!add_ov_64(1, 2, 3));
assert!(!add_ov_64(u64::MAX, 0, u64::MAX));
}
#[test]
fn add_positive_overflow() {
// INT64_MAX + 1 = INT64_MIN — signed overflow
let a = i64::MAX as u64;
let b = 1u64;
let r = a.wrapping_add(b);
assert!(add_ov_64(a, b, r));
}
#[test]
fn add_negative_overflow() {
// INT64_MIN + -1 = INT64_MAX — signed overflow
let a = i64::MIN as u64;
let b = (-1i64) as u64;
let r = a.wrapping_add(b);
assert!(add_ov_64(a, b, r));
}
#[test]
fn sub_overflow_min_minus_pos() {
// INT64_MIN - 1 overflows
let b = i64::MIN as u64;
let a = 1u64;
let r = b.wrapping_sub(a);
assert!(sub_ov_64(a, b, r));
}
#[test]
fn sub_no_overflow() {
let b = 5u64;
let a = 2u64;
let r = b.wrapping_sub(a);
assert!(!sub_ov_64(a, b, r));
}
#[test]
fn mullw_fits_32_bits() {
assert!(!mullw_ov((i32::MAX as i64) * 1));
assert!(!mullw_ov(-1i64));
}
#[test]
fn mullw_overflows_32_bits() {
let p = (i32::MAX as i64) * 2;
assert!(mullw_ov(p));
}
#[test]
fn mulld_overflows() {
assert!(mulld_ov(i64::MAX, 2));
assert!(!mulld_ov(i64::MAX, 1));
// PPCBUG-022: INT_MIN * -1 overflows (=-INT_MIN > INT_MAX).
// checked_mul correctly returns None for this case.
assert!(mulld_ov(i64::MIN, -1), "INT_MIN * -1 overflows i64");
assert!(!mulld_ov(i64::MIN, 1));
assert!(!mulld_ov(i64::MIN + 1, -1), "INT_MIN+1 * -1 = INT_MAX, no overflow");
}
#[test]
fn neg_ov_only_at_min() {
assert!(neg_ov_64(i64::MIN as u64));
assert!(!neg_ov_64(0));
assert!(!neg_ov_64(1));
}
}