Skip to content

Commit 697dbc7

Browse files
authored
Merge pull request #1389 from boostorg/1387
Fix cohort handling of zero to match IEEE 754 requirements
2 parents 2b920bf + 54c3fb1 commit 697dbc7

18 files changed

Lines changed: 301 additions & 51 deletions

.drone.jsonnet

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,34 @@ local windows_pipeline(name, image, environment, arch = "amd64") =
259259
"g++-14-multilib",
260260
),
261261

262+
linux_pipeline(
263+
"Linux 26.04 GCC 15 32",
264+
"cppalliance/droneubuntu2604:1",
265+
{ TOOLSET: 'gcc', COMPILER: 'g++-15', CXXSTD: '03,11,14,17,20,23', ADDRMD: '32', CXXFLAGS: "-fexcess-precision=fast" },
266+
"g++-15-multilib",
267+
),
268+
269+
linux_pipeline(
270+
"Linux 26.04 GCC 15 64",
271+
"cppalliance/droneubuntu2604:1",
272+
{ TOOLSET: 'gcc', COMPILER: 'g++-15', CXXSTD: '03,11,14,17,20,23', ADDRMD: '64', CXXFLAGS: "-fexcess-precision=fast" },
273+
"g++-15-multilib",
274+
),
275+
276+
linux_pipeline(
277+
"Linux 26.04 GCC 16 32",
278+
"cppalliance/droneubuntu2604:1",
279+
{ TOOLSET: 'gcc', COMPILER: 'g++-16', CXXSTD: '03,11,14,17,20,23', ADDRMD: '32', CXXFLAGS: "-fexcess-precision=fast" },
280+
"g++-16-multilib",
281+
),
282+
283+
linux_pipeline(
284+
"Linux 26.04 GCC 16 64",
285+
"cppalliance/droneubuntu2604:1",
286+
{ TOOLSET: 'gcc', COMPILER: 'g++-16', CXXSTD: '03,11,14,17,20,23', ADDRMD: '64', CXXFLAGS: "-fexcess-precision=fast" },
287+
"g++-16-multilib",
288+
),
289+
262290
linux_pipeline(
263291
"Linux 18.04 Clang 6.0",
264292
"cppalliance/droneubuntu1804:1",
@@ -316,16 +344,9 @@ local windows_pipeline(name, image, environment, arch = "amd64") =
316344
),
317345

318346
linux_pipeline(
319-
"Linux 22.04 Clang 14 UBSAN",
347+
"Linux 22.04 Clang 14",
320348
"cppalliance/droneubuntu2204:1",
321-
{ TOOLSET: 'clang', COMPILER: 'clang++-14', CXXSTD: '03,11,14,17,20,2b' } + ubsan,
322-
"clang-14",
323-
),
324-
325-
linux_pipeline(
326-
"Linux 22.04 Clang 14 ASAN",
327-
"cppalliance/droneubuntu2204:1",
328-
{ TOOLSET: 'clang', COMPILER: 'clang++-14', CXXSTD: '03,11,14,17,20,2b' } + asan,
349+
{ TOOLSET: 'clang', COMPILER: 'clang++-14', CXXSTD: '03,11,14,17,20,2b' },
329350
"clang-14",
330351
),
331352

@@ -350,31 +371,55 @@ local windows_pipeline(name, image, environment, arch = "amd64") =
350371
"cppalliance/droneubuntu2404:1",
351372
{ TOOLSET: 'clang', COMPILER: 'clang++-17', CXXSTD: '03,11,14,17,20,2b' },
352373
"clang-17",
353-
["deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-17 main"],
374+
["deb http://apt.llvm.org/noble/ llvm-toolchain-noble-17 main"],
354375
),
355376

356377
linux_pipeline(
357378
"Linux 24.04 Clang 18",
358379
"cppalliance/droneubuntu2404:1",
359380
{ TOOLSET: 'clang', COMPILER: 'clang++-18', CXXSTD: '03,11,14,17,20,2b' },
360381
"clang-18",
361-
["deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main"],
382+
["deb http://apt.llvm.org/noble/ llvm-toolchain-noble-18 main"],
362383
),
363384

364385
linux_pipeline(
365386
"Linux 24.04 Clang 19",
366387
"cppalliance/droneubuntu2404:1",
367388
{ TOOLSET: 'clang', COMPILER: 'clang++-19', CXXSTD: '03,11,14,17,20,2b' },
368389
"clang-19",
369-
["deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-19 main"],
390+
["deb http://apt.llvm.org/noble/ llvm-toolchain-noble-19 main"],
370391
),
371392

372393
linux_pipeline(
373394
"Linux 24.04 Clang 20",
374395
"cppalliance/droneubuntu2404:1",
375396
{ TOOLSET: 'clang', COMPILER: 'clang++-20', CXXSTD: '03,11,14,17,20,2b' },
376397
"clang-20",
377-
["deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-20 main"],
398+
["deb http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main"],
399+
),
400+
401+
linux_pipeline(
402+
"Linux 24.04 Clang 21",
403+
"cppalliance/droneubuntu2404:1",
404+
{ TOOLSET: 'clang', COMPILER: 'clang++-21', CXXSTD: '14,17,20,2b' },
405+
"clang-21",
406+
["deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"],
407+
),
408+
409+
linux_pipeline(
410+
"Linux 24.04 Clang 21 UBSAN",
411+
"cppalliance/droneubuntu2404:1",
412+
{ TOOLSET: 'clang', COMPILER: 'clang++-21', CXXSTD: '14,17,20,2b' } + ubsan,
413+
"clang-21",
414+
["deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"],
415+
),
416+
417+
linux_pipeline(
418+
"Linux 24.04 Clang 21 ASAN",
419+
"cppalliance/droneubuntu2404:1",
420+
{ TOOLSET: 'clang', COMPILER: 'clang++-21', CXXSTD: '14,17,20,2b' } + asan,
421+
"clang-21",
422+
["deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main"],
378423
),
379424

380425
windows_pipeline(

include/boost/decimal/charconv.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1071,7 +1071,7 @@ BOOST_DECIMAL_CUDA_CONSTEXPR auto to_chars_cohort_preserving_scientific(char* fi
10711071
using unsigned_integer = typename TargetDecimalType::significand_type;
10721072

10731073
const auto fp = fpclassify(value);
1074-
if (!(fp == FP_NORMAL || fp == FP_SUBNORMAL))
1074+
if (!(fp == FP_NORMAL || fp == FP_SUBNORMAL || fp == FP_ZERO))
10751075
{
10761076
// Cohorts are irrelevant for non-finite values
10771077
return to_chars_nonfinite(first, last, value, fp, chars_format::scientific, -1);

include/boost/decimal/decimal128_t.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,18 @@ BOOST_DECIMAL_CUDA_CONSTEXPR decimal128_t::decimal128_t(T1 coeff, T2 exp, const
725725

726726
if (reduced_coeff == zero)
727727
{
728+
// IEEE 754-2008 3.5.1: zero has a cohort with one representation per exponent.
729+
// Clamp the requested exponent to the representable range and encode it; sign was already set.
730+
auto zero_biased_exp {biased_exp};
731+
if (zero_biased_exp < 0)
732+
{
733+
zero_biased_exp = 0;
734+
}
735+
else if (zero_biased_exp > static_cast<int>(detail::d128_max_biased_exponent))
736+
{
737+
zero_biased_exp = static_cast<int>(detail::d128_max_biased_exponent);
738+
}
739+
bits_.high |= (static_cast<std::uint64_t>(zero_biased_exp) << detail::d128_not_11_exp_high_word_shift) & detail::d128_not_11_exp_mask;
728740
return;
729741
}
730742

include/boost/decimal/decimal32_t.hpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,18 @@ BOOST_DECIMAL_CUDA_CONSTEXPR decimal32_t::decimal32_t(T1 coeff, T2 exp, const de
642642

643643
if (reduced_coeff == 0U)
644644
{
645-
// Normalize our handling of zeros
645+
// IEEE 754-2008 3.5.1: zero has a cohort with one representation per exponent.
646+
// Clamp the requested exponent to the representable range and encode it; sign was already set.
647+
auto zero_biased_exp {biased_exp};
648+
if (zero_biased_exp < 0)
649+
{
650+
zero_biased_exp = 0;
651+
}
652+
else if (zero_biased_exp > static_cast<int>(detail::d32_max_biased_exponent))
653+
{
654+
zero_biased_exp = static_cast<int>(detail::d32_max_biased_exponent);
655+
}
656+
bits_ |= (static_cast<std::uint32_t>(zero_biased_exp) << detail::d32_not_11_exp_shift) & detail::d32_not_11_exp_mask;
646657
return;
647658
}
648659

include/boost/decimal/decimal64_t.hpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,18 @@ BOOST_DECIMAL_CUDA_CONSTEXPR decimal64_t::decimal64_t(T1 coeff, T2 exp, const de
636636

637637
if (reduced_coeff == 0U)
638638
{
639-
// Normalize our handling of zeros
639+
// IEEE 754-2008 3.5.1: zero has a cohort with one representation per exponent.
640+
// Clamp the requested exponent to the representable range and encode it; sign was already set.
641+
auto zero_biased_exp {biased_exp};
642+
if (zero_biased_exp < 0)
643+
{
644+
zero_biased_exp = 0;
645+
}
646+
else if (zero_biased_exp > static_cast<int>(detail::d64_max_biased_exponent))
647+
{
648+
zero_biased_exp = static_cast<int>(detail::d64_max_biased_exponent);
649+
}
650+
bits_ |= (static_cast<std::uint64_t>(zero_biased_exp) << detail::d64_not_11_exp_shift) & detail::d64_not_11_exp_mask;
640651
return;
641652
}
642653

include/boost/decimal/decimal_fast128_t.hpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -522,13 +522,32 @@ constexpr decimal_fast128_t::decimal_fast128_t(T1 coeff, T2 exp, const detail::c
522522
const auto is_negative {static_cast<bool>(resultant_sign)};
523523
sign_ = is_negative;
524524

525+
// IEEE 754-2008 3.5.1: zero has a cohort with one representation per exponent.
526+
// Skip normalization for zero (which would otherwise expand the significand and shift the exponent)
527+
// and clamp the requested exponent to the representable range.
528+
if (min_coeff == minimum_coefficient_size{0})
529+
{
530+
significand_ = static_cast<significand_type>(0);
531+
auto biased_exp {static_cast<int>(exp) + detail::bias_v<decimal_fast128_t>};
532+
if (biased_exp < 0)
533+
{
534+
biased_exp = 0;
535+
}
536+
else if (biased_exp > detail::max_biased_exp_v<decimal_fast128_t>)
537+
{
538+
biased_exp = detail::max_biased_exp_v<decimal_fast128_t>;
539+
}
540+
exponent_ = static_cast<exponent_type>(biased_exp);
541+
return;
542+
}
543+
525544
// Normalize the significand in the constructor, so we don't have
526545
// to calculate the number of digits for operations
527546
detail::normalize<decimal_fast128_t>(min_coeff, exp, is_negative);
528547

529548
significand_ = static_cast<significand_type>(min_coeff);
530549

531-
const auto biased_exp {significand_ == 0U ? 0 : exp + detail::bias_v<decimal_fast128_t>};
550+
const auto biased_exp {static_cast<int>(exp) + detail::bias_v<decimal_fast128_t>};
532551

533552
if (biased_exp > detail::max_biased_exp_v<decimal_fast128_t>)
534553
{
@@ -540,10 +559,9 @@ constexpr decimal_fast128_t::decimal_fast128_t(T1 coeff, T2 exp, const detail::c
540559
}
541560
else
542561
{
543-
// Flush denorms to zero
562+
// Flush denorms to zero, preserving sign per IEEE 754-2008 3.5.1
544563
significand_ = static_cast<significand_type>(0);
545-
exponent_ = static_cast<exponent_type>(detail::bias_v<decimal_fast128_t>);
546-
sign_ = false;
564+
exponent_ = static_cast<exponent_type>(0);
547565
}
548566
}
549567

include/boost/decimal/decimal_fast32_t.hpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -516,12 +516,31 @@ constexpr decimal_fast32_t::decimal_fast32_t(T1 coeff, T2 exp, const detail::con
516516
const auto is_negative {static_cast<bool>(resultant_sign)};
517517
sign_ = is_negative;
518518

519+
// IEEE 754-2008 3.5.1: zero has a cohort with one representation per exponent.
520+
// Skip normalization for zero (which would otherwise expand the significand and shift the exponent)
521+
// and clamp the requested exponent to the representable range.
522+
if (min_coeff == minimum_coefficient_size{0})
523+
{
524+
significand_ = static_cast<significand_type>(0);
525+
auto biased_exp {static_cast<int>(exp) + detail::bias};
526+
if (biased_exp < 0)
527+
{
528+
biased_exp = 0;
529+
}
530+
else if (biased_exp > detail::max_biased_exp_v<decimal_fast32_t>)
531+
{
532+
biased_exp = detail::max_biased_exp_v<decimal_fast32_t>;
533+
}
534+
exponent_ = static_cast<exponent_type>(biased_exp);
535+
return;
536+
}
537+
519538
// Normalize in the constructor, so we never have to worry about it again
520539
detail::normalize<decimal_fast32_t>(min_coeff, exp, is_negative);
521540

522541
significand_ = static_cast<significand_type>(min_coeff);
523542

524-
const auto biased_exp {significand_ == 0U ? 0 : exp + detail::bias};
543+
const auto biased_exp {static_cast<int>(exp) + detail::bias};
525544

526545
// decimal32_t exponent holds 8 bits
527546
if (biased_exp > detail::max_biased_exp_v<decimal_fast32_t>)
@@ -534,10 +553,9 @@ constexpr decimal_fast32_t::decimal_fast32_t(T1 coeff, T2 exp, const detail::con
534553
}
535554
else
536555
{
537-
// Flush denorms to zero
556+
// Flush denorms to zero, preserving sign per IEEE 754-2008 3.5.1
538557
significand_ = static_cast<significand_type>(0);
539-
exponent_ = static_cast<exponent_type>(detail::bias);
540-
sign_ = false;
558+
exponent_ = static_cast<exponent_type>(0);
541559
}
542560
}
543561

include/boost/decimal/decimal_fast64_t.hpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -525,12 +525,31 @@ constexpr decimal_fast64_t::decimal_fast64_t(T1 coeff, T2 exp, const detail::con
525525
const auto is_negative {static_cast<bool>(resultant_sign)};
526526
sign_ = is_negative;
527527

528+
// IEEE 754-2008 3.5.1: zero has a cohort with one representation per exponent.
529+
// Skip normalization for zero (which would otherwise expand the significand and shift the exponent)
530+
// and clamp the requested exponent to the representable range.
531+
if (min_coeff == minimum_coefficient_size{0})
532+
{
533+
significand_ = static_cast<significand_type>(0);
534+
auto biased_exp {static_cast<int>(exp) + detail::bias_v<decimal64_t>};
535+
if (biased_exp < 0)
536+
{
537+
biased_exp = 0;
538+
}
539+
else if (biased_exp > detail::max_biased_exp_v<decimal64_t>)
540+
{
541+
biased_exp = detail::max_biased_exp_v<decimal64_t>;
542+
}
543+
exponent_ = static_cast<exponent_type>(biased_exp);
544+
return;
545+
}
546+
528547
// Normalize the value, so we don't have to worry about it with operations
529548
detail::normalize<decimal_fast64_t>(min_coeff, exp, is_negative);
530549

531550
significand_ = static_cast<significand_type>(min_coeff);
532551

533-
const auto biased_exp {significand_ == 0U ? 0 : exp + detail::bias_v<decimal64_t>};
552+
const auto biased_exp {static_cast<int>(exp) + detail::bias_v<decimal64_t>};
534553

535554
if (biased_exp > detail::max_biased_exp_v<decimal64_t>)
536555
{
@@ -542,10 +561,9 @@ constexpr decimal_fast64_t::decimal_fast64_t(T1 coeff, T2 exp, const detail::con
542561
}
543562
else
544563
{
545-
// Flush denorms to zero
564+
// Flush denorms to zero, preserving sign per IEEE 754-2008 3.5.1
546565
significand_ = static_cast<significand_type>(0);
547-
exponent_ = static_cast<exponent_type>(detail::bias_v<decimal64_t>);
548-
sign_ = false;
566+
exponent_ = static_cast<exponent_type>(0);
549567
}
550568
}
551569

include/boost/decimal/detail/add_impl.hpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,28 @@ BOOST_DECIMAL_CUDA_CONSTEXPR auto add_impl(const T& lhs, const T& rhs) noexcept
4242
auto lhs_exp {lhs.biased_exponent()};
4343
auto rhs_exp {rhs.biased_exponent()};
4444

45+
// IEEE 754-2008 3.5.1: zero has a cohort with one representation per exponent.
46+
// The exponent-comparison logic below assumes both operands have full precision
47+
// (which expand_significand normalizes), but a cohorted zero has no precision to
48+
// expand and its exponent can be arbitrarily large or small. Short-circuit so the
49+
// result is the non-zero operand (or zero with a sensible cohort if both are zero).
50+
if (big_lhs == 0U && big_rhs == 0U)
51+
{
52+
// IEEE 754-2008 6.1: preferred quantum for the sum of two zeros is min(exp_x, exp_y).
53+
// IEEE 754-2008 6.3: sum of opposite-sign zeros is +0 in default rounding.
54+
const auto result_exp {lhs_exp < rhs_exp ? lhs_exp : rhs_exp};
55+
const bool result_sign {lhs.isneg() && rhs.isneg()};
56+
return ReturnType{lhs.full_significand(), result_exp, result_sign};
57+
}
58+
if (big_lhs == 0U)
59+
{
60+
return ReturnType{rhs.full_significand(), rhs.biased_exponent(), rhs.isneg()};
61+
}
62+
if (big_rhs == 0U)
63+
{
64+
return ReturnType{lhs.full_significand(), lhs.biased_exponent(), lhs.isneg()};
65+
}
66+
4567
// Align to larger exponent
4668
if (lhs_exp != rhs_exp)
4769
{
@@ -212,6 +234,27 @@ BOOST_DECIMAL_CUDA_CONSTEXPR auto d128_add_impl_new(const T& lhs, const T& rhs)
212234
promoted_sig_type promoted_lhs {big_lhs};
213235
promoted_sig_type promoted_rhs {big_rhs};
214236

237+
// IEEE 754-2008 3.5.1: zero has a cohort with one representation per exponent.
238+
// The exponent-comparison alignment below mishandles a cohorted zero whose exp
239+
// is far from the non-zero operand's exp. Short-circuit so the result is the
240+
// non-zero operand (or zero with a sensible cohort if both are zero).
241+
if (big_lhs == typename T::significand_type{0} && big_rhs == typename T::significand_type{0})
242+
{
243+
// IEEE 754-2008 6.1: preferred quantum for the sum of two zeros is min(exp_x, exp_y).
244+
// IEEE 754-2008 6.3: sum of opposite-sign zeros is +0 in default rounding.
245+
const auto result_exp {lhs_exp < rhs_exp ? lhs_exp : rhs_exp};
246+
const bool result_sign {lhs.isneg() && rhs.isneg()};
247+
return ReturnType{lhs.full_significand(), result_exp, result_sign};
248+
}
249+
if (big_lhs == typename T::significand_type{0})
250+
{
251+
return ReturnType{rhs.full_significand(), rhs.biased_exponent(), rhs.isneg()};
252+
}
253+
if (big_rhs == typename T::significand_type{0})
254+
{
255+
return ReturnType{lhs.full_significand(), lhs.biased_exponent(), lhs.isneg()};
256+
}
257+
215258
// Align to larger exponent
216259
if (lhs_exp != rhs_exp)
217260
{

include/boost/decimal/detail/comparison.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ BOOST_DECIMAL_FORCE_INLINE BOOST_DECIMAL_CUDA_CONSTEXPR auto equality_impl(Decim
6262
return (lhs_sig == 0U && rhs_sig == 0U);
6363
}
6464

65+
// Step 4b: Same-sign zeros from any cohort compare equal regardless of their exponents
66+
// (IEEE 754-2008 3.5.1). Without this, two zeros with delta_exp greater than the type's
67+
// precision would fall through to the early-return below and wrongly compare unequal.
68+
if (lhs_sig == 0U && rhs_sig == 0U)
69+
{
70+
return true;
71+
}
72+
6573
// Step 5: Check the exponents
6674
// If the difference is greater than we can represent in the significand than we can assume they are different
6775
const auto lhs_exp {lhs_components.exp};

0 commit comments

Comments
 (0)