• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stillwater-sc / universal / 24956707653

26 Apr 2026 12:32PM UTC coverage: 84.274% (-0.04%) from 84.313%
24956707653

push

github

web-flow
test(math): harden pow signed-zero and special-value test checks (CodeRabbit) (#773)

Two findings on PR #770 (already merged) that warrant a follow-up
test-hardening PR:

1. Signed-zero static_asserts cannot detect sign regressions.
   `+0.0 == -0.0` is true in IEEE-754, so `static_assert(value == -0.0)`
   passes whether value is +0 or -0. The implementation in pow.hpp
   correctly handles signed zero (it uses bit_cast to detect sign), but
   a future regression to the wrong-sign result would not have been
   caught by these tests.

   Fix: add constexpr-friendly sign_bit / is_pos_zero / is_neg_zero
   helpers in the test (using std::bit_cast since std::signbit isn't
   constexpr until C++23 and Universal targets C++20). Use these in
   the static_asserts that pin signed-zero contracts: pow(-inf, -3)
   == -0, pow(-inf, -4) == +0, pow(-0, 3) == -0, pow(-0, 4) == +0,
   pow(-0, 2.5) == +0, pow(+0, 3) == +0.

   ==-inf vs +inf comparisons remain via == because +inf != -inf in
   IEEE-754; only ==0.0 has the signed-zero-equality issue.

2. Runtime check lambda silently passed certain failure modes.
   For ref == +/-inf, computing (our - ref) / ref produces NaN (when
   our is also inf), and NaN > tol is false, so the check passed even
   when our had wrong sign or was finite. For ref finite and our NaN,
   the same arithmetic produced NaN > tol = false, masking the failure.

   Fix: add explicit special-case handling at the top of the lambda:
     - std::isnan(ref) -> require std::isnan(our) (existing behavior,
       restated using std::isnan for clarity)
     - std::isinf(ref) -> require std::isinf(our) AND matching std::signbit
     - !std::isfinite(our) when ref is finite -> count as failure
     - both finite -> the existing (our-ref)/ref relative-error check

Verified cm_pow PASSes on gcc and clang with the hardened checks (which
shows the implementation has always been correct -- only the tests were
under-strict).

Relates to #765, #770

Co-auth... (continued)

3 of 15 new or added lines in 1 file covered. (20.0%)

18 existing lines in 4 files now uncovered.

45429 of 53906 relevant lines covered (84.27%)

6353118.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

48.98
/internal/constexpr_math/api/pow.cpp
1
// pow.cpp: regression for sw::math::constexpr_math::pow (integer + general overloads)
2
//
3
// Copyright (C) 2017 Stillwater Supercomputing, Inc.
4
// SPDX-License-Identifier: MIT
5
//
6
// This file is part of the universal numbers project, which is released under an MIT Open Source license.
7

8
#include <bit>
9
#include <cmath>
10
#include <cstdint>
11
#include <iostream>
12
#include <iomanip>
13
#include <limits>
14

15
#include <math/constexpr_math.hpp>
16

17
namespace cm = sw::math::constexpr_math;
18

19
// Constexpr-friendly sign-bit accessor. std::signbit isn't constexpr until
20
// C++23 and Universal targets C++20. Bit-cast is constexpr for trivially-
21
// copyable types since C++20, so we extract the sign bit directly.
22
constexpr bool sign_bit(double v) {
23
        return (std::bit_cast<std::uint64_t>(v) >> 63) != 0;
24
}
25
constexpr bool sign_bit(float v) {
26
        return (std::bit_cast<std::uint32_t>(v) >> 31) != 0;
27
}
28

29
// Helpers for asserting the IEEE-754 contract on signed zeros. `value == 0.0`
30
// is true for both +0 and -0, so == cannot distinguish them. These predicates
31
// pin the sign explicitly.
32
constexpr bool is_pos_zero(double v) { return v == 0.0 && !sign_bit(v); }
33
constexpr bool is_neg_zero(double v) { return v == 0.0 &&  sign_bit(v); }
34

35
// ============================================================================
36
// Compile-time correctness via static_assert
37
// ============================================================================
38

39
// ----------------------------------------------------------------------------
40
// Integer-exponent fast path (pow(T, int))
41
// ----------------------------------------------------------------------------
42

43
// Positive integer exponent
44
static_assert(cm::pow(2.0, 0)   == 1.0,    "pow(2, 0) == 1");
45
static_assert(cm::pow(2.0, 1)   == 2.0,    "pow(2, 1) == 2");
46
static_assert(cm::pow(2.0, 10)  == 1024.0, "pow(2, 10) == 1024");
47
static_assert(cm::pow(3.0, 4)   == 81.0,   "pow(3, 4) == 81");
48
static_assert(cm::pow(0.5, 8)   == 1.0/256.0, "pow(0.5, 8) == 1/256");
49

50
// Negative integer exponent
51
static_assert(cm::pow(2.0, -1)  == 0.5,    "pow(2, -1) == 0.5");
52
static_assert(cm::pow(2.0, -3)  == 0.125,  "pow(2, -3) == 0.125");
53
static_assert(cm::pow(2.0, -10) == 1.0/1024.0, "pow(2, -10) == 1/1024");
54

55
// Negative base, integer exponent: sign depends on parity
56
static_assert(cm::pow(-2.0, 3)  == -8.0,   "pow(-2, 3) == -8 (odd exponent)");
57
static_assert(cm::pow(-2.0, 4)  == 16.0,   "pow(-2, 4) == 16 (even exponent)");
58
static_assert(cm::pow(-1.0, 100) == 1.0,   "pow(-1, 100) == 1");
59
static_assert(cm::pow(-1.0, 101) == -1.0,  "pow(-1, 101) == -1");
60

61
// Float overload
62
static_assert(cm::pow(2.0f, 10) == 1024.0f, "powf(2, 10) == 1024");
63
static_assert(cm::pow(0.5f, 8)  == 1.0f/256.0f, "powf(0.5, 8) == 1/256");
64
static_assert(cm::pow(-2.0f, 5) == -32.0f, "powf(-2, 5) == -32");
65

66
// ----------------------------------------------------------------------------
67
// General overload pow(T, T) -- integer arguments via fast-path
68
// ----------------------------------------------------------------------------
69

70
// When the floating-point exponent is exactly an integer, the integer fast
71
// path is dispatched; results match the integer overload bit-for-bit.
72
static_assert(cm::pow(2.0, 10.0)  == 1024.0, "pow(2.0, 10.0) == 1024 via fast path");
73
static_assert(cm::pow(3.0, 4.0)   == 81.0,   "pow(3.0, 4.0) == 81 via fast path");
74
static_assert(cm::pow(-2.0, 3.0)  == -8.0,   "pow(-2.0, 3.0) == -8 via fast path");
75
static_assert(cm::pow(-1.0, 100.0) == 1.0,   "pow(-1.0, 100.0) == 1 via fast path");
76

77
// Negative-base + integer exponent must use the squaring fast path for exact
78
// integer-power semantics (regression for the CodeRabbit Major fix).
79
static_assert(cm::pow(-3.0, 5.0)  == -243.0, "pow(-3.0, 5.0) == -243 via fast path (negative base, odd exp)");
80
static_assert(cm::pow(-3.0, 4.0)  == 81.0,   "pow(-3.0, 4.0) == 81 via fast path (negative base, even exp)");
81
static_assert(cm::pow(-7.0, 3.0)  == -343.0, "pow(-7.0, 3.0) == -343 via fast path");
82
static_assert(cm::pow(-2.0, -3.0) == -0.125, "pow(-2.0, -3.0) == -0.125 via fast path (negative base, negative odd exp)");
83
static_assert(cm::pow(-2.0, -4.0) == 0.0625, "pow(-2.0, -4.0) == 0.0625 via fast path (negative base, negative even exp)");
84

85
// Float overload regression for the same fix.
86
static_assert(cm::pow(-3.0f, 5.0f) == -243.0f, "powf(-3.0, 5.0) == -243 via fast path");
87
static_assert(cm::pow(-3.0f, 4.0f) == 81.0f,   "powf(-3.0, 4.0) == 81 via fast path");
88

89
// ----------------------------------------------------------------------------
90
// General overload pow(T, T) -- transcendental path
91
// ----------------------------------------------------------------------------
92

93
// pow(2, 0.5) == sqrt(2) ~= 1.4142135623730951
94
constexpr double cx_sqrt2 = cm::pow(2.0, 0.5);
95
static_assert(cx_sqrt2 > 1.4142135 && cx_sqrt2 < 1.4142137,
96
              "pow(2.0, 0.5) ~= sqrt(2)");
97

98
// pow(8, 1/3) ~= 2.0 (within transcendental round-trip error)
99
constexpr double cx_cbrt8 = cm::pow(8.0, 1.0/3.0);
100
static_assert(cx_cbrt8 > 1.99999 && cx_cbrt8 < 2.00001,
101
              "pow(8, 1/3) ~= 2");
102

103
// ----------------------------------------------------------------------------
104
// IEEE-754 special-case table (C99 7.12.7.4)
105
// ----------------------------------------------------------------------------
106

107
// pow(x, 0) == 1 for any x including NaN, infinity
108
static_assert(cm::pow(0.0, 0.0)   == 1.0, "pow(0, 0) == 1");
109
static_assert(cm::pow(-0.0, 0.0)  == 1.0, "pow(-0, 0) == 1");
110
static_assert(cm::pow(2.0, 0.0)   == 1.0, "pow(2, 0) == 1");
111
static_assert(cm::pow(-3.5, 0.0)  == 1.0, "pow(-3.5, 0) == 1");
112
static_assert(cm::pow(std::numeric_limits<double>::infinity(), 0.0) == 1.0,
113
              "pow(+inf, 0) == 1");
114
static_assert(cm::pow(std::numeric_limits<double>::quiet_NaN(), 0.0) == 1.0,
115
              "pow(NaN, 0) == 1");
116

117
// pow(1, y) == 1 for any y including NaN, infinity
118
static_assert(cm::pow(1.0, 5.0) == 1.0, "pow(1, 5) == 1");
119
static_assert(cm::pow(1.0, std::numeric_limits<double>::infinity()) == 1.0,
120
              "pow(1, +inf) == 1");
121
static_assert(cm::pow(1.0, std::numeric_limits<double>::quiet_NaN()) == 1.0,
122
              "pow(1, NaN) == 1");
123

124
// NaN propagation when neither override applies
125
static_assert(cm::pow(std::numeric_limits<double>::quiet_NaN(), 2.0)
126
              != cm::pow(std::numeric_limits<double>::quiet_NaN(), 2.0),
127
              "pow(NaN, 2) == NaN");
128
static_assert(cm::pow(2.0, std::numeric_limits<double>::quiet_NaN())
129
              != cm::pow(2.0, std::numeric_limits<double>::quiet_NaN()),
130
              "pow(2, NaN) == NaN");
131

132
// pow(-1, +/-inf) == 1
133
static_assert(cm::pow(-1.0,  std::numeric_limits<double>::infinity()) == 1.0,
134
              "pow(-1, +inf) == 1");
135
static_assert(cm::pow(-1.0, -std::numeric_limits<double>::infinity()) == 1.0,
136
              "pow(-1, -inf) == 1");
137

138
// |x| < 1, +/-inf
139
static_assert(cm::pow(0.5,  std::numeric_limits<double>::infinity()) == 0.0,
140
              "pow(0.5, +inf) == 0");
141
static_assert(cm::pow(0.5, -std::numeric_limits<double>::infinity())
142
              == std::numeric_limits<double>::infinity(),
143
              "pow(0.5, -inf) == +inf");
144

145
// |x| > 1, +/-inf
146
static_assert(cm::pow(2.0,  std::numeric_limits<double>::infinity())
147
              == std::numeric_limits<double>::infinity(),
148
              "pow(2, +inf) == +inf");
149
static_assert(cm::pow(2.0, -std::numeric_limits<double>::infinity()) == 0.0,
150
              "pow(2, -inf) == 0");
151

152
// pow(+inf, y < 0) == 0;  pow(+inf, y > 0) == +inf
153
static_assert(cm::pow(std::numeric_limits<double>::infinity(), -1.0) == 0.0,
154
              "pow(+inf, -1) == 0");
155
static_assert(cm::pow(std::numeric_limits<double>::infinity(), 2.0)
156
              == std::numeric_limits<double>::infinity(),
157
              "pow(+inf, 2) == +inf");
158

159
// pow(-inf, ...): sign depends on exponent parity. ==-inf vs +inf is OK
160
// (they compare unequal) but ==0.0 cannot distinguish +0 from -0, so use the
161
// is_pos_zero / is_neg_zero predicates for the underflow cases.
162
static_assert(cm::pow(-std::numeric_limits<double>::infinity(), 3.0)
163
              == -std::numeric_limits<double>::infinity(),
164
              "pow(-inf, 3) == -inf (odd integer)");
165
static_assert(cm::pow(-std::numeric_limits<double>::infinity(), 4.0)
166
              == std::numeric_limits<double>::infinity(),
167
              "pow(-inf, 4) == +inf (even integer)");
168
static_assert(is_neg_zero(cm::pow(-std::numeric_limits<double>::infinity(), -3.0)),
169
              "pow(-inf, -3) == -0 (odd integer, neg) -- sign matters");
170
static_assert(is_pos_zero(cm::pow(-std::numeric_limits<double>::infinity(), -4.0)),
171
              "pow(-inf, -4) == +0 (even integer, neg) -- sign matters");
172

173
// pow(+/-0, y > 0) -- y odd integer preserves sign of zero.
174
// Use is_pos_zero/is_neg_zero so the sign bit is asserted explicitly.
175
static_assert(is_pos_zero(cm::pow(0.0, 3.0)),  "pow(+0, 3) == +0");
176
static_assert(is_neg_zero(cm::pow(-0.0, 3.0)), "pow(-0, 3) == -0 (odd integer preserves sign)");
177
static_assert(is_pos_zero(cm::pow(-0.0, 4.0)), "pow(-0, 4) == +0 (even integer drops sign)");
178
static_assert(is_pos_zero(cm::pow(-0.0, 2.5)), "pow(-0, 2.5) == +0 (non-integer drops sign)");
179

180
// pow(+/-0, y < 0) -- y odd integer preserves sign of infinity
181
static_assert(cm::pow(0.0, -1.0)
182
              == std::numeric_limits<double>::infinity(),
183
              "pow(+0, -1) == +inf");
184
static_assert(cm::pow(-0.0, -3.0)
185
              == -std::numeric_limits<double>::infinity(),
186
              "pow(-0, -3) == -inf (odd integer)");
187
static_assert(cm::pow(-0.0, -4.0)
188
              == std::numeric_limits<double>::infinity(),
189
              "pow(-0, -4) == +inf (even integer)");
190
static_assert(cm::pow(-0.0, -2.5)
191
              == std::numeric_limits<double>::infinity(),
192
              "pow(-0, -2.5) == +inf (non-integer)");
193

194
// pow(x < 0, finite non-integer y) == NaN
195
static_assert(cm::pow(-2.0, 0.5)
196
              != cm::pow(-2.0, 0.5),
197
              "pow(-2, 0.5) == NaN");
198
static_assert(cm::pow(-3.5, 1.5)
199
              != cm::pow(-3.5, 1.5),
200
              "pow(-3.5, 1.5) == NaN");
201

202
// ============================================================================
203
// Runtime cross-check vs std::pow
204
// ============================================================================
205

206
int main() {
1✔
207
        std::cout << "sw::math::constexpr_math::pow verification\n";
1✔
208

209
        int errors = 0;
1✔
210
        auto check = [&](const char* name, double x, double y, double our, double ref, double tol) {
21✔
211
                // NaN reference: expect our to also be NaN (NaN != NaN test).
212
                if (std::isnan(ref)) {
21✔
NEW
213
                        if (!std::isnan(our)) {
×
214
                                ++errors;
×
215
                                std::cout << "FAIL " << name << "  x=" << x << "  y=" << y
×
216
                                          << "  our=" << our << "  ref=NaN (expected NaN)\n";
×
217
                        }
218
                        return;
×
219
                }
220
                // Infinite reference: require matching sign explicitly. Computing
221
                // (our - ref) / ref for inf-vs-inf produces NaN which compares
222
                // false against tol and silently passes the test.
223
                if (std::isinf(ref)) {
21✔
NEW
224
                        if (!std::isinf(our) || std::signbit(our) != std::signbit(ref)) {
×
NEW
225
                                ++errors;
×
NEW
226
                                std::cout << "FAIL " << name << "  x=" << x << "  y=" << y
×
NEW
227
                                          << "  our=" << our << "  ref=" << ref
×
NEW
228
                                          << " (sign or finiteness mismatch)\n";
×
229
                        }
NEW
230
                        return;
×
231
                }
232
                // Finite reference but non-finite our: catch silently-passing NaN/inf.
233
                if (!std::isfinite(our)) {
21✔
NEW
234
                        ++errors;
×
NEW
235
                        std::cout << "FAIL " << name << "  x=" << x << "  y=" << y
×
NEW
236
                                  << "  our=" << our << "  ref=" << ref
×
NEW
237
                                  << " (our is not finite)\n";
×
NEW
238
                        return;
×
239
                }
240
                // Both finite: compute relative error (or absolute when ref == 0).
241
                double err = (ref == 0.0) ? std::abs(our) : std::abs((our - ref) / ref);
21✔
242
                if (err > tol) {
21✔
243
                        ++errors;
×
244
                        std::cout << "FAIL " << name << "  x=" << x << "  y=" << y
×
245
                                  << "  our=" << std::setprecision(17) << our
×
246
                                  << "  ref=" << ref
×
247
                                  << "  rel-err=" << err << '\n';
×
248
                }
249
        };
1✔
250

251
        // Sweep of (base, exponent) pairs against std::pow.
252
        struct Pair { double x; double y; };
253
        const Pair pts[] = {
1✔
254
                // Integer exponents (fast path)
255
                { 2.0,   10.0  }, { 2.0,  -10.0 }, { 0.5,  20.0 }, { 1.5,  7.0  },
256
                { 3.0,    4.0  }, { 10.0,  3.0  }, { 7.0,  -2.0 }, { 1.1,  100.0},
257
                // Negative base, integer exponent
258
                { -2.0,   3.0  }, { -2.0,   4.0 }, { -1.0, 7.0  }, { -1.5, 5.0  },
259
                // Non-integer exponent (transcendental path)
260
                { 2.0,    0.5  }, { 8.0, 1.0/3.0}, { 10.0, 0.301029995663981}, // ~ log10(2)
261
                { 1.5,    2.7  }, { 0.7,  -1.5  }, { 100.0,  0.5 },
262
                // Large dynamic range
263
                { 1e10,   2.0  }, { 1e-10, 3.0  }, { 1.001, 1000.0 },
264
        };
265
        for (auto p : pts) {
22✔
266
                double our = cm::pow(p.x, p.y);
21✔
267
                double ref = std::pow(p.x, p.y);
21✔
268
                check("double sweep", p.x, p.y, our, ref, 1e-13);
21✔
269
        }
270

271
        // Float sweep
272
        struct PairF { float x; float y; };
273
        const PairF fpts[] = {
1✔
274
                { 2.0f, 10.0f }, { 2.0f, -10.0f }, { 0.5f, 8.0f }, { 3.14f, 2.5f },
275
                { -2.0f, 3.0f }, { -1.0f, 100.0f }, { 1.5f, 0.5f },
276
        };
277
        for (auto p : fpts) {
8✔
278
                float our = cm::pow(p.x, p.y);
7✔
279
                float ref = std::pow(p.x, p.y);
7✔
280
                double err = (ref == 0.0f) ? std::abs(our) : std::abs((our - ref) / ref);
7✔
281
                if (err > 1e-5) {  // allow a bit more for float
7✔
282
                        ++errors;
×
283
                        std::cout << "FAIL float sweep  x=" << p.x << "  y=" << p.y
×
284
                                  << "  our=" << our << "  ref=" << ref
×
285
                                  << "  rel-err=" << err << '\n';
×
286
                }
287
        }
288

289
        std::cout << "constexpr_math::pow: " << (errors == 0 ? "PASS" : "FAIL")
1✔
290
                  << " (" << errors << " error" << (errors == 1 ? "" : "s") << ")\n";
1✔
291
        return (errors == 0) ? 0 : 1;
1✔
292
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc