Skip to content

Commit 3041856

Browse files
fix(math): raise FE_DIVBYZERO/FE_INVALID for log/tgamma poles+domain (glibc parity)
Found via FP-exception differential probe: log(0)/log(-1)/tgamma(neg-integer) returned the correct VALUE but raised NO floating-point exception, while glibc raises FE_DIVBYZERO (log pole) / FE_INVALID (log domain, tgamma pole). The libm-backed special-input paths special-case the result and bypass the hardware op that would set the flag. Programs using feenableexcept/fetestexcept (e.g. scientific NaN-trapping) depend on these. Fix: re-raise the exception on the COLD special-input path via a safe hardware op (force_eval-style, the hot path is unaffected): log(±0) -> FE_DIVBYZERO via black_box(-1.0)/black_box(0.0) (= -inf) log(x<0) -> FE_INVALID via black_box(0.0)/black_box(0.0) (= NaN) tgamma(negative integer) -> FE_INVALID likewise sqrt/log2/log10/exp/pow/acos/asin/acosh/atanh/fmod were already correct. Gate: conformance_diff_fp_exceptions checks both the raised flags (exact) and the returned value (<=4 ULP / NaN-inf-category) vs host glibc across domain/pole/ overflow inputs. All 155 math lib tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4806dd0 commit 3041856

3 files changed

Lines changed: 84 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#![cfg(target_os = "linux")]
2+
#![allow(unsafe_code)] // live host-glibc math + fetestexcept oracle
3+
//! C99/IEEE floating-point exception-flag parity vs glibc for math functions on
4+
//! domain/pole/overflow inputs. libm software impls return the correct VALUE but
5+
//! often omit the flag glibc raises; fl re-raises FE_DIVBYZERO (log(±0)),
6+
//! FE_INVALID (log(x<0), tgamma(neg-int)) on the cold special path. Programs
7+
//! using feenableexcept / fetestexcept (scientific NaN-trapping) depend on this.
8+
//! Checks both the raised flags AND the returned value.
9+
10+
use frankenlibc_core::math as fl;
11+
use std::ffi::c_int;
12+
unsafe extern "C" {
13+
fn feclearexcept(e: c_int) -> c_int;
14+
fn fetestexcept(e: c_int) -> c_int;
15+
fn sqrt(x: f64) -> f64; fn log(x: f64) -> f64; fn log2(x: f64) -> f64; fn log10(x: f64) -> f64;
16+
fn exp(x: f64) -> f64; fn pow(x: f64, y: f64) -> f64; fn acos(x: f64) -> f64; fn asin(x: f64) -> f64;
17+
fn acosh(x: f64) -> f64; fn atanh(x: f64) -> f64; fn tgamma(x: f64) -> f64; fn fmod(x: f64, y: f64) -> f64;
18+
}
19+
const HARD: c_int = 0x1D; // INVALID|DIVBYZERO|OVERFLOW|UNDERFLOW (drop noisy INEXACT)
20+
fn key(x: f64) -> i64 { let b = x.to_bits() as i64; if b < 0 { i64::MIN - b } else { b } }
21+
// Value parity: NaN<->NaN, exact at inf/zero boundaries, <=4 ULP for finite
22+
// transcendental results (the math conformance contract; exception flags stay exact).
23+
fn beq(a: f64, b: f64) -> bool {
24+
if a.is_nan() && b.is_nan() { return true; }
25+
if a.is_nan() != b.is_nan() { return false; }
26+
if a.is_infinite() || b.is_infinite() || a == 0.0 || b == 0.0 { return a.to_bits() == b.to_bits(); }
27+
(key(a).wrapping_sub(key(b))).unsigned_abs() <= 4
28+
}
29+
30+
#[test]
31+
fn fp_exception_and_value_parity_vs_glibc() {
32+
let mut div = Vec::new();
33+
macro_rules! chk { ($lbl:literal, $flf:expr, $gf:expr) => {{
34+
unsafe { feclearexcept(HARD); }
35+
let fv = $flf;
36+
let ff = unsafe { fetestexcept(HARD) };
37+
unsafe { feclearexcept(HARD); }
38+
let gv = unsafe { $gf };
39+
let gf = unsafe { fetestexcept(HARD) };
40+
if (ff & HARD) != (gf & HARD) {
41+
div.push(format!("{} flags: fl={:#x} glibc={:#x}", $lbl, ff & HARD, gf & HARD));
42+
}
43+
if !beq(fv, gv) {
44+
div.push(format!("{} value: fl={:?} glibc={:?}", $lbl, fv, gv));
45+
}
46+
}}; }
47+
chk!("sqrt(-1)", fl::sqrt(-1.0), sqrt(-1.0));
48+
chk!("log(0)", fl::log(0.0), log(0.0));
49+
chk!("log(-0)", fl::log(-0.0), log(-0.0));
50+
chk!("log(-1)", fl::log(-1.0), log(-1.0));
51+
chk!("log(-inf)", fl::log(f64::NEG_INFINITY), log(f64::NEG_INFINITY));
52+
chk!("log2(0)", fl::log2(0.0), log2(0.0));
53+
chk!("log10(-2)", fl::log10(-2.0), log10(-2.0));
54+
chk!("exp(1000)", fl::exp(1000.0), exp(1000.0));
55+
chk!("pow(0,-1)", fl::pow(0.0,-1.0), pow(0.0,-1.0));
56+
chk!("pow(-1,0.5)", fl::pow(-1.0,0.5), pow(-1.0,0.5));
57+
chk!("acos(2)", fl::acos(2.0), acos(2.0));
58+
chk!("asin(-2)", fl::asin(-2.0), asin(-2.0));
59+
chk!("acosh(0.5)", fl::acosh(0.5), acosh(0.5));
60+
chk!("atanh(2)", fl::atanh(2.0), atanh(2.0));
61+
chk!("tgamma(0)", fl::tgamma(0.0), tgamma(0.0));
62+
chk!("tgamma(-1)", fl::tgamma(-1.0), tgamma(-1.0));
63+
chk!("tgamma(-5)", fl::tgamma(-5.0), tgamma(-5.0));
64+
chk!("tgamma(-2.5)", fl::tgamma(-2.5), tgamma(-2.5));
65+
chk!("fmod(1,0)", fl::fmod(1.0,0.0), fmod(1.0,0.0));
66+
assert!(div.is_empty(), "fp-exception/value divergences vs glibc:\n {}", div.join("\n "));
67+
}

crates/frankenlibc-core/src/math/exp.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ pub fn log(x: f64) -> f64 {
4242
if x.is_normal() && x > 0.0 {
4343
return log2_kernel(x) * std::f64::consts::LN_2;
4444
}
45+
// libm::log returns the correct value for special inputs but does NOT raise
46+
// the C99/IEEE floating-point exception glibc raises. Re-raise it via a
47+
// hardware op on this cold special-input path (safe; the hot path returned
48+
// above): log(±0) = -inf -> FE_DIVBYZERO (pole); log(x<0) = NaN ->
49+
// FE_INVALID (domain). NaN and +inf inputs raise nothing, matching glibc.
50+
if x == 0.0 {
51+
let _ = core::hint::black_box(core::hint::black_box(-1.0_f64) / core::hint::black_box(0.0_f64));
52+
} else if x < 0.0 {
53+
let _ = core::hint::black_box(core::hint::black_box(0.0_f64) / core::hint::black_box(0.0_f64));
54+
}
4555
libm::log(x)
4656
}
4757

crates/frankenlibc-core/src/math/special.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,13 @@ pub fn tgamma(x: f64) -> f64 {
189189
if x > 0.0 && x <= 13.0 {
190190
tgamma_reduced(x)
191191
} else {
192+
// Negative-integer poles: glibc raises FE_INVALID (result NaN); libm
193+
// returns NaN without the flag. Re-raise on this cold path via a hardware
194+
// 0/0 (NaN + FE_INVALID). (tgamma(0) is handled by libm with FE_DIVBYZERO
195+
// already; positive/large/non-integer args raise nothing here.)
196+
if x < 0.0 && x.is_finite() && x == x.floor() {
197+
let _ = core::hint::black_box(core::hint::black_box(0.0_f64) / core::hint::black_box(0.0_f64));
198+
}
192199
libm::tgamma(x)
193200
}
194201
}

0 commit comments

Comments
 (0)