fix(cpu): PPCBUG-221+227 round_to_i64 + PPCBUG-432 vrfin round-to-even

Phase 5 batch 1 (5a): round-to-int correctness.

PPCBUG-221+227 (coupled): round_to_i64 NearestEven tie-breaking used
`(diff - 0.5).abs() < f64::EPSILON` to detect half-integers, but for
|v| > 2^52 every f64 value is an exact integer (v.trunc() == v), giving
diff == 0. The buggy check fell through to v.round() (round-half-away-
from-zero), giving wrong results for large odd half-integers. Replaced
with a fractional-part-only check that's exact for |v| <= 2^52 and
degenerates to truncation above.

PPCBUG-432: vrfin/vrfin128 used Rust's `f32::round()` which is round-
half-away-from-zero. ISA requires round-to-nearest-even (banker's
rounding). Implemented inline.

PPCBUG-201 (FPSCR.RN for double arithmetic) deferred — requires
MXCSR-set/restore wrappers around 10+ FPU arms; will land in a focused
sub-batch after the remaining 5a-5f fixes.

Tests:
- round_to_i64_nearest_even_on_tie: extended with 0.5, 1.5, -0.5, -1.5.
- round_to_i64_non_tie_cases: 0.4/0.6 (non-tie sanity).
- round_to_i32_nearest_even_on_tie: PPCBUG-227 coverage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-02 12:13:08 +02:00
parent 5c45108249
commit f6a444b9d1
2 changed files with 54 additions and 9 deletions

View File

@@ -220,15 +220,22 @@ pub fn round_to_single(ctx: &PpcContext, v: f64) -> f64 {
pub fn round_to_i64(ctx: &PpcContext, v: f64) -> i64 {
match rounding_mode(ctx) {
RoundingMode::NearestEven => {
// Round-half-to-even (banker's rounding).
let r = v.round();
// Rust's f64::round is round-half-away-from-zero. Correct ties to even:
let diff = (v - v.trunc()).abs();
if (diff - 0.5).abs() < f64::EPSILON {
let floor = v.floor();
if (floor as i64) & 1 == 0 { floor as i64 } else { v.ceil() as i64 }
// PPCBUG-221: round-half-to-even (banker's rounding). The previous
// tie-detection used `(diff - 0.5).abs() < f64::EPSILON` which
// breaks for |v| > 2^52 (where v.trunc() == v exactly, giving diff
// == 0). Use a fractional-part-only check that's exact for
// |v| <= 2^52 and degenerates correctly above.
let t = v.trunc();
let frac = v - t;
let fa = frac.abs();
if fa > 0.5 {
t as i64 + if v >= 0.0 { 1 } else { -1 }
} else if fa < 0.5 {
t as i64
} else {
r as i64
// Exact 0.5 tie — round to even.
let fi = t as i64;
if fi & 1 == 0 { fi } else { fi + if v >= 0.0 { 1 } else { -1 } }
}
}
RoundingMode::TowardZero => v.trunc() as i64,
@@ -355,11 +362,35 @@ mod tests {
#[test]
fn round_to_i64_nearest_even_on_tie() {
let c = ctx();
assert_eq!(round_to_i64(&c, 0.5_f64), 0);
assert_eq!(round_to_i64(&c, 1.5_f64), 2);
assert_eq!(round_to_i64(&c, 2.5_f64), 2);
assert_eq!(round_to_i64(&c, 3.5_f64), 4);
assert_eq!(round_to_i64(&c, -0.5_f64), 0);
assert_eq!(round_to_i64(&c, -1.5_f64), -2);
assert_eq!(round_to_i64(&c, -2.5_f64), -2);
}
#[test]
fn round_to_i64_non_tie_cases() {
// PPCBUG-221 regression: non-tie fractions must round to nearest.
let c = ctx();
assert_eq!(round_to_i64(&c, 0.4_f64), 0);
assert_eq!(round_to_i64(&c, 0.6_f64), 1);
assert_eq!(round_to_i64(&c, -0.4_f64), 0);
assert_eq!(round_to_i64(&c, -0.6_f64), -1);
}
#[test]
fn round_to_i32_nearest_even_on_tie() {
// PPCBUG-227: round_to_i32 inherits round_to_i64's tie semantics.
let c = ctx();
assert_eq!(round_to_i32(&c, 0.5_f64), 0);
assert_eq!(round_to_i32(&c, 1.5_f64), 2);
assert_eq!(round_to_i32(&c, 2.5_f64), 2);
assert_eq!(round_to_i32(&c, -1.5_f64), -2);
}
#[test]
fn check_invalid_add_detects_inf_minus_inf() {
let mut c = ctx();