Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 7 additions & 52 deletions include/boost/decimal/decimal128_t.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,12 @@ BOOST_DECIMAL_CUDA_CONSTEXPR decimal128_t::decimal128_t(T1 coeff, T2 exp, const
coeff_digits = detail::coefficient_rounding<decimal128_t>(coeff, exp, biased_exp, is_negative, detail::num_digits(coeff));
}
}
else if (biased_exp < -(detail::precision_v<decimal128_t> - 1))
{
// A narrow coefficient far below the range also needs this rounding,
// else the pow10 call below reads past the end of its table
coeff_digits = detail::coefficient_rounding<decimal128_t>(coeff, exp, biased_exp, is_negative, detail::num_digits(coeff));
}

constexpr int128::uint128_t zero {0, 0};
auto reduced_coeff {static_cast<significand_type>(coeff)};
Expand Down Expand Up @@ -813,58 +819,7 @@ BOOST_DECIMAL_CUDA_CONSTEXPR decimal128_t::decimal128_t(T1 coeff, T2 exp, const

const auto exp_delta {biased_exp - static_cast<int>(detail::d128_max_biased_exponent)};
const auto digit_delta {coeff_digits - exp_delta};
if (biased_exp < 0 && coeff_digits == 1)
{
// This needs to be flushed to 0 or rounded to subnormal min
rounding_mode current_round_mode {_boost_decimal_global_rounding_mode};

#ifndef BOOST_DECIMAL_NO_CONSTEVAL_DETECTION

if (!BOOST_DECIMAL_IS_CONSTANT_EVALUATED(coeff))
{
current_round_mode = _boost_decimal_global_runtime_rounding_mode;
}

#endif

bool round {false};
if (biased_exp == -1)
{
switch (current_round_mode)
{
case rounding_mode::fe_dec_to_nearest_from_zero:
BOOST_DECIMAL_FALLTHROUGH
case rounding_mode::fe_dec_to_nearest:
if (reduced_coeff >= 5U)
{
round = true;
}
break;
case rounding_mode::fe_dec_upward:
if (!is_negative && reduced_coeff != 0U)
{
round = true;
}
break;
default:
round = false;
break;
}
}

if (round)
{
// Subnormal min is just 1
bits_ = UINT64_C(1);
}
else
{
bits_ = UINT64_C(0);
}

bits_.high |= is_negative ? detail::d128_sign_mask : UINT64_C(0);
}
else if (digit_delta > 0 && coeff_digits + digit_delta <= detail::precision_v<decimal128_t>)
if (digit_delta > 0 && coeff_digits + digit_delta <= detail::precision_v<decimal128_t>)
{
// Same overflow-fold pattern as d32/d64: post-shift coeff is <= max_significand_v
// and biased_exp lands in [0, max], so pack_in_range routes to direct_pack.
Expand Down
53 changes: 1 addition & 52 deletions include/boost/decimal/decimal32_t.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -703,58 +703,7 @@ BOOST_DECIMAL_CUDA_CONSTEXPR decimal32_t::decimal32_t(T1 coeff, T2 exp, const de

const auto exp_delta {biased_exp - static_cast<int>(detail::d32_max_biased_exponent)};
const auto digit_delta {coeff_digits - exp_delta};
if (biased_exp < 0 && coeff_digits == 1)
{
// This needs to be flushed to 0 or rounded to subnormal min
rounding_mode current_round_mode {_boost_decimal_global_rounding_mode};

#ifndef BOOST_DECIMAL_NO_CONSTEVAL_DETECTION

if (!BOOST_DECIMAL_IS_CONSTANT_EVALUATED(coeff))
{
current_round_mode = _boost_decimal_global_runtime_rounding_mode;
}

#endif

bool round {false};
if (biased_exp == -1)
{
switch (current_round_mode)
{
case rounding_mode::fe_dec_to_nearest_from_zero:
BOOST_DECIMAL_FALLTHROUGH
case rounding_mode::fe_dec_to_nearest:
if (reduced_coeff >= 5U)
{
round = true;
}
break;
case rounding_mode::fe_dec_upward:
if (!is_negative && reduced_coeff != 0)
{
round = true;
}
break;
default:
round = false;
break;
}
}

if (round)
{
// Subnormal min is just 1
bits_ = UINT32_C(1);
}
else
{
bits_ = UINT32_C(0);
}

bits_ |= is_negative ? detail::d32_sign_mask : UINT32_C(0);
}
else if (digit_delta > 0 && coeff_digits + digit_delta <= detail::precision)
if (digit_delta > 0 && coeff_digits + digit_delta <= detail::precision)
{
// After the shift, coeff has coeff_digits+digit_delta <= precision digits
// (so coeff <= max_significand_v) and biased_exp lands in [0, max] because
Expand Down
54 changes: 1 addition & 53 deletions include/boost/decimal/decimal64_t.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -743,59 +743,7 @@ BOOST_DECIMAL_CUDA_CONSTEXPR decimal64_t::decimal64_t(T1 coeff, T2 exp, const de

const auto exp_delta {biased_exp - static_cast<int>(detail::d64_max_biased_exponent)};
const auto digit_delta {coeff_digits - exp_delta};
if (biased_exp < 0 && coeff_digits == 1)
{
// This needs to be flushed to 0 or rounded to subnormal min
// e.g. 7e-399 should not become 70e-398 but 7e-400 should become 0
rounding_mode current_round_mode {_boost_decimal_global_rounding_mode};

#ifndef BOOST_DECIMAL_NO_CONSTEVAL_DETECTION

if (!BOOST_DECIMAL_IS_CONSTANT_EVALUATED(coeff))
{
current_round_mode = _boost_decimal_global_runtime_rounding_mode;
}

#endif

bool round {false};
if (biased_exp == -1)
{
switch (current_round_mode)
{
case rounding_mode::fe_dec_to_nearest_from_zero:
BOOST_DECIMAL_FALLTHROUGH
case rounding_mode::fe_dec_to_nearest:
if (reduced_coeff >= 5U)
{
round = true;
}
break;
case rounding_mode::fe_dec_upward:
if (!is_negative && reduced_coeff != 0)
{
round = true;
}
break;
default:
round = false;
break;
}
}

if (round)
{
// Subnormal min is just 1
bits_ = UINT64_C(1);
}
else
{
bits_ = UINT64_C(0);
}

bits_ |= is_negative ? detail::d64_sign_mask : UINT64_C(0);
}
else if (digit_delta > 0 && coeff_digits + digit_delta <= detail::precision_v<decimal64_t>)
if (digit_delta > 0 && coeff_digits + digit_delta <= detail::precision_v<decimal64_t>)
{
// Coeff stays in range (<= max_significand_v) by the branch's digit budget,
// and biased_exp lands in [0, max] by construction. pack_in_range hits
Expand Down
12 changes: 9 additions & 3 deletions include/boost/decimal/detail/fenv_rounding.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -717,8 +717,13 @@ BOOST_DECIMAL_CUDA_CONSTEXPR auto coefficient_rounding(T1& coeff, T2& exp, T3& b

if (BOOST_DECIMAL_UNLIKELY(shift > std::numeric_limits<T1>::digits10))
{
// Bounds check for our tables in pow10
coeff = 0;
// Bounds check for our tables in pow10. All digits drop, so round a zero with a sticky
// bit in the current mode. A directed mode then gives the smallest subnormal.
demoted_integer_type zero_coeff {0U};
const auto removed_digits {detail::fenv_round<TargetDecimalType>(zero_coeff, sign, coeff != 0U)};
coeff = static_cast<T1>(zero_coeff);
exp += removed_digits + shift;
biased_exp += removed_digits + shift;
return 1;
}

Expand All @@ -745,7 +750,8 @@ BOOST_DECIMAL_CUDA_CONSTEXPR auto coefficient_rounding(T1& coeff, T2& exp, T3& b
// so prefer the int compare over a 256-bit compare.
// This is slightly more conservative for the narrow band of (digits10+1)-digit values that fit in the
// demoted type by virtue of its high-bit slack which land in the wide-divmod branch.
if (coeff_digits <= std::numeric_limits<demoted_integer_type>::digits10)
if (coeff_digits <= std::numeric_limits<demoted_integer_type>::digits10 &&
shift <= std::numeric_limits<demoted_integer_type>::digits10)
{
const auto smaller_coeff {static_cast<demoted_integer_type>(coeff)};
const auto smaller_pow10 {static_cast<demoted_integer_type>(shift_pow_ten)};
Expand Down
1 change: 1 addition & 0 deletions test/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ run github_issue_1459_toward_zero.cpp
: : : <pch>off ;
run github_issue_1467.cpp ;
run github_issue_1473.cpp ;
run github_issue_1476.cpp ;

run link_1.cpp link_2.cpp link_3.cpp ;
run quick.cpp ;
Expand Down
85 changes: 85 additions & 0 deletions test/github_issue_1476.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2026 Shen-Ta Hsieh
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/decimal.hpp>
#include <boost/core/lightweight_test.hpp>
#include <limits>

using namespace boost::decimal;

// A value below the smallest subnormal rounds in the current mode: a one digit significand, a
// value whose digits all drop, and a coefficient type narrower or wider than the significand.
template <typename T>
void test()
{
using sig_type = typename T::significand_type;
using wide_type = decimal128_t::significand_type;
constexpr int etiny {std::numeric_limits<T>::min_exponent10 - std::numeric_limits<T>::digits10 + 1};
const T zero {0};

// Half of the smallest step is a tie, and the even neighbour is zero
fesetround(rounding_mode::fe_dec_to_nearest);
BOOST_TEST_EQ(T(sig_type{5}, etiny - 1), zero);
BOOST_TEST_EQ(T(sig_type{5}, etiny - 1, true), -zero);
BOOST_TEST(signbit(T(sig_type{5}, etiny - 1, true)));
BOOST_TEST_EQ(T(sig_type{5}, etiny + 1) * T(sig_type{1}, -2), zero);
// An int coefficient far below: decimal128_t read past the pow10 table here
BOOST_TEST_EQ(T(11, etiny - 70), zero);
BOOST_TEST_EQ(T(10, etiny - 55), zero);
// A 128-bit coefficient far below: the divisor of 32 and 64-bit types lost its high bits
BOOST_TEST_EQ(T(wide_type{123}, etiny - 33), zero);
BOOST_TEST_EQ(T(wide_type{UINT64_C(3976006815679288586)}, etiny - 38), zero);

#ifndef BOOST_DECIMAL_NO_CONSTEVAL_DETECTION
// fesetround has an effect only with the detection of constant evaluation
const T smallest {std::numeric_limits<T>::denorm_min()};

// A positive value goes up to the smallest step, also when all its digits drop
fesetround(rounding_mode::fe_dec_upward);
BOOST_TEST_EQ(T(sig_type{1}, etiny - 2), smallest);
BOOST_TEST_EQ(T(sig_type{1}, etiny + 1) * T(sig_type{1}, -3), smallest);
BOOST_TEST_EQ(T(sig_type{11}, etiny - 70), smallest);
BOOST_TEST_EQ(T(11, etiny - 70), smallest);
BOOST_TEST_EQ(T(wide_type{123}, etiny - 33), smallest);
// A zero stays zero
BOOST_TEST_EQ(T(sig_type{0}, etiny - 70), zero);
BOOST_TEST_EQ(T(sig_type{5}, etiny + 5) * T(sig_type{0}, -70), zero);

// A negative value goes down to minus the smallest step, also when all its digits drop
fesetround(rounding_mode::fe_dec_downward);
BOOST_TEST_EQ(T(sig_type{1}, etiny - 2, true), -smallest);
BOOST_TEST_EQ(T(sig_type{7}, etiny - 1, true), -smallest);
BOOST_TEST_EQ(T(sig_type{1}, etiny + 1, true) * T(sig_type{1}, -3), -smallest);
BOOST_TEST_EQ(T(sig_type{11}, etiny - 70, true), -smallest);

// A tie goes away from zero, but a value less than half of the smallest step goes to zero
fesetround(rounding_mode::fe_dec_to_nearest_from_zero);
BOOST_TEST_EQ(T(sig_type{5}, etiny - 1), smallest);
BOOST_TEST_EQ(T(sig_type{5}, etiny - 1, true), -smallest);
BOOST_TEST_EQ(T(sig_type{4}, etiny - 1), zero);
BOOST_TEST_EQ(T(sig_type{5}, etiny + 1) * T(sig_type{1}, -2), smallest);
BOOST_TEST_EQ(T(11, etiny - 70), zero);
BOOST_TEST_EQ(T(wide_type{123}, etiny - 33), zero);

// All values go to zero and keep their sign
fesetround(rounding_mode::fe_dec_toward_zero);
BOOST_TEST_EQ(T(sig_type{9}, etiny - 1), zero);
BOOST_TEST(signbit(T(sig_type{9}, etiny - 1, true)));
BOOST_TEST_EQ(T(sig_type{1}, etiny + 1) * T(sig_type{9}, -2), zero);
BOOST_TEST_EQ(T(sig_type{11}, etiny - 70, true), -zero);
BOOST_TEST_EQ(T(-11, etiny - 70), -zero);
BOOST_TEST_EQ(T(wide_type{123}, etiny - 33), zero);

fesetround(rounding_mode::fe_dec_to_nearest);
#endif
}

int main()
{
test<decimal32_t>();
test<decimal64_t>();
test<decimal128_t>();

return boost::report_errors();
}
Loading