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
10 changes: 10 additions & 0 deletions include/boost/int128/detail/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -348,4 +348,14 @@ BOOST_int128EST_EXPORT using builtin_u128 = std::_Unsigned128;
// "SYCL_EXTERNAL" (SYCL), or nothing (host).
#define BOOST_INT128_HOST_DEVICE BOOST_INT128_CUDA_ENABLED BOOST_INT128_SYCL_ENABLED

// C++23 <stdfloat> extended floating-point types each have their own macro that needs to be checked
#if (__cplusplus > 202002L || (defined(_MSVC_LANG) && _MSVC_LANG > 202002L)) && !defined(BOOST_INT128_HAS_GPU_SUPPORT) && defined(__has_include)
# if __has_include(<stdfloat>)
# ifndef BOOST_INT128_BUILD_MODULE
# include <stdfloat>
# endif
# define BOOST_INT128_HAS_STDFLOAT
# endif
#endif

#endif // BOOST_INT128_DETAIL_CONFIG_HPP
66 changes: 57 additions & 9 deletions include/boost/int128/detail/float_conversion.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,72 @@ BOOST_INT128_HOST_DEVICE constexpr T unsigned_words_to_float_impl(const std::uin
// as many as three times and lands up to one ulp away from the correctly rounded result.
// Round the 128-bit value to exactly digits bits here instead, once, then apply an exact
// power of two. See the note on ties in the body
//
// Every scale factor is checked against numeric_limits<T>::max_exponent before it is ever
// materialized as a T, so a value too large for a narrow extended type (std::float16_t,
// whose largest finite value is 65504) overflows to +infinity exactly the way a builtin
// conversion does, rather than building an out of range floating-point constant, which is
// undefined behavior and not a constant expression
template <typename T>
BOOST_INT128_HOST_DEVICE constexpr T unsigned_words_to_float_impl(const std::uint64_t high, const std::uint64_t low,
std::false_type) noexcept
{
// Anything below 2^64 is one conversion the compiler already rounds correctly
constexpr int digits {std::numeric_limits<T>::digits};

if (high == UINT64_C(0))
{
return static_cast<T>(low);
if (low == UINT64_C(0))
{
return T{0};
}

const auto shift_lo {static_cast<unsigned>(countl_zero(low))};
const auto bit_length_lo {64 - static_cast<int>(shift_lo)};

if (bit_length_lo > std::numeric_limits<T>::max_exponent)
{
return std::numeric_limits<T>::infinity();
}

// low fits in digits bits exactly (no bit below the top bit_length_lo bits is
// ever set), so a single int to float conversion is already correctly rounded
// and, thanks to the check above, always in range
if (bit_length_lo <= digits)
{
return static_cast<T>(low);
}

constexpr int residue_bits_lo {64 - digits};
const std::uint64_t residue_mask_lo {(UINT64_C(1) << residue_bits_lo) - UINT64_C(1)};
const std::uint64_t halfway_lo {UINT64_C(1) << (residue_bits_lo - 1)};

const auto norm {low << shift_lo};
const auto significand_lo {norm >> residue_bits_lo};
const auto residue_lo {norm & residue_mask_lo};

const bool round_up_lo {residue_lo > halfway_lo ||
(residue_lo == halfway_lo && (significand_lo & UINT64_C(1)) != UINT64_C(0))};

const auto rounded_lo {significand_lo + (round_up_lo ? UINT64_C(1) : UINT64_C(0))};

// bit_length_lo > digits here, so this exponent is always strictly positive
return static_cast<T>(rounded_lo) * exact_power_of_two<T>(bit_length_lo - digits);
}

// Normalize so bit 127 of the pair is set, which puts the significand at the top of the
// high word. high is non-zero here, so the distance is always less than 64
const auto shift {static_cast<unsigned>(countl_zero(high))};
const auto bit_length {128 - static_cast<int>(shift)};

if (bit_length > std::numeric_limits<T>::max_exponent)
{
return std::numeric_limits<T>::infinity();
}

constexpr int digits {std::numeric_limits<T>::digits};
constexpr int residue_bits {64 - digits};
constexpr std::uint64_t residue_mask {(UINT64_C(1) << residue_bits) - UINT64_C(1)};
constexpr std::uint64_t halfway {UINT64_C(1) << (residue_bits - 1)};

// Normalize so bit 127 of the pair is set, which puts the significand at the top of the
// high word. high is non-zero here, so the distance is always less than 64
const auto shift {static_cast<unsigned>(countl_zero(high))};
const auto norm_high {shift == 0U ? high : ((high << shift) | (low >> (64U - shift)))};
const auto norm_low {shift == 0U ? low : (low << shift)};

Expand All @@ -79,9 +127,9 @@ BOOST_INT128_HOST_DEVICE constexpr T unsigned_words_to_float_impl(const std::uin

// rounded holds digits bits, or digits + 1 when it carried, in which case it is a power of
// two. Either way the conversion and the scaling are exact, so the product is the value
// rounded exactly once. It overflows to infinity precisely when the correctly rounded
// result does, which is the required behavior for round to nearest
return static_cast<T>(rounded) * exact_power_of_two<T>(static_cast<int>(64U - shift) + residue_bits);
// rounded exactly once. bit_length > digits always here (bit_length >= 65, and digits < 64
// is this overload's precondition), so the exponent is always strictly positive
return static_cast<T>(rounded) * exact_power_of_two<T>(bit_length - digits);
}

// Converts the 128-bit value (high, low) to T, correctly rounded to nearest with ties to even
Expand Down
16 changes: 16 additions & 0 deletions include/boost/int128/detail/int128_imp.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ int128
constexpr operator long double() const noexcept;
#endif

// C++23 <stdfloat> extended types
#if !defined(BOOST_INT128_HAS_GPU_SUPPORT) && defined(BOOST_INT128_HAS_STDFLOAT)
template <typename ExtFloat, std::enable_if_t<detail::is_extended_floating_point_v<ExtFloat>, bool> = true>
BOOST_INT128_HOST_DEVICE constexpr operator ExtFloat() const noexcept
{
return detail::signed_words_to_float<ExtFloat>(signed_high(), low);
}
#endif

// Compound Or
template <BOOST_INT128_DEFAULTED_INTEGER_CONCEPT>
BOOST_INT128_HOST_DEVICE constexpr int128& operator|=(Integer rhs) noexcept;
Expand Down Expand Up @@ -404,6 +413,13 @@ constexpr int128::operator long double() const noexcept
template <BOOST_INT128_FLOATING_POINT_CONCEPT>
BOOST_INT128_HOST_DEVICE constexpr int128::int128(Float f) noexcept
{
// A type this narrow cannot hold the ladder's scale constants below
BOOST_INT128_IF_CONSTEXPR (std::numeric_limits<Float>::max_exponent < 128)
{
*this = int128(static_cast<float>(f));
return;
}

constexpr Float two_32 {static_cast<Float>(UINT64_C(1) << 32)};
constexpr Float two_64 {two_32 * two_32};
constexpr Float two_127 {two_64 * static_cast<Float>(UINT64_C(1) << 63)};
Expand Down
23 changes: 22 additions & 1 deletion include/boost/int128/detail/traits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,23 @@ struct floating_point
{
static constexpr bool value = std::is_same<T, float>::value || std::is_same<T, double>::value
#ifndef BOOST_INT128_HAS_GPU_SUPPORT
|| std::is_same<T, long double>::value;
|| std::is_same<T, long double>::value
#if defined(BOOST_INT128_HAS_STDFLOAT) && defined(__STDCPP_FLOAT16_T__)
|| std::is_same<T, std::float16_t>::value
#endif
#if defined(BOOST_INT128_HAS_STDFLOAT) && defined(__STDCPP_FLOAT32_T__)
|| std::is_same<T, std::float32_t>::value
#endif
#if defined(BOOST_INT128_HAS_STDFLOAT) && defined(__STDCPP_FLOAT64_T__)
|| std::is_same<T, std::float64_t>::value
#endif
#if defined(BOOST_INT128_HAS_STDFLOAT) && defined(__STDCPP_FLOAT128_T__)
|| std::is_same<T, std::float128_t>::value
#endif
#if defined(BOOST_INT128_HAS_STDFLOAT) && defined(__STDCPP_BFLOAT16_T__)
|| std::is_same<T, std::bfloat16_t>::value
#endif
;
#else
;
#endif
Expand All @@ -63,6 +79,11 @@ struct floating_point
template <typename T>
BOOST_INT128_INLINE_CONSTEXPR bool is_floating_point_v = floating_point<T>::value;

// True only for a C++23 <stdfloat> types
template <typename T>
BOOST_INT128_INLINE_CONSTEXPR bool is_extended_floating_point_v = floating_point<T>::value
&& !std::is_same<T, float>::value && !std::is_same<T, double>::value && !std::is_same<T, long double>::value;

// The type integral promotion gives an operand of type T, which is the result type of a
// shift with T on the left. The rule depends on the rank of T and not only on its size, so
// long and char32_t differ where both are the width of an int; asking the compiler is exact
Expand Down
56 changes: 37 additions & 19 deletions include/boost/int128/detail/uint128_imp.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ uint128
constexpr operator long double() const noexcept;
#endif

// C++23 <stdfloat> extended types
#if !defined(BOOST_INT128_HAS_GPU_SUPPORT) && defined(BOOST_INT128_HAS_STDFLOAT)
template <typename ExtFloat, std::enable_if_t<detail::is_extended_floating_point_v<ExtFloat>, bool> = true>
BOOST_INT128_HOST_DEVICE constexpr operator ExtFloat() const noexcept
{
return detail::unsigned_words_to_float<ExtFloat>(high, low);
}
#endif

// Compound OR
template <BOOST_INT128_DEFAULTED_INTEGER_CONCEPT>
BOOST_INT128_HOST_DEVICE constexpr uint128& operator|=(Integer rhs) noexcept;
Expand Down Expand Up @@ -400,29 +409,38 @@ constexpr uint128::operator long double() const noexcept
template <BOOST_INT128_FLOATING_POINT_CONCEPT>
BOOST_INT128_HOST_DEVICE constexpr uint128::uint128(Float f) noexcept
{
constexpr Float two_32 {static_cast<Float>(UINT64_C(1) << 32)};
constexpr Float two_64 {two_32 * two_32};

// !(f >= 0) catches both NaN and negative values without using <cmath>
if (!(f >= Float{0}))
// A type this narrow cannot hold the ladder's 2^32/2^64 scale constants below,
// so we must widen via cast first
BOOST_INT128_IF_CONSTEXPR (std::numeric_limits<Float>::max_exponent < 128)
{
return;
*this = uint128(static_cast<float>(f));
}

// Overflow test: f >= 2^128 iff f / 2^64 >= 2^64. Comparing scaled values
// avoids materializing 2^128 as a Float, which overflows to +infinity for
// `float` and is therefore not constant-evaluable on older compilers.
const Float scaled {f / two_64};
if (scaled >= two_64)
else
{
high = UINT64_MAX;
low = UINT64_MAX;
return;
}
constexpr Float two_32 {static_cast<Float>(UINT64_C(1) << 32)};
constexpr Float two_64 {two_32 * two_32};

high = detail::float_to_uint64(scaled);
const Float remainder {f - static_cast<Float>(high) * two_64};
low = detail::float_to_uint64(remainder);
// !(f >= 0) catches both NaN and negative values without using <cmath>
if (!(f >= Float{0}))
{
return;
}

// Overflow test: f >= 2^128 iff f / 2^64 >= 2^64. Comparing scaled values
// avoids materializing 2^128 as a Float, which overflows to +infinity for
// `float` and is therefore not constant-evaluable on older compilers.
const Float scaled {f / two_64};
if (scaled >= two_64)
{
high = UINT64_MAX;
low = UINT64_MAX;
return;
}

high = detail::float_to_uint64(scaled);
const Float remainder {f - static_cast<Float>(high) * two_64};
low = detail::float_to_uint64(remainder);
}
}

//=====================================
Expand Down
1 change: 1 addition & 0 deletions test/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ run test_builtin_parity.cpp ;
run test_float_mixed_ops.cpp ;
run test_integer_mixed_ops.cpp ;
run test_float_conversion.cpp ;
run test_stdfloat.cpp ;

run test_consteval_funcs.cpp ;
run test_sign_compare.cpp ;
Expand Down
72 changes: 59 additions & 13 deletions test/test_cross_type_assign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#include <cmath>

#ifdef __has_include
# if __has_include(<stdfloat>)
# if (__cplusplus > 202002L || (defined(_MSVC_LANG) && _MSVC_LANG > 202002L)) && __has_include(<stdfloat>)
# include <stdfloat>
# endif
#endif
Expand Down Expand Up @@ -268,24 +268,70 @@ void test_constexpr_float_construction()
// constexpr contexts.
}

// The extended floating point types cannot hold the 2^64 scale factor the constructor needs,
// so they are excluded from detail::is_floating_point_v and reach the constructor through the
// promotion to float instead. std::is_floating_point is true for them in C++23, and using it
// here used to yield 2^63 for every value
void test_extended_float_construction()
{
#ifdef __cpp_lib_stdfloat
#ifdef __STDCPP_FLOAT16_T__

const uint128 u {std::float16_t{3}};
BOOST_TEST_EQ(u, uint128{3});
const uint128 u16 {std::float16_t{3}};
BOOST_TEST_EQ(u16, uint128{3});

const int128 i {std::float16_t{-3}};
BOOST_TEST_EQ(i, int128{-3});
const int128 i16 {std::float16_t{-3}};
BOOST_TEST_EQ(i16, int128{-3});

static_assert(std::is_same<decltype(uint128{} + std::float16_t{}), float>::value,
"an extended float operand promotes to float");
static_assert(std::is_same<decltype(uint128{} + std::float16_t{}), std::float16_t>::value,
"an extended float operand keeps its own type as the result type");

#endif // __cpp_lib_stdfloat
// A value beyond float16_t's range (65504) converts to infinity, not UB
const uint128 big {UINT64_C(0), UINT64_C(1000000)};
BOOST_TEST(std::isinf(static_cast<float>(static_cast<std::float16_t>(big))));

#endif // __STDCPP_FLOAT16_T__

#ifdef __STDCPP_BFLOAT16_T__

const uint128 ub {std::bfloat16_t{5}};
BOOST_TEST_EQ(ub, uint128{5});

const int128 ib {std::bfloat16_t{-5}};
BOOST_TEST_EQ(ib, int128{-5});

#endif // __STDCPP_BFLOAT16_T__

#ifdef __STDCPP_FLOAT32_T__

const uint128 u32 {std::float32_t{7}};
BOOST_TEST_EQ(u32, uint128{7});

const int128 i32 {std::float32_t{-7}};
BOOST_TEST_EQ(i32, int128{-7});

#endif // __STDCPP_FLOAT32_T__

#ifdef __STDCPP_FLOAT64_T__

const uint128 u64 {std::float64_t{9}};
BOOST_TEST_EQ(u64, uint128{9});

const int128 i64 {std::float64_t{-9}};
BOOST_TEST_EQ(i64, int128{-9});

#endif // __STDCPP_FLOAT64_T__

#ifdef __STDCPP_FLOAT128_T__

const uint128 u128 {std::float128_t{11}};
BOOST_TEST_EQ(u128, uint128{11});

const int128 i128v {std::float128_t{-11}};
BOOST_TEST_EQ(i128v, int128{-11});

// MIN round trips exactly through the wide-significand (113 digit) path
constexpr auto mn {(std::numeric_limits<int128>::min)()};
const auto as_f128 {static_cast<std::float128_t>(mn)};
const auto back {static_cast<int128>(as_f128)};
BOOST_TEST_EQ(back, mn);

#endif // __STDCPP_FLOAT128_T__
}

int main()
Expand Down
Loading
Loading