From 0b2212e1e89a3abce083bf66fefcd439b9d21300 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Thu, 9 Jul 2026 01:30:18 +0000 Subject: [PATCH] proto: bound powi test drift by relative tolerance, not ULP The previous portability fix bounded the powi test's drift from std::powi at an absolute ULP count, but Windows/MSVC drift grows with the exponent (3 ULP by exp=14), so no fixed ULP bound is portable. Replace the ULP oracle with a loose relative-tolerance sanity sweep (1e-11), which any sane libm clears by ~1000x regardless of exponent while still catching a grossly wrong impl. Add a golden-bit pin for the exp=14 case that exposed the growth. The powi function is unchanged; the golden-bit determinism pins already passed on Windows. --- src/proto/math.rs | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/proto/math.rs b/src/proto/math.rs index 86e7fb2..a8c761d 100644 --- a/src/proto/math.rs +++ b/src/proto/math.rs @@ -13,8 +13,9 @@ /// This is a strict improvement over `f64::powi`, which is not portably /// bit-stable: Linux and macOS lower it to compiler-rt's `__powidf2` (whose /// multiply-then-square order this mirrors, so we match them exactly), but -/// Windows/MSVC rounds ~1 ULP differently. The `powi_deterministic_and_close` -/// test pins our output to golden bits and bounds the drift from `std` at 2 ULP. +/// Windows/MSVC rounds differently (and by more as the exponent grows). The +/// `powi_deterministic_and_close` test pins our output to golden bits and bounds +/// the relative drift from `std`. pub(crate) fn powi(base: f64, exp: u32) -> f64 { let mut result = 1.0_f64; let mut b = base; @@ -53,6 +54,7 @@ mod tests { ((0.1, 3), 4562254508917369341), ((0.5493, 4), 4591224636877479401), ((0.5508, 4), 4591296588123960286), + ((0.5469, 14), 4552032634403898040), ]; for ((base, exp), bits) in golden { assert_eq!( @@ -62,23 +64,34 @@ mod tests { ); } - // Correctness: stay within a couple ULP of `std::powi` across every base - // and exponent the protocol math reaches — a broad sanity sweep backing - // the exact golden pins above. `f64::powi` is not portably bit-stable - // (Windows/MSVC drifts from Linux/macOS by ~1 ULP), so we bound the drift - // rather than assert exact equality; 2 ULP leaves margin over the - // observed 1-ULP delta while still catching a grossly wrong impl. + // Correctness: stay within a loose relative tolerance of `std::powi` + // across every base and exponent the protocol math reaches — a broad + // sanity sweep backing the exact golden pins above. `f64::powi` is not + // portably bit-stable: Windows/MSVC drifts from Linux/macOS, and that + // drift grows with the exponent (3 ULP already by exp=14), so an absolute + // ULP bound is not portable. Relative error of any sane libm stays around + // 1e-15 regardless of exponent, so 1e-11 clears the drift by ~1000x while + // still catching a grossly wrong impl (multiply-order subtleties are + // caught by the golden pins, not here). let bases = [ 0.0, 1.0, 0.5, 0.5469, 0.5493, 0.5508, 0.9999, 1.5, 2.0, 3.7, 0.1, 1e-3, ]; for &base in &bases { for exp in 0u32..=64 { - let ours = powi(base, exp).to_bits() as i64; - let std = base.powi(exp as i32).to_bits() as i64; - assert!( - (ours - std).abs() <= 2, - "powi drift >2 ULP at base={base}, exp={exp}: ours={ours}, std={std}" - ); + let ours = powi(base, exp); + let std = base.powi(exp as i32); + if std == 0.0 { + assert_eq!( + ours, 0.0, + "powi zero-case mismatch at base={base}, exp={exp}" + ); + } else { + let rel = ((ours - std) / std).abs(); + assert!( + rel <= 1e-11, + "powi relative drift at base={base}, exp={exp}: ours={ours:e}, std={std:e}, rel={rel:e}" + ); + } } } }