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

stillwater-sc / universal / 26478780477

26 May 2026 10:28PM UTC coverage: 84.011% (-0.08%) from 84.087%
26478780477

push

github

web-flow
fix(ereal): deliver extended precision in mathlib transcendentals (#1002) (#1004)

* fix(ereal): deliver extended precision in mathlib transcendentals (#1002)

Root cause: every transcendental that used a math constant initialized it from
a double literal -- e.g. `Real pi(3.14159...100 digits...)` or `Real ln2(...)`.
A double literal carries only ~16 digits, so the long string was truncated
before the ereal saw it, capping sin/cos/exp2/exp10/log2/log10/... at double
precision regardless of maxlimbs.

Fix:
- ereal_constants.hpp: real high-precision constants. pi, ln2, ln10 are parsed
  from verified decimal strings (parse() builds a true multi-component
  expansion); everything else is derived via exact operations (scale by powers
  of two / small ints) or extended-precision algorithms (ereal sqrt/exp/divide
  are themselves extended-precision), so correctness follows from the algorithm
  rather than hand-transcribed tail digits. Each accessor caches its value.
- mathlib.hpp: include the constants header (it was dead code -- nothing
  included it, which is why the functions inlined their own truncated copies).
- exponent.hpp / logarithm.hpp / trigonometry.hpp: use the shared
  ereal_pi/pi_2/2pi/ln2/ln10 constants instead of inline double-literal copies.

Result (verified): sin/cos/exp/log now scale with maxlimbs --
sin^2 + cos^2 - 1 == 1.8e-133, exp(ln2) - 2 == 7e-120, exp(1) == e exactly,
two independent computations of cos(0.3) agree to ~101 digits.

progressive_precision.cpp: was failing 56 (then 24) precision-scaling checks --
but several of its hardcoded MPFR reference strings were generated incorrectly
(cos(0.3)'s was wrong beyond ~16 digits; the function is correct). Redesigned to
be self-referential: each ereal<N> is measured against the function evaluated at
a higher maxlimbs, with the reference level scaled by REGRESSION_LEVEL (L1
references ereal<8> for fast CI; L4 references ereal<19>). Added the missing
MANUAL_TESTING/REGRESSION_LEVEL guar... (continued)

41 of 43 new or added lines in 6 files covered. (95.35%)

232 existing lines in 14 files now uncovered.

46820 of 55731 relevant lines covered (84.01%)

6420694.79 hits per line

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

93.1
/internal/expansion/api/api.cpp
1
// api.cpp: API demonstration of expansion (multi-component) arithmetic
2
//
3
// This file is the curated, pedagogical entry point for the expansion API. It
4
// demonstrates the three error-free transformations (EFTs) that all expansion
5
// arithmetic is built on, making the "error-free" property explicit by
6
// reconstructing the exact result from the (head, tail) pair each one returns:
7
//
8
//     two_sum   :  a + b  ==  s + r      (sum  s, exact roundoff r)
9
//     two_prod  :  a * b  ==  p + e      (prod p, exact roundoff e)
10
//     two_div   :  a      ==  q*b + r    (quot q = fl(a/b), exact residual r)
11
//
12
// and then shows how these compose into multi-component expansions that carry
13
// far more than 53 bits of precision.
14
//
15
// Copyright (C) 2017 Stillwater Supercomputing, Inc.
16
// SPDX-License-Identifier: MIT
17
//
18
// This file is part of the universal numbers project, which is released under an MIT Open Source license.
19
#include <universal/utility/directives.hpp>
20
#include <universal/internal/expansion/expansion_ops.hpp>
21
#include <universal/number/einteger/einteger.hpp>
22
#include <universal/verification/test_case.hpp>
23
#include <iostream>
24
#include <iomanip>
25
#include <string>
26
#include <vector>
27
#include <cmath>
28

29
namespace {
30

31
        void print_expansion(const std::string& label, const std::vector<double>& e) {
8✔
32
                std::cout << "    " << std::left << std::setw(20) << label
8✔
33
                          << "[" << e.size() << " limb(s)] {";
8✔
34
                std::cout << std::setprecision(17) << std::scientific;
8✔
35
                for (size_t i = 0; i < e.size(); ++i) {
25✔
36
                        if (i) std::cout << ", ";
17✔
37
                        std::cout << e[i];
17✔
38
                }
39
                std::cout << "}\n";
8✔
40
        }
8✔
41

42
}  // anonymous namespace
43

44
int main()
1✔
45
try {
46
        using namespace sw::universal;
47
        using namespace sw::universal::expansion_ops;
48

49
        std::cout << "Expansion arithmetic API -- error-free transformations\n";
1✔
50
        std::cout << "======================================================\n\n";
1✔
51

52
        // ---------------------------------------------------------------------
53
        // TWO-SUM:  a + b == s + r exactly.  s is fl(a+b); r is the part that
54
        // rounding dropped.  Pick a + b that is NOT representable as one double.
55
        // ---------------------------------------------------------------------
56
        std::cout << "two_sum:  a + b == s + r   (s = fl(a+b), r = exact roundoff)\n";
1✔
57
        std::cout << "-----------------------------------------------------------\n";
1✔
58
        {
59
                double a = 9007199254740992.0;  // 2^53
1✔
60
                double b = 1.0;                 // 2^53 + 1 is NOT a double
1✔
61
                double s, r;
62
                two_sum(a, b, s, r);
1✔
63
                ReportValue(a, "a =", 25u, 17u);
2✔
64
                ReportValue(b, "b =", 25u, 17u);
2✔
65
                ReportValue(s, "s = fl(a+b) =", 25u, 17u);          // rounds back to 2^53; b appears lost
2✔
66
                ReportValue(r, "r = roundoff =", 25u, 17u);        // == 1.0: exactly what s dropped
2✔
67
                ReportValue(a+b, "naive (a+b) in double =", 25u, 17u);  // == s: the +1 is gone
1✔
68
                std::cout << "    => the single double a+b loses b, but the pair (s,r) holds 2^53+1 exactly.\n";
1✔
69
                einteger s_r = einteger(s) + einteger(r);
1✔
70
                einteger a_b = einteger(a) + einteger(b);
1✔
71
                ReportValue(s_r, "s + r =", 25, 17u);
2✔
72
                ReportValue(a_b, "a + b =", 25, 17u);
1✔
73
                bool     exact = (s_r == a_b);
1✔
74
                std::cout << "    => identity: s + r == a + b, reconstructed exactly (dyadic check: "
75
                          << (exact ? "holds" : "FAILS") << ").\n\n";
1✔
76
        }
1✔
77

78
        // A fractional case: 1 + 2^-53 (also not a double); r carries the tail.
79
        {
80
                double a = 1.0, b = std::ldexp(1.0, -53);
1✔
81
                double s, r;
82
                two_sum(a, b, s, r);
1✔
83
                ReportValue(a, "a =", 25, 17);
2✔
84
                ReportValue(b, "b = 2^-53 =", 25, 17);
2✔
85
                ReportValue(s, "s =", 25, 17);  // 1.0
2✔
86
                ReportValue(r, "r =", 25, 17);  // 2^-53, the dropped tail
1✔
87
                std::cout << "    => (s,r) = (1, 2^-53) represents 1 + 2^-53 exactly.\n\n";
1✔
88
        }
89

90
        // ---------------------------------------------------------------------
91
        // FAST-TWO-SUM:  same identity, 3 ops, valid only when |a| >= |b|.
92
        // ---------------------------------------------------------------------
93
        std::cout << "fast_two_sum:  a + b == s + r   (requires |a| >= |b|)\n";
1✔
94
        std::cout << "-----------------------------------------------------\n";
1✔
95
        {
96
                double a = 1.0e16, b = 7.0;   // |a| >= |b|
1✔
97
                double s, r;
98
                fast_two_sum(a, b, s, r);
1✔
99
                ReportValue(a, "a =", 25, 17);
2✔
100
                ReportValue(b, "b =", 25, 17);
2✔
101
                ReportValue(s, "s =", 25, 17);
2✔
102
                ReportValue(r, "r =", 25, 17);
1✔
103
                std::cout << "    => 3-op version; reuse only with magnitude-ordered operands.\n\n";
1✔
104
        }
105

106
        // ---------------------------------------------------------------------
107
        // TWO-PROD:  a * b == p + e exactly.  Squaring a rounded sqrt(2) shows the
108
        // product is not 2, and e captures the exact low-order bits.
109
        // ---------------------------------------------------------------------
110
        std::cout << "two_prod:  a * b == p + e   (p = fl(a*b), e = exact roundoff)\n";
1✔
111
        std::cout << "------------------------------------------------------------\n";
1✔
112
        {
113
                double a = std::sqrt(2.0);    // the rounded square root
1✔
114
                double b = a;
1✔
115
                double p, e;
116
                two_prod(a, b, p, e);
1✔
117
                ReportValue(a, "a = fl(sqrt 2) =", 25, 17);
2✔
118
                ReportValue(p, "p = fl(a*a) =", 25, 17);  // ~2, but not exactly 2
2✔
119
                ReportValue(e, "e = roundoff =", 25, 17);  // the bits below p
2✔
120
                ReportValue(p - 2.0, "p - 2.0 =", 25, 17);  // shows p != 2
1✔
121
                std::cout << "    => a*a is not 2; the exact product a*a == p + e (e holds the tail).\n\n";
1✔
122
        }
123
        {
124
                double a = 0.1, b = 0.3;      // neither is exact in binary
1✔
125
                double p, e;
126
                two_prod(a, b, p, e);
1✔
127
                ReportValue(a, "a =", 25, 17);
2✔
128
                ReportValue(b, "b =", 25, 17);
2✔
129
                ReportValue(p, "p = fl(a*b) =", 25, 17);
2✔
130
                ReportValue(e, "e = roundoff =", 25, 17);
1✔
131
                std::cout << "    => p + e is the exact product of the two stored doubles.\n\n";
1✔
132
        }
133

134
        // ---------------------------------------------------------------------
135
        // TWO-DIV (residual form):  a == q*b + r,  q = fl(a/b),  r = fma(-q,b,a).
136
        // fma computes a - q*b with a single rounding, so r is the EXACT residual
137
        // -- division's rounding error is recoverable even though a/b is inexact.
138
        // ---------------------------------------------------------------------
139
        std::cout << "two_div:  a == q*b + r   (q = fl(a/b), r = fma(-q,b,a) exact residual)\n";
1✔
140
        std::cout << "--------------------------------------------------------------------\n";
1✔
141
        {
142
                double a = 1.0, b = 3.0;
1✔
143
                double q = a / b;                 // rounded quotient ~0.3333...
1✔
144
                double r = std::fma(-q, b, a);    // exact residual a - q*b
1✔
145
                ReportValue(a, "a =", 25, 17);
2✔
146
                ReportValue(b, "b =", 25, 17);
2✔
147
                ReportValue(q, "q = fl(a/b) =", 25, 17);
2✔
148
                ReportValue(r, "r = a - q*b (exact) =", 25, 17);
2✔
149
                ReportValue(a - q * b, "naive a - q*b in double =", 25, 17);  // lossy; often 0
1✔
150
                double improved = q + r / b;      // one Newton-like correction step
1✔
151
                ReportValue(improved, "q + r/b (refined) =", 25, 17);
1✔
152
                std::cout << "    => r is the exact rounding error of the division (|r| < ulp).\n";
1✔
153
                std::cout << "    => fma recovers r exactly; the naive a - q*b cannot.\n\n";
1✔
154
        }
155

156
        // ---------------------------------------------------------------------
157
        // Composition: EFTs build multi-component expansions that carry the bits
158
        // a single double would drop.
159
        // ---------------------------------------------------------------------
160
        std::cout << "Composition into expansions\n";
1✔
161
        std::cout << "---------------------------\n";
1✔
162
        {
163
                // grow_expansion: fold a scalar into an expansion, keeping the roundoff.
164
                std::vector<double> e = { 3.0, 5.0e-16 };
3✔
165
                print_expansion("e =", e);
1✔
166
                std::vector<double> h = grow_expansion(e, 1.0);
1✔
167
                print_expansion("grow(e, 1.0) =", renormalize_expansion(h));
3✔
168
                std::cout << '\n';
1✔
169

170
                // linear_expansion_sum: add two expansions exactly, then renormalize.
171
                std::vector<double> x = { 1.0e16, 1.0 };
2✔
172
                std::vector<double> y = { 1.0e16, 3.0 };
3✔
173
                print_expansion("x =", x);
2✔
174
                print_expansion("y =", y);
1✔
175
                std::vector<double> z = renormalize_expansion(linear_expansion_sum(x, y));
1✔
176
                print_expansion("x + y =", z);
1✔
177
                std::cout << std::setprecision(17) << std::scientific;
1✔
178
                std::cout << "    estimate(x+y) = " << estimate(z)
1✔
179
                          << "   (carries the +4 a single double would lose)\n\n";
1✔
180

181
                // scale_expansion: expansion * scalar, exact via two_prod per limb.
182
                std::vector<double> s = scale_expansion(x, 3.0);
1✔
183
                print_expansion("x * 3.0 =", renormalize_expansion(s));
3✔
184
                std::cout << '\n';
1✔
185

186
                // invariants the expansion algorithms maintain: components are ordered by
187
                // decreasing magnitude and are non-overlapping. Non-overlap means the
188
                // binary exponent gap between adjacent limbs exceeds the 52-bit mantissa
189
                // width, so the limbs share no significand bits. (The canonical
190
                // structural check lives in the verification layer as check_priest_normal.)
191
                std::vector<double> ok  = { 2.0, std::ldexp(1.0, -60), std::ldexp(1.0, -120) };
2✔
192
                std::vector<double> bad = { 10.0, 0.1, 1.0 };  // not decreasing in magnitude
3✔
193
                print_expansion("well-formed:", ok);
1✔
194
                std::cout << "      is_decreasing_magnitude: " << (is_decreasing_magnitude(ok) ? "yes" : "no") << '\n';
1✔
195
                std::cout << "      adjacent exponent gaps (need > 52): ";
1✔
196
                for (size_t i = 1; i < ok.size(); ++i) std::cout << (std::ilogb(ok[i-1]) - std::ilogb(ok[i])) << ' ';
3✔
197
                std::cout << " -> non-overlapping\n";
1✔
198
                print_expansion("ill-formed:", bad);
1✔
199
                std::cout << "      is_decreasing_magnitude: " << (is_decreasing_magnitude(bad) ? "yes" : "no") << '\n';
1✔
200
        }
1✔
201

202
        std::cout << "\nAll API examples completed.\n";
1✔
203
        return EXIT_SUCCESS;
1✔
204
}
UNCOV
205
catch (const std::exception& e) {
×
UNCOV
206
        std::cerr << "Caught exception: " << e.what() << std::endl;
×
UNCOV
207
        return EXIT_FAILURE;
×
UNCOV
208
}
×
UNCOV
209
catch (...) {
×
UNCOV
210
        std::cerr << "Caught unknown exception" << std::endl;
×
UNCOV
211
        return EXIT_FAILURE;
×
UNCOV
212
}
×
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