contrib/math: preserve negative zero in Expm1 - #3273
Open
EylonKrause wants to merge 1 commit into
Open
Conversation
Expm1(-0.0) returned +0.0 instead of -0.0. On the |x| < ln2/2 polynomial path, ExpPoly forms 0.5 * (+0) + (-0) = +0, dropping the sign bit, and Expm1 -- unlike the odd functions in this file -- never reapplies the sign of x. C11 Annex F (F.10.3.3) requires expm1(+/-0) = +/-0. Reapply x's sign on the polynomial path via Or(y, And(x, kNegZero)), using the sign-bit mask already defined in this function. Since expm1(x) shares the sign of x, this only affects the x == -0.0 case; every other input (including +0, small nonzero x, and the large-|x| branch) is unchanged. Add a bit-exact signed-zero regression test for Expm1; the existing ULP comparator treats +0 == -0 and cannot catch this.
jan-wassenberg
requested changes
Aug 13, 2026
| // Reapply the sign of x on the polynomial path so a negative-zero input is | ||
| // preserved: expm1(-0) = -0 (C11 F.10.3.3), which the polynomial drops when | ||
| // it forms 0.5 * (+0) + (-0) = +0. | ||
| const V z = IfThenElse(Lt(Abs(x), kLn2Over2), Or(y, And(x, kNegZero)), |
Member
There was a problem hiding this comment.
Thanks again for fixing! Rather than Or+And, please use CopySign(y, x) - this is more efficient on some platforms.
| // Reapply the sign of x on the polynomial path so a negative-zero input is | ||
| // preserved: expm1(-0) = -0 (C11 F.10.3.3), which the polynomial drops when | ||
| // it forms 0.5 * (+0) + (-0) = +0. | ||
| const V z = IfThenElse(Lt(Abs(x), kLn2Over2), Or(y, And(x, kNegZero)), |
Member
There was a problem hiding this comment.
Once again thanks for fixing! Rather than Or+And, please use CopySign(y, x) - this is more efficient on some platforms.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Expm1(-0.0)returns+0.0instead of-0.0, losing the sign of zero. C11 Annex F (F.10.3.3) requiresexpm1(±0) = ±0, and scalarstd::expm1complies — so this is a signed-zero correctness bug (observable viastd::signbit, or1.0 / expm1(-0.0)giving+infinstead of-inf).Cause
On the
|x| < ln2/2polynomial path,ExpPolycomputesMulAdd(poly, x*x, x). Forx = -0.0this is0.5 * (+0.0) + (-0.0), and IEEE addition of oppositely-signed zeros yields+0.0, so the sign bit is dropped. Unlike the odd functions in this file (Sin,Tan,Sinh, …),Expm1never reapplies the sign ofx, so the negative zero is not restored.Fix
Reapply
x's sign on the polynomial path using the sign-bit mask already defined in the function:expm1(x)shares the sign ofxfor allx, so OR-ing inx's sign bit is a no-op for every input exceptx == -0.0, which it corrects to-0.0.+0.0, small nonzerox, the large-|x|branch, and the-1clamp are unchanged, on every SIMD target.Testing
TestExpm1SignedZero, a bit-exact check thatExpm1(±0)returns±0— the existing ULP comparator treats+0 == -0and cannot detect this (which is why it went unnoticed).Expm1(-0)yields+0); after, it passes.math_testpasses (104 tests) across every enabled SIMD target.Disclosure: this contribution was authored with an AI coding assistant (Claude) and reviewed before submission.