Skip to content

Commit 17e2a32

Browse files
fix(math): C23 sinpi/cospi/tanpi exact at integer/half-integer args (glibc parity)
sinpi/cospi/tanpi (+ f32) were implemented naively as sin(x*PI)/cos(x*PI)/ tan(x*PI), violating the entire C23 contract for the pi-scaled trig functions. A differential probe vs host glibc found 13 divergences: - sinpi(1.0) returned 1.2e-16 instead of EXACT 0; sinpi(1e20) returned -0.39 (catastrophic — argument reduction lost all precision) instead of 0. - cospi(0.5)/cospi(1.5)/... returned ~1e-16 instead of EXACT +0. - tanpi(0.5) returned a huge finite (~1.6e16) instead of +inf + FE_DIVBYZERO; tanpi(1.0) lost its -0 sign. Reimplemented via the exact identity f(n+r) with n=round(x), r=x-n in [-0.5,0.5] (exact by Sterbenz): sinpi(x)=(-1)^n*sin(PI*r), cospi(x)= (-1)^n*cos(PI*r), tanpi=sinpi/cospi (the (-1)^n cancels; the half-integer pole becomes ±1/±0 which auto-raises FE_DIVBYZERO → ±inf). Integer and half-integer args, huge args (>=2^53 are even integers), inf (→NaN+INVALID), and NaN are all special-cased for exactness; irrational args use the in-tree sin/cos kernels (<=4 ULP vs glibc). acospi/asinpi/atanpi/atan2pi were already correct (plain f(x)/PI) and are unchanged. New gate conformance_diff_pi_trig: live differential (glibc via dlsym to bypass fl's no_mangle interposition; hardware FP flags) over 41 f64 + 17 f32 inputs — specials exact, irrational <=4 ULP, exception flags exact. 116 math_abi unit tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 44ac9c7 commit 17e2a32

2 files changed

Lines changed: 270 additions & 6 deletions

File tree

crates/frankenlibc-abi/src/math_abi.rs

Lines changed: 126 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3578,12 +3578,68 @@ pub unsafe extern "C" fn atan2pif128(x: f64, y: f64) -> f64 {
35783578
unsafe { atan2pi(x, y) }
35793579
}
35803580
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
3581+
/// Re-raise FE_INVALID on the cold pi-function domain path (x = ±inf).
3582+
#[inline]
3583+
fn pi_fn_raise_invalid_f64() {
3584+
let _ = core::hint::black_box(core::hint::black_box(0.0_f64) / core::hint::black_box(0.0_f64));
3585+
}
3586+
#[inline]
3587+
fn pi_fn_raise_invalid_f32() {
3588+
let _ = core::hint::black_box(core::hint::black_box(0.0_f32) / core::hint::black_box(0.0_f32));
3589+
}
3590+
3591+
// C23 pi-scaled trig: sinpi(x)=sin(pi*x), cospi(x)=cos(pi*x), tanpi(x)=tan(pi*x),
3592+
// computed via the exact identity f(n+r) with n=round(x), r=x-n in [-0.5,0.5]
3593+
// (exact by Sterbenz for |x|<2^53). This yields EXACT results at integer and
3594+
// half-integer arguments (sinpi(1)=+0, cospi(0.5)=+0, tanpi(0.5)=+inf) and
3595+
// stays correct for huge arguments where the naive sin(x*PI) loses all
3596+
// precision. |x|>=2^53 is always an even integer: sinpi=±0, cospi=1, tanpi=±0.
35813597
pub unsafe extern "C" fn cospi(x: f64) -> f64 {
3582-
unsafe { cos(x * std::f64::consts::PI) }
3598+
if x.is_nan() {
3599+
return x;
3600+
}
3601+
if x.is_infinite() {
3602+
pi_fn_raise_invalid_f64();
3603+
return f64::NAN;
3604+
}
3605+
if x.abs() >= 9007199254740992.0 {
3606+
return 1.0; // even integer → cos(pi*even)=+1
3607+
}
3608+
let n = x.round();
3609+
let r = x - n;
3610+
let n_odd = (n as i64) & 1 != 0;
3611+
if r == 0.0 {
3612+
return if n_odd { -1.0 } else { 1.0 };
3613+
}
3614+
if r == 0.5 || r == -0.5 {
3615+
return 0.0; // cos at odd multiple of pi/2 is +0
3616+
}
3617+
let c = frankenlibc_core::math::cos(r * std::f64::consts::PI);
3618+
if n_odd { -c } else { c }
35833619
}
35843620
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
35853621
pub unsafe extern "C" fn cospif(x: f32) -> f32 {
3586-
unsafe { cosf(x * std::f32::consts::PI) }
3622+
if x.is_nan() {
3623+
return x;
3624+
}
3625+
if x.is_infinite() {
3626+
pi_fn_raise_invalid_f32();
3627+
return f32::NAN;
3628+
}
3629+
if x.abs() >= 16777216.0 {
3630+
return 1.0;
3631+
}
3632+
let n = x.round();
3633+
let r = x - n;
3634+
let n_odd = (n as i64) & 1 != 0;
3635+
if r == 0.0 {
3636+
return if n_odd { -1.0 } else { 1.0 };
3637+
}
3638+
if r == 0.5 || r == -0.5 {
3639+
return 0.0;
3640+
}
3641+
let c = frankenlibc_core::math::cosf(r * std::f32::consts::PI);
3642+
if n_odd { -c } else { c }
35873643
}
35883644
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
35893645
pub unsafe extern "C" fn cospil(x: f64) -> f64 {
@@ -3611,11 +3667,54 @@ pub unsafe extern "C" fn cospif128(x: f64) -> f64 {
36113667
}
36123668
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
36133669
pub unsafe extern "C" fn sinpi(x: f64) -> f64 {
3614-
unsafe { sin(x * std::f64::consts::PI) }
3670+
if x.is_nan() {
3671+
return x;
3672+
}
3673+
if x.is_infinite() {
3674+
pi_fn_raise_invalid_f64();
3675+
return f64::NAN;
3676+
}
3677+
if x.abs() >= 9007199254740992.0 {
3678+
// even integer → sin(pi*even)=±0 with sign of x
3679+
return if x.is_sign_negative() { -0.0 } else { 0.0 };
3680+
}
3681+
let n = x.round();
3682+
let r = x - n;
3683+
let n_odd = (n as i64) & 1 != 0;
3684+
if r == 0.0 {
3685+
return if x.is_sign_negative() { -0.0 } else { 0.0 };
3686+
}
3687+
if r == 0.5 || r == -0.5 {
3688+
let m = if r > 0.0 { 1.0 } else { -1.0 };
3689+
return if n_odd { -m } else { m };
3690+
}
3691+
let s = frankenlibc_core::math::sin(r * std::f64::consts::PI);
3692+
if n_odd { -s } else { s }
36153693
}
36163694
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
36173695
pub unsafe extern "C" fn sinpif(x: f32) -> f32 {
3618-
unsafe { sinf(x * std::f32::consts::PI) }
3696+
if x.is_nan() {
3697+
return x;
3698+
}
3699+
if x.is_infinite() {
3700+
pi_fn_raise_invalid_f32();
3701+
return f32::NAN;
3702+
}
3703+
if x.abs() >= 16777216.0 {
3704+
return if x.is_sign_negative() { -0.0 } else { 0.0 };
3705+
}
3706+
let n = x.round();
3707+
let r = x - n;
3708+
let n_odd = (n as i64) & 1 != 0;
3709+
if r == 0.0 {
3710+
return if x.is_sign_negative() { -0.0 } else { 0.0 };
3711+
}
3712+
if r == 0.5 || r == -0.5 {
3713+
let m = if r > 0.0 { 1.0 } else { -1.0 };
3714+
return if n_odd { -m } else { m };
3715+
}
3716+
let s = frankenlibc_core::math::sinf(r * std::f32::consts::PI);
3717+
if n_odd { -s } else { s }
36193718
}
36203719
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
36213720
pub unsafe extern "C" fn sinpil(x: f64) -> f64 {
@@ -3643,11 +3742,32 @@ pub unsafe extern "C" fn sinpif128(x: f64) -> f64 {
36433742
}
36443743
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
36453744
pub unsafe extern "C" fn tanpi(x: f64) -> f64 {
3646-
unsafe { tan(x * std::f64::consts::PI) }
3745+
if x.is_nan() {
3746+
return x;
3747+
}
3748+
if x.is_infinite() {
3749+
pi_fn_raise_invalid_f64();
3750+
return f64::NAN;
3751+
}
3752+
// tanpi = sinpi/cospi: the (-1)^n factors cancel, the half-integer pole
3753+
// becomes ±1/±0 (auto-raising FE_DIVBYZERO → ±inf), and the integer zero
3754+
// becomes ±0/±1 with the correct sign.
3755+
let s = unsafe { sinpi(x) };
3756+
let c = unsafe { cospi(x) };
3757+
s / c
36473758
}
36483759
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
36493760
pub unsafe extern "C" fn tanpif(x: f32) -> f32 {
3650-
unsafe { tanf(x * std::f32::consts::PI) }
3761+
if x.is_nan() {
3762+
return x;
3763+
}
3764+
if x.is_infinite() {
3765+
pi_fn_raise_invalid_f32();
3766+
return f32::NAN;
3767+
}
3768+
let s = unsafe { sinpif(x) };
3769+
let c = unsafe { cospif(x) };
3770+
s / c
36513771
}
36523772
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
36533773
pub unsafe extern "C" fn tanpil(x: f64) -> f64 {
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//! Differential gate for the C23 pi-scaled trig functions sinpi/cospi/tanpi
2+
//! (+ f32) vs host glibc.
3+
//!
4+
//! These must produce EXACT results at integer and half-integer arguments
5+
//! (sinpi(n)=±0, cospi(n+0.5)=+0, cospi(n)=±1, tanpi(n+0.5)=±inf with
6+
//! FE_DIVBYZERO) and stay correct for huge arguments — properties the naive
7+
//! `sin(x*PI)` formulation violates. fl is called via Rust paths; glibc is
8+
//! reached through `dlsym` on libm.so.6 so the fn pointer bypasses fl's
9+
//! no_mangle interposition of the same symbol. FP exception flags are hardware
10+
//! (MXCSR), read directly with fetestexcept — no interposition concern.
11+
#![cfg(target_os = "linux")]
12+
#![allow(unsafe_code)]
13+
14+
use frankenlibc_abi::math_abi as fl;
15+
use std::ffi::{c_char, c_int, c_void};
16+
17+
const RTLD_NOW: c_int = 2;
18+
const HARD: c_int = 0x1D; // INVALID|DIVBYZERO|OVERFLOW|UNDERFLOW
19+
20+
unsafe extern "C" {
21+
fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
22+
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
23+
fn feclearexcept(e: c_int) -> c_int;
24+
fn fetestexcept(e: c_int) -> c_int;
25+
}
26+
27+
fn libm() -> *mut c_void {
28+
let h = unsafe { dlopen(c"libm.so.6".as_ptr(), RTLD_NOW) };
29+
assert!(!h.is_null(), "dlopen libm failed");
30+
h
31+
}
32+
fn sym(h: *mut c_void, name: &std::ffi::CStr) -> *mut c_void {
33+
let p = unsafe { dlsym(h, name.as_ptr()) };
34+
assert!(!p.is_null(), "missing libm symbol {name:?}");
35+
p
36+
}
37+
38+
fn ulp_ok_f64(a: f64, b: f64) -> bool {
39+
// b is the glibc reference. Exact bit match for non-finite / zero (sign
40+
// matters); <=4 ULP for finite nonzero (the math conformance contract).
41+
if b.is_nan() {
42+
return a.is_nan();
43+
}
44+
if !b.is_finite() || b == 0.0 {
45+
return a.to_bits() == b.to_bits();
46+
}
47+
if a.is_nan() || a.is_sign_negative() != b.is_sign_negative() {
48+
return false;
49+
}
50+
let ai = a.to_bits() as i64;
51+
let bi = b.to_bits() as i64;
52+
(ai - bi).unsigned_abs() <= 4
53+
}
54+
fn ulp_ok_f32(a: f32, b: f32) -> bool {
55+
if b.is_nan() {
56+
return a.is_nan();
57+
}
58+
if !b.is_finite() || b == 0.0 {
59+
return a.to_bits() == b.to_bits();
60+
}
61+
if a.is_nan() || a.is_sign_negative() != b.is_sign_negative() {
62+
return false;
63+
}
64+
let ai = a.to_bits() as i32;
65+
let bi = b.to_bits() as i32;
66+
(ai - bi).unsigned_abs() <= 4
67+
}
68+
69+
const XS: &[f64] = &[
70+
0.0, -0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, -0.5, -1.0, -1.5, -2.0, -2.5, -3.0, 0.25, 0.75, -0.25,
71+
0.1, 0.2, 0.3, 0.4, 0.45, 0.7, 1.3, 2.7, -3.3, 4.25, 10.1, 10.25, -0.123, 100.5, 1000.25,
72+
1.0 / 3.0, 1e15, 1e20, -1e20, 9007199254740992.0, 0.5 + 1e15,
73+
f64::INFINITY, f64::NEG_INFINITY, f64::NAN,
74+
];
75+
const XSF: &[f32] = &[
76+
0.0, -0.0, 0.5, 1.0, 1.5, 2.0, -0.5, -1.0, 0.25, 0.75, 0.1, 0.3, 0.7, 10.25, 16777216.0,
77+
f32::INFINITY, f32::NAN,
78+
];
79+
80+
#[test]
81+
fn pi_trig_matches_glibc() {
82+
let h = libm();
83+
let g_sinpi: extern "C" fn(f64) -> f64 = unsafe { core::mem::transmute(sym(h, c"sinpi")) };
84+
let g_cospi: extern "C" fn(f64) -> f64 = unsafe { core::mem::transmute(sym(h, c"cospi")) };
85+
let g_tanpi: extern "C" fn(f64) -> f64 = unsafe { core::mem::transmute(sym(h, c"tanpi")) };
86+
let g_sinpif: extern "C" fn(f32) -> f32 = unsafe { core::mem::transmute(sym(h, c"sinpif")) };
87+
let g_cospif: extern "C" fn(f32) -> f32 = unsafe { core::mem::transmute(sym(h, c"cospif")) };
88+
let g_tanpif: extern "C" fn(f32) -> f32 = unsafe { core::mem::transmute(sym(h, c"tanpif")) };
89+
90+
let mut div: Vec<String> = Vec::new();
91+
92+
macro_rules! cmp64 {
93+
($name:literal, $flf:path, $gf:expr, $x:expr) => {{
94+
let x: f64 = $x;
95+
unsafe { feclearexcept(HARD) };
96+
let fv = unsafe { $flf(x) };
97+
let ff = unsafe { fetestexcept(HARD) } & HARD;
98+
unsafe { feclearexcept(HARD) };
99+
let gv = $gf(x);
100+
let gf2 = unsafe { fetestexcept(HARD) } & HARD;
101+
if !ulp_ok_f64(fv, gv) || ff != gf2 {
102+
div.push(format!(
103+
"{}({:.6e}): fl={:016x}/flags{:#x} glibc={:016x}/flags{:#x}",
104+
$name, x, fv.to_bits(), ff, gv.to_bits(), gf2
105+
));
106+
}
107+
}};
108+
}
109+
macro_rules! cmp32 {
110+
($name:literal, $flf:path, $gf:expr, $x:expr) => {{
111+
let x: f32 = $x;
112+
unsafe { feclearexcept(HARD) };
113+
let fv = unsafe { $flf(x) };
114+
let ff = unsafe { fetestexcept(HARD) } & HARD;
115+
unsafe { feclearexcept(HARD) };
116+
let gv = $gf(x);
117+
let gf2 = unsafe { fetestexcept(HARD) } & HARD;
118+
if !ulp_ok_f32(fv, gv) || ff != gf2 {
119+
div.push(format!(
120+
"{}({:.6e}): fl={:08x}/flags{:#x} glibc={:08x}/flags{:#x}",
121+
$name, x, fv.to_bits(), ff, gv.to_bits(), gf2
122+
));
123+
}
124+
}};
125+
}
126+
127+
for &x in XS {
128+
cmp64!("sinpi", fl::sinpi, g_sinpi, x);
129+
cmp64!("cospi", fl::cospi, g_cospi, x);
130+
cmp64!("tanpi", fl::tanpi, g_tanpi, x);
131+
}
132+
for &x in XSF {
133+
cmp32!("sinpif", fl::sinpif, g_sinpif, x);
134+
cmp32!("cospif", fl::cospif, g_cospif, x);
135+
cmp32!("tanpif", fl::tanpif, g_tanpif, x);
136+
}
137+
138+
assert!(
139+
div.is_empty(),
140+
"pi-trig divergences vs glibc ({}):\n {}",
141+
div.len(),
142+
div.join("\n ")
143+
);
144+
}

0 commit comments

Comments
 (0)