← board

crtl's integral-part math loses the sign of zero, and gives up past 2^63

Measured — gcc -O1 -lm as oracle, results compared as raw bit patterns

38 arguments x the whole libm surface. 17 differing results, in three groups:

1. fabs(-0.0) returns -0.0. C requires +0.0; fabs is defined as clearing the sign bit, and x < 0.0 ? -x : x never fires for -0.0 because -0.0 is not less than zero. One-line fix, and the same shape as the bug already fixed in floor/ceil (see their comment: "Sign of zero is OBSERVABLE and C preserves it" — that lesson has not been applied to the rest of the file).

2. Negative results that should be -0.0 come back +0.0trunc(-0.5), rint(-0.5), round(-0.0), frexp(-0.0), modf(-1.0)'s fractional part and modf's integral out-parameter for every negative input. All go through a (long long) round trip, which is where the sign dies. floor/ceil already restore it explicitly; these did not get the same treatment.

3. |x| >= 2^63 is undefined-cast territory. trunc(1e300), round(1e300) and modf(1e300) answer -2^63 (0xc3e0000000000000) where gcc returns the input unchanged — every double that large is already an integer. floor/ceil guard this with an explicit |x| >= 2^52 early return; trunc, round and modf do not, and trunc's own comment concedes it ("|x| >= 2^63 is out of scope, like the other loop-form helpers here"). It should not be out of scope: the guard is one comparison and the answer is the argument.

Why it is filed low

Every case is a boundary value, none is a crash, and -0.0 is observable only through printing or a sign-bit test. But group 3 is a wrong magnitude, not a wrong sign, and all three are cheap: floor/ceil already carry both fixes and can simply be copied down the file. Do all six together — this is one bug wearing six hats (see normalise-dont-special-case.md), and fixing one at a time is how the other five stay broken.

Not in scope here

The differential's other 131 differing results are asin/acos/atan2/cbrt/ log10/hypot/sinh/cosh/tanh/pow. Those are the correctly-rounded double-double kernels, where a difference from glibc is often glibc misrounding (the file header documents measured misround rates: cbrt ~55% of arguments). They need a decimal reference, not gcc, to judge — a separate, bigger job.

Gate

The probe (see the parent ticket) showing zero trunc/round/rint/modf/ frexp/fabs differences against gcc, plus make lib-test and the C suites.

2026-08-10 (Track B): fixed, all six together

Done as one change, per the ticket's own "one bug wearing six hats" call — the two corrections floor/ceil already carried were copied down the file rather than each function getting its own variant.

function was now
fabs x < 0.0 ? -x : x — misses -0.0, which is not < 0.0 tests the sign BIT (crtl_signbit_d), which is fabs's actual definition
trunc bare (long long) cast |x| >= 2^52 early return + -0.0 restore
round (long long)(x +/- 0.5) fraction compare vs 0.5, same guard + restore
rint returned the neighbour's zero same guard + restore
frexp return 0.0 for zero; hung on infinity returns x; NaN/inf exit early
modf cast through long, rebuilt sign by negating rides trunc(), so it inherits both fixes and has no range logic of its own

Normalised on 2^52, not the 2^63 the ticket named: it is the threshold floor/ceil already use, every double at or above it is integral, and one threshold across the family is one fewer thing to get inconsistent.

Two things found that the ticket did not list

frexp(1.0/0.0 HUNG — not a wrong value, a non-terminating loop: while (a >= 1.0) a = a * 0.5 never converges for an infinity. Measured both ways, since a hang is easy to assert and easy to get wrong: the pre-fix build times out at 5s, the fixed build returns inf immediately. The differential probe could not have caught this — it skips infinity for frexp — so it would have survived the ticket's own gate.

round(0.49999999999999994) answered 1, C requires 0. A separate bug in the same function and unrelated to sign or range: the old body cast x + 0.5, and for the double just below 0.5 that sum rounds UP to exactly 1.0. Comparing against x - trunc(x) never adds and so cannot round.

Verification

Out of scope as filed: the 131 asin/acos/atan2/cbrt/log10/hypot/ sinh/cosh/tanh/pow differences are the correctly-rounded double-double kernels, where a gcc difference is often glibc misrounding. Those need a decimal reference, not gcc.

Log