From a441f3d8f64981cd6382dd894b4e99525c993716 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Wed, 21 Jan 2026 11:48:05 -0500 Subject: [PATCH 1/6] Add minimal reproducer test set --- test/Jamfile | 1 + test/github_issue_1306.cpp | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 test/github_issue_1306.cpp diff --git a/test/Jamfile b/test/Jamfile index 2696639a1..e439f7ebf 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -86,6 +86,7 @@ run github_issue_1294.cpp ; run github_issue_1299.cpp ; run github_issue_1302.cpp ; run github_issue_1304.cpp ; +run github_issue_1306.cpp ; run link_1.cpp link_2.cpp link_3.cpp ; run quick.cpp ; diff --git a/test/github_issue_1306.cpp b/test/github_issue_1306.cpp new file mode 100644 index 000000000..3ed98563d --- /dev/null +++ b/test/github_issue_1306.cpp @@ -0,0 +1,19 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt +// +// See: https://github.com/cppalliance/decimal/issues/1306 + +#include +#include +#include + +using namespace boost::decimal; + +int main() +{ + BOOST_TEST_EQ(decimal64_t{"1.3e-394"} * decimal64_t{"1e-4"}, decimal64_t{"1e-398"}); + BOOST_TEST_EQ(decimal64_t{"1.5e-394"} * decimal64_t{"1e-4"}, decimal64_t{"2e-398"}); + + return boost::report_errors(); +} From 1fdf88830e15bfe9b697b8eb82a1bb92f3d55e1c Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Wed, 21 Jan 2026 12:08:17 -0500 Subject: [PATCH 2/6] Add handling of need to round subnormals in construction --- include/boost/decimal/decimal64_t.hpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/include/boost/decimal/decimal64_t.hpp b/include/boost/decimal/decimal64_t.hpp index a510705a2..8c7fb6e99 100644 --- a/include/boost/decimal/decimal64_t.hpp +++ b/include/boost/decimal/decimal64_t.hpp @@ -745,6 +745,23 @@ constexpr decimal64_t::decimal64_t(T1 coeff, T2 exp, const detail::construction_ { reduced_coeff *= detail::pow10(static_cast(biased_exp)); } + else if (biased_exp < 0) + { + const auto pos_biased_exp {-biased_exp}; + bool sticky {false}; + if (pos_biased_exp > 1) + { + // Need to ensure that we are following the current global rounding mode when packing subnormals + const auto shift_pow_10 {detail::pow10(static_cast(pos_biased_exp - 1))}; + const auto div_res {detail::impl::divmod(reduced_coeff, shift_pow_10)}; + reduced_coeff = div_res.quotient; + sticky = div_res.remainder != 0U; + } + // We may have to round the value so that it fits correctly + // e.g. 13e-399 -> 1e-398 + detail::fenv_round(reduced_coeff, is_negative, sticky); + } + bits_ |= reduced_coeff; } else if (digit_delta < 0 && coeff_digits - digit_delta <= detail::precision_v) From 53694cbee739aa108498e1aef270acba4746b3e1 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Wed, 21 Jan 2026 12:08:33 -0500 Subject: [PATCH 3/6] Add testing of known sticky bit subnormal values --- test/github_issue_1306.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/github_issue_1306.cpp b/test/github_issue_1306.cpp index 3ed98563d..86efaedea 100644 --- a/test/github_issue_1306.cpp +++ b/test/github_issue_1306.cpp @@ -15,5 +15,8 @@ int main() BOOST_TEST_EQ(decimal64_t{"1.3e-394"} * decimal64_t{"1e-4"}, decimal64_t{"1e-398"}); BOOST_TEST_EQ(decimal64_t{"1.5e-394"} * decimal64_t{"1e-4"}, decimal64_t{"2e-398"}); + BOOST_TEST_EQ(decimal64_t{"1234e-401"}, decimal64_t{"1e-398"}); + BOOST_TEST_EQ(decimal64_t{"1999e-401"}, decimal64_t{"2e-398"}); + return boost::report_errors(); } From 3f24b13e649cc23683dce4e33a12f2111d5ba387 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Wed, 21 Jan 2026 13:20:19 -0500 Subject: [PATCH 4/6] Make tests generic --- test/github_issue_1306.cpp | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/test/github_issue_1306.cpp b/test/github_issue_1306.cpp index 86efaedea..18d005341 100644 --- a/test/github_issue_1306.cpp +++ b/test/github_issue_1306.cpp @@ -10,13 +10,27 @@ using namespace boost::decimal; -int main() +template +void test() { - BOOST_TEST_EQ(decimal64_t{"1.3e-394"} * decimal64_t{"1e-4"}, decimal64_t{"1e-398"}); - BOOST_TEST_EQ(decimal64_t{"1.5e-394"} * decimal64_t{"1e-4"}, decimal64_t{"2e-398"}); + const T downward {13, detail::etiny_v + 3}; + BOOST_TEST_EQ(downward * T{"1e-4"}, std::numeric_limits::denorm_min()); + + const T upward {15, detail::etiny_v + 3}; + BOOST_TEST_EQ(upward * T{"1e-4"}, T(2, detail::etiny_v)); - BOOST_TEST_EQ(decimal64_t{"1234e-401"}, decimal64_t{"1e-398"}); - BOOST_TEST_EQ(decimal64_t{"1999e-401"}, decimal64_t{"2e-398"}); + const T non_rounded {1234, detail::etiny_v - 3}; + BOOST_TEST_EQ(non_rounded, std::numeric_limits::denorm_min()); + + const T rounded {1999, detail::etiny_v - 3}; + BOOST_TEST_EQ(rounded, T(2, detail::etiny_v)); +} + +int main() +{ + test(); + test(); + test(); return boost::report_errors(); } From 49d0d751e4c5cac61d349c103a26feaae437a87b Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Wed, 21 Jan 2026 13:22:23 -0500 Subject: [PATCH 5/6] Correct d32 and d128 --- include/boost/decimal/decimal128_t.hpp | 17 +++++++++++++++++ include/boost/decimal/decimal32_t.hpp | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/include/boost/decimal/decimal128_t.hpp b/include/boost/decimal/decimal128_t.hpp index 6e9693511..4daf10730 100644 --- a/include/boost/decimal/decimal128_t.hpp +++ b/include/boost/decimal/decimal128_t.hpp @@ -815,6 +815,23 @@ constexpr decimal128_t::decimal128_t(T1 coeff, T2 exp, const detail::constructio { reduced_coeff *= detail::pow10(static_cast(biased_exp)); } + else if (biased_exp < 0) + { + const auto pos_biased_exp {-biased_exp}; + bool sticky {false}; + if (pos_biased_exp > 1) + { + // Need to ensure that we are following the current global rounding mode when packing subnormals + const auto shift_pow_10 {detail::pow10(static_cast(pos_biased_exp - 1))}; + const auto div_res {detail::impl::divmod(reduced_coeff, shift_pow_10)}; + reduced_coeff = div_res.quotient; + sticky = div_res.remainder != 0U; + } + // We may have to round the value so that it fits correctly + // e.g. 13e-399 -> 1e-398 + detail::fenv_round(reduced_coeff, is_negative, sticky); + } + bits_ = reduced_coeff; bits_.high |= is_negative ? detail::d128_sign_mask : UINT64_C(0); // Reset the sign bit } diff --git a/include/boost/decimal/decimal32_t.hpp b/include/boost/decimal/decimal32_t.hpp index 0018665b6..735ecc0b7 100644 --- a/include/boost/decimal/decimal32_t.hpp +++ b/include/boost/decimal/decimal32_t.hpp @@ -749,6 +749,23 @@ constexpr decimal32_t::decimal32_t(T1 coeff, T2 exp, const detail::construction_ { reduced_coeff *= detail::pow10(static_cast(biased_exp)); } + else if (biased_exp < 0) + { + const auto pos_biased_exp {-biased_exp}; + bool sticky {false}; + if (pos_biased_exp > 1) + { + // Need to ensure that we are following the current global rounding mode when packing subnormals + const auto shift_pow_10 {detail::pow10(static_cast(pos_biased_exp - 1))}; + const auto div_res {detail::impl::divmod(reduced_coeff, shift_pow_10)}; + reduced_coeff = div_res.quotient; + sticky = div_res.remainder != 0U; + } + // We may have to round the value so that it fits correctly + // e.g. 13e-399 -> 1e-398 + detail::fenv_round(reduced_coeff, is_negative, sticky); + } + bits_ |= reduced_coeff; } else if (digit_delta < 0 && coeff_digits - digit_delta <= detail::precision) From 5d83a8a7260deaba724d01c57225c826c01a4f7d Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Wed, 21 Jan 2026 14:14:54 -0500 Subject: [PATCH 6/6] Fix next for absolute minimum representable values --- include/boost/decimal/detail/cmath/next.hpp | 8 +++++++- test/github_issue_1105.cpp | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/include/boost/decimal/detail/cmath/next.hpp b/include/boost/decimal/detail/cmath/next.hpp index fbd15c8bd..7ebcbc930 100644 --- a/include/boost/decimal/detail/cmath/next.hpp +++ b/include/boost/decimal/detail/cmath/next.hpp @@ -57,7 +57,13 @@ constexpr auto nextafter_impl(const DecimalType val, const bool direction) noexc sig = removed_zeros.trimmed_number; exp += static_cast(removed_zeros.number_of_removed_zeros); - if (removed_zeros.number_of_removed_zeros > 0) + if (exp == detail::etiny_v) + { + // If we are at the absolute minimum just add to the sig + // e.g. 2e-101 < 1.1e-100 + ++sig; + } + else if (removed_zeros.number_of_removed_zeros > 0) { // We need to shift an add // 1 -> 11 instead of 2 since 11e-101 < 2e-100 starting at 1e-100 diff --git a/test/github_issue_1105.cpp b/test/github_issue_1105.cpp index 2baf85f64..bcd501cfe 100644 --- a/test/github_issue_1105.cpp +++ b/test/github_issue_1105.cpp @@ -61,7 +61,7 @@ void test_non_preserving() BOOST_TEST_LE(two_val, next); const auto nines_value = decimal32_t{"99e-101"}; - const auto next_nines_res = decimal32_t{"991e-102"}; + const auto next_nines_res = decimal32_t{"100e-101"}; const auto res = nextafter(nines_value, one); BOOST_TEST_EQ(res, next_nines_res);