Skip to content

Commit 67ee00f

Browse files
fix(math): C23 fromfp/ufromfp/fromfpx/ufromfpx domain + flag parity vs glibc
Continuing the zero-test-coverage scan, the round-to-integer-of-width family diverged from host glibc on several axes: - never raised FE_INVALID on out-of-range / NaN / inf (glibc always does), and the *x variants never raised FE_INEXACT. - out-of-range returned NaN (or, for width 64, the original non-integer value via a saturating `as i64` cast) instead of glibc's clamp (max / min). - ufromfp rejected every negative input early, so ufromfp(-0.4, UPWARD) gave NaN where glibc rounds up to 0. - width-64 overflow returns a rounded max (2^63 != 2^63-1), so glibc raises FE_INEXACT alongside FE_INVALID — fl raised neither correctly. Rewrote the shared cores: round flag-free (round-half-away/even built from trunc + exact arithmetic; f64::round and 2f64.powi(runtime) both raise spurious FE_INEXACT), compute the value and the INTENDED exception bits as pure logic, then stamp the fenv to exactly {caller's pre-existing flags} | {intended}, making the contract exact regardless of incidental rounding. -0 results normalised to +0. New gate conformance_diff_fromfp: 3000 golden tuples captured from a gcc -fno-builtin oracle (glibc fromfp is an IFUNC, so a dlsym/&fn pointer hits the resolver and returns garbage — only direct calls work), value bits + INVALID/INEXACT flags, all 4 fns x 5 modes x 6 widths x 25 values. 116 math_abi unit tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b6e168d commit 67ee00f

2 files changed

Lines changed: 3205 additions & 47 deletions

File tree

crates/frankenlibc-abi/src/math_abi.rs

Lines changed: 159 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4859,67 +4859,179 @@ const FP_INT_TONEARESTFROMZERO: c_int = 3;
48594859
#[allow(dead_code)]
48604860
const FP_INT_TONEAREST: c_int = 4;
48614861

4862-
fn fromfp_impl(x: f64, rnd: c_int, width: u32) -> f64 {
4863-
if x.is_nan() || x.is_infinite() {
4864-
return f64::NAN;
4865-
}
4866-
let r = match rnd {
4862+
/// Raise FE_INEXACT via a safe force_eval (1/3 is inexact). Used by the
4863+
/// fromfpx/ufromfpx variants when the kept (in-range) result differs from x.
4864+
#[inline]
4865+
fn raise_inexact_f64() {
4866+
let _ = core::hint::black_box(core::hint::black_box(1.0_f64) / core::hint::black_box(3.0_f64));
4867+
}
4868+
4869+
/// 2^n for n in 0..=64, exact and FE-flag-free (bit construction). `2f64.powi(n)`
4870+
/// with a runtime n lowers to a libm pow call that raises FE_INEXACT even though
4871+
/// 2^n is exact, which would pollute fromfp's flag contract.
4872+
#[inline]
4873+
fn pow2_exact(n: u32) -> f64 {
4874+
f64::from_bits((1023u64 + n as u64) << 52)
4875+
}
4876+
4877+
/// Round in the requested FP_INT direction WITHOUT raising FP exceptions.
4878+
/// `fromfp` must raise only FE_INVALID (the *x variants add FE_INEXACT
4879+
/// explicitly), so the rounding itself must be flag-free. ceil/floor/trunc
4880+
/// lower to suppress-precision roundsd; round-half-away/even are built from
4881+
/// trunc + exact integer arithmetic (x - trunc(x) is exact; adding ±1 to an
4882+
/// integer-valued f64 is exact). `f64::round` is NOT used: it raises INEXACT.
4883+
#[inline]
4884+
fn fromfp_round(x: f64, rnd: c_int) -> f64 {
4885+
match rnd {
48674886
FP_INT_UPWARD => x.ceil(),
48684887
FP_INT_DOWNWARD => x.floor(),
48694888
FP_INT_TOWARDZERO => x.trunc(),
4870-
FP_INT_TONEARESTFROMZERO => x.round(),
4889+
FP_INT_TONEARESTFROMZERO => {
4890+
let t = x.trunc();
4891+
if (x - t).abs() >= 0.5 { t + x.signum() } else { t }
4892+
}
48714893
_ => {
4872-
let r = x.round();
4873-
if (x - r).abs() == 0.5 {
4874-
if (r as i64) % 2 != 0 {
4875-
r - r.signum()
4876-
} else {
4877-
r
4878-
}
4894+
// FP_INT_TONEAREST: round half to even.
4895+
let t = x.trunc();
4896+
let af = (x - t).abs();
4897+
if af < 0.5 {
4898+
t
4899+
} else if af > 0.5 {
4900+
t + x.signum()
4901+
} else if (t as i64) % 2 == 0 {
4902+
t // tie: t already even
48794903
} else {
4880-
r
4904+
t + x.signum()
48814905
}
48824906
}
4883-
};
4884-
if width == 0 || width > 64 {
4885-
return f64::NAN;
48864907
}
4887-
let max = if width >= 64 {
4888-
i64::MAX
4908+
}
4909+
4910+
/// Shared core for fromfp/fromfpx. Matches glibc: round x in the requested
4911+
/// direction to a signed integer of `width` bits; NaN/±inf and out-of-range
4912+
/// results raise FE_INVALID and return glibc's clamp (max for NaN/+inf/overflow,
4913+
/// min for -inf/underflow). With `inexact`, also raise FE_INEXACT when the kept
4914+
/// result differs from x (the fromfpx variant).
4915+
const FE_INVALID_BIT: c_int = 0x01;
4916+
const FE_INEXACT_BIT: c_int = 0x20;
4917+
4918+
unsafe extern "C" {
4919+
fn feraiseexcept(excepts: c_int) -> c_int;
4920+
fn feclearexcept(excepts: c_int) -> c_int;
4921+
fn fetestexcept(excepts: c_int) -> c_int;
4922+
}
4923+
4924+
/// Set the FP exception state for the fromfp family to EXACTLY the caller's
4925+
/// pre-existing flags plus `want` (FE_INVALID/FE_INEXACT). The decision logic
4926+
/// itself performs FP arithmetic (rounding, 2^w construction) that can raise
4927+
/// spurious flags; computing the intended flags as pure booleans and then
4928+
/// stamping them here makes the contract exact regardless.
4929+
#[inline]
4930+
fn fromfp_set_flags(entry: c_int, want: c_int) {
4931+
unsafe {
4932+
feclearexcept(FE_INVALID_BIT | FE_INEXACT_BIT);
4933+
let f = (entry & (FE_INVALID_BIT | FE_INEXACT_BIT)) | want;
4934+
if f != 0 {
4935+
feraiseexcept(f);
4936+
}
4937+
}
4938+
}
4939+
4940+
fn fromfp_core(x: f64, rnd: c_int, width: u32, inexact: bool) -> f64 {
4941+
let entry = unsafe { fetestexcept(FE_INVALID_BIT | FE_INEXACT_BIT) };
4942+
let w = width.min(64);
4943+
if w == 0 {
4944+
fromfp_set_flags(entry, FE_INVALID_BIT);
4945+
return 0.0;
4946+
}
4947+
let thresh_hi = pow2_exact(w - 1); // 2^(w-1): first out-of-range high value
4948+
// max = 2^(w-1)-1, exact iff w <= 54; for wider w it rounds to 2^(w-1)
4949+
// (= thresh_hi), exactly what glibc returns — and that rounding means a high
4950+
// overflow raises FE_INEXACT alongside FE_INVALID. The min is exact.
4951+
let hi_inexact = if w > 54 { FE_INEXACT_BIT } else { 0 };
4952+
let clamp_hi = if w <= 54 { thresh_hi - 1.0 } else { thresh_hi };
4953+
let clamp_lo = -thresh_hi; // min = -2^(w-1)
4954+
let (value, want) = if x.is_nan() {
4955+
(clamp_hi, FE_INVALID_BIT | hi_inexact)
4956+
} else if x.is_infinite() {
4957+
if x > 0.0 {
4958+
(clamp_hi, FE_INVALID_BIT | hi_inexact)
4959+
} else {
4960+
(clamp_lo, FE_INVALID_BIT)
4961+
}
48894962
} else {
4890-
(1i64 << (width - 1)) - 1
4963+
let r = fromfp_round(x, rnd);
4964+
if r >= thresh_hi {
4965+
(clamp_hi, FE_INVALID_BIT | hi_inexact)
4966+
} else if r < clamp_lo {
4967+
(clamp_lo, FE_INVALID_BIT)
4968+
} else {
4969+
let inx = if inexact && r != x { FE_INEXACT_BIT } else { 0 };
4970+
(if r == 0.0 { 0.0 } else { r }, inx) // glibc normalises a zero to +0
4971+
}
48914972
};
4892-
let min = if width >= 64 {
4893-
i64::MIN
4973+
fromfp_set_flags(entry, want);
4974+
value
4975+
}
4976+
4977+
/// Shared core for ufromfp/ufromfpx. Unsigned range [0, 2^width-1]; negatives
4978+
/// that round into range are valid (e.g. ufromfp(-0.4, UPWARD) = 0).
4979+
fn ufromfp_core(x: f64, rnd: c_int, width: u32, inexact: bool) -> f64 {
4980+
let entry = unsafe { fetestexcept(FE_INVALID_BIT | FE_INEXACT_BIT) };
4981+
let w = width.min(64);
4982+
if w == 0 {
4983+
fromfp_set_flags(entry, FE_INVALID_BIT);
4984+
return 0.0;
4985+
}
4986+
let thresh_hi = pow2_exact(w); // 2^w
4987+
let hi_inexact = if w > 53 { FE_INEXACT_BIT } else { 0 };
4988+
let clamp_hi = if w <= 53 { thresh_hi - 1.0 } else { thresh_hi };
4989+
let (value, want) = if x.is_nan() {
4990+
(clamp_hi, FE_INVALID_BIT | hi_inexact)
4991+
} else if x.is_infinite() {
4992+
if x > 0.0 {
4993+
(clamp_hi, FE_INVALID_BIT | hi_inexact)
4994+
} else {
4995+
(0.0, FE_INVALID_BIT)
4996+
}
48944997
} else {
4895-
-(1i64 << (width - 1))
4998+
let r = fromfp_round(x, rnd);
4999+
if r >= thresh_hi {
5000+
(clamp_hi, FE_INVALID_BIT | hi_inexact)
5001+
} else if r < 0.0 {
5002+
(0.0, FE_INVALID_BIT)
5003+
} else {
5004+
let inx = if inexact && r != x { FE_INEXACT_BIT } else { 0 };
5005+
(if r == 0.0 { 0.0 } else { r }, inx) // normalise -0 -> +0
5006+
}
48965007
};
4897-
let ri = r as i64;
4898-
if ri < min || ri > max { f64::NAN } else { r }
5008+
fromfp_set_flags(entry, want);
5009+
value
5010+
}
5011+
5012+
fn fromfp_impl(x: f64, rnd: c_int, width: u32) -> f64 {
5013+
fromfp_core(x, rnd, width, false)
5014+
}
5015+
fn fromfpx_impl(x: f64, rnd: c_int, width: u32) -> f64 {
5016+
fromfp_core(x, rnd, width, true)
48995017
}
49005018
fn fromfpf_impl(x: f32, rnd: c_int, width: u32) -> f32 {
4901-
fromfp_impl(x as f64, rnd, width) as f32
5019+
fromfp_core(x as f64, rnd, width, false) as f32
5020+
}
5021+
fn fromfpxf_impl(x: f32, rnd: c_int, width: u32) -> f32 {
5022+
fromfp_core(x as f64, rnd, width, true) as f32
49025023
}
49035024
fn ufromfp_impl(x: f64, rnd: c_int, width: u32) -> f64 {
4904-
if x.is_nan() || x.is_infinite() || x < 0.0 {
4905-
return f64::NAN;
4906-
}
4907-
let r = fromfp_impl(x, rnd, 64);
4908-
if r.is_nan() || r < 0.0 {
4909-
return f64::NAN;
4910-
}
4911-
if width == 0 || width > 64 {
4912-
return f64::NAN;
4913-
}
4914-
let max = if width >= 64 {
4915-
u64::MAX
4916-
} else {
4917-
(1u64 << width) - 1
4918-
};
4919-
if (r as u64) > max { f64::NAN } else { r }
5025+
ufromfp_core(x, rnd, width, false)
5026+
}
5027+
fn ufromfpx_impl(x: f64, rnd: c_int, width: u32) -> f64 {
5028+
ufromfp_core(x, rnd, width, true)
49205029
}
49215030
fn ufromfpf_impl(x: f32, rnd: c_int, width: u32) -> f32 {
4922-
ufromfp_impl(x as f64, rnd, width) as f32
5031+
ufromfp_core(x as f64, rnd, width, false) as f32
5032+
}
5033+
fn ufromfpxf_impl(x: f32, rnd: c_int, width: u32) -> f32 {
5034+
ufromfp_core(x as f64, rnd, width, true) as f32
49235035
}
49245036

49255037
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
@@ -4988,11 +5100,11 @@ pub unsafe extern "C" fn ufromfpf128(x: f64, rnd: c_int, width: u32) -> f64 {
49885100
}
49895101
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
49905102
pub unsafe extern "C" fn fromfpx(x: f64, rnd: c_int, width: u32) -> f64 {
4991-
fromfp_impl(x, rnd, width)
5103+
fromfpx_impl(x, rnd, width)
49925104
}
49935105
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
49945106
pub unsafe extern "C" fn fromfpxf(x: f32, rnd: c_int, width: u32) -> f32 {
4995-
fromfpf_impl(x, rnd, width)
5107+
fromfpxf_impl(x, rnd, width)
49965108
}
49975109
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
49985110
pub unsafe extern "C" fn fromfpxl(x: f64, rnd: c_int, width: u32) -> f64 {
@@ -5020,11 +5132,11 @@ pub unsafe extern "C" fn fromfpxf128(x: f64, rnd: c_int, width: u32) -> f64 {
50205132
}
50215133
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
50225134
pub unsafe extern "C" fn ufromfpx(x: f64, rnd: c_int, width: u32) -> f64 {
5023-
ufromfp_impl(x, rnd, width)
5135+
ufromfpx_impl(x, rnd, width)
50245136
}
50255137
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
50265138
pub unsafe extern "C" fn ufromfpxf(x: f32, rnd: c_int, width: u32) -> f32 {
5027-
ufromfpf_impl(x, rnd, width)
5139+
ufromfpxf_impl(x, rnd, width)
50285140
}
50295141
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
50305142
pub unsafe extern "C" fn ufromfpxl(x: f64, rnd: c_int, width: u32) -> f64 {

0 commit comments

Comments
 (0)