diff --git a/doc/modules/ROOT/pages/int128_t.adoc b/doc/modules/ROOT/pages/int128_t.adoc index 18830ade..8dbe1a8f 100644 --- a/doc/modules/ROOT/pages/int128_t.adoc +++ b/doc/modules/ROOT/pages/int128_t.adoc @@ -409,6 +409,10 @@ This operation is subject to mixed sign limitations discussed xref:int128_t.adoc [#i128_math_operators] == Arithmetic Operators +All of the arithmetic operators use two's-complement wrap-around on overflow. +When a result is not representable in an `int128_t` it silently rolls over modulo `2^128` rather than trapping or invoking undefined behavior, matching `std::numeric_limits::is_modulo`. +For example, `BOOST_INT128_INT128_MAX + 1` wraps to `BOOST_INT128_INT128_MIN`, and `BOOST_INT128_INT128_MIN - 1` wraps to `BOOST_INT128_INT128_MAX`. + === Unary Plus and Minus [source, c++] diff --git a/include/boost/int128/detail/int128_imp.hpp b/include/boost/int128/detail/int128_imp.hpp index b94d5fc2..35c1f2ad 100644 --- a/include/boost/int128/detail/int128_imp.hpp +++ b/include/boost/int128/detail/int128_imp.hpp @@ -1883,18 +1883,30 @@ BOOST_INT128_HOST_DEVICE constexpr int128_t operator+(const UnsignedInteger lhs, return detail::default_add(rhs, lhs); } +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable : 4146) // Unary minus applied to unsigned type +#endif + BOOST_INT128_EXPORT template BOOST_INT128_HOST_DEVICE constexpr int128_t operator+(const int128_t lhs, const SignedInteger rhs) noexcept { - return rhs > 0 ? detail::default_add(lhs, rhs) : detail::default_sub(lhs, -rhs); + // Negate in the unsigned domain so INT64_MIN does not overflow (UBSAN) + return rhs < 0 ? detail::default_sub(lhs, -static_cast(rhs)) : + detail::default_add(lhs, static_cast(rhs)); } BOOST_INT128_EXPORT template BOOST_INT128_HOST_DEVICE constexpr int128_t operator+(const SignedInteger lhs, const int128_t rhs) noexcept { - return lhs > 0 ? detail::default_add(rhs, lhs) : detail::default_sub(rhs, -lhs); + return lhs < 0 ? detail::default_sub(rhs, -static_cast(lhs)) : + detail::default_add(rhs, static_cast(lhs)); } +#ifdef _MSC_VER +# pragma warning(pop) +#endif + #if defined(BOOST_INT128_HAS_INT128) || defined(BOOST_INT128_HAS_MSVC_INT128) @@ -2272,12 +2284,22 @@ BOOST_INT128_HOST_DEVICE constexpr int128_t operator/(const UnsignedInteger lhs, else { auto abs_rhs {abs(rhs)}; + // rhs == -2^64 has |rhs| greater than any 64-bit lhs, so the quotient is 0 (also avoids /0) + if (abs_rhs.high != 0) + { + return {0, 0}; + } const auto res {static_cast(lhs) / abs_rhs.low}; const int128_t result {0, res}; return rhs < 0 ? -result : result; } } +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable : 4146) // Unary minus applied to unsigned type +#endif + BOOST_INT128_EXPORT template BOOST_INT128_HOST_DEVICE constexpr int128_t operator/(const int128_t lhs, const SignedInteger rhs) noexcept { @@ -2292,7 +2314,8 @@ BOOST_INT128_HOST_DEVICE constexpr int128_t operator/(const int128_t lhs, const constexpr int128_t min_val {INT64_MIN, 0}; const auto negative_res {static_cast((lhs.high < 0) ^ (rhs < 0))}; - const auto abs_rhs {rhs < 0 ? -rhs : rhs}; + // Negate in the unsigned domain so INT64_MIN does not overflow (UBSAN) + const auto abs_rhs {rhs < 0 ? -static_cast(rhs) : static_cast(rhs)}; const auto abs_lhs {abs(lhs)}; if (lhs != min_val && abs_lhs < abs_rhs) @@ -2300,7 +2323,7 @@ BOOST_INT128_HOST_DEVICE constexpr int128_t operator/(const int128_t lhs, const return {0, 0}; } - detail::one_word_div(abs_lhs, static_cast(abs_rhs), quotient); + detail::one_word_div(abs_lhs, abs_rhs, quotient); return negative_res ? -quotient : quotient; } @@ -2321,13 +2344,23 @@ BOOST_INT128_HOST_DEVICE constexpr int128_t operator/(const SignedInteger lhs, c { const auto negative_res {static_cast((rhs.high < 0) ^ (lhs < 0))}; const auto abs_rhs {abs(rhs)}; - const auto abs_lhs {lhs < 0 ? -lhs : lhs}; - const int128_t res {0, static_cast(abs_lhs) / abs_rhs.low}; + // rhs == -2^64 has |rhs| greater than any 64-bit lhs, so the quotient is 0 (also avoids /0) + if (abs_rhs.high != 0) + { + return {0, 0}; + } + // Negate in the unsigned domain so INT64_MIN does not overflow (UBSAN) + const auto abs_lhs {lhs < 0 ? -static_cast(lhs) : static_cast(lhs)}; + const int128_t res {0, abs_lhs / abs_rhs.low}; return negative_res ? -res : res; } } +#ifdef _MSC_VER +# pragma warning(pop) +#endif + #if defined(BOOST_INT128_HAS_INT128) || defined(BOOST_INT128_HAS_MSVC_INT128) diff --git a/test/Jamfile b/test/Jamfile index 2be537df..cb5170e5 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -104,6 +104,9 @@ run test_to_string.cpp ; # Warnings about padding propagate out of run test_hash.cpp : : : msvc:/wd4324 ; +run test_boundaries.cpp ; +run test_constexpr_boundaries.cpp ; + # Make sure we run the examples as well run ../examples/construction.cpp ; run ../examples/bit.cpp ; diff --git a/test/test_boundaries.cpp b/test/test_boundaries.cpp new file mode 100644 index 00000000..d36f9585 --- /dev/null +++ b/test/test_boundaries.cpp @@ -0,0 +1,69 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include +#include +#include + +using boost::int128::int128_t; + +namespace +{ + +std::int64_t opaque(std::int64_t v) +{ + volatile std::int64_t s {v}; + return s; +} + +std::uint64_t opaque(std::uint64_t v) +{ + volatile std::uint64_t s {v}; + return s; +} + +int128_t opaque(int128_t v) +{ + return int128_t{opaque(static_cast(v.high)), opaque(static_cast(v.low))}; +} + +} // namespace + +int main() +{ + const auto int_max {opaque(BOOST_INT128_INT128_MAX)}; + const auto int_min {opaque(BOOST_INT128_INT128_MIN)}; + const int128_t one {opaque(int128_t{0, 1})}; + + // Negating a signed scalar of INT64_MIN must not overflow the scalar type. + // The scalar-operand result must equal promoting the scalar to int128_t first. + const std::int64_t m {opaque((std::numeric_limits::min)())}; + const int128_t m128 {m}; + const int128_t a {opaque(int128_t{42, 1234})}; + const int128_t b {opaque(int128_t{0, 5})}; + + BOOST_TEST_EQ(a + m, a + m128); // operator+(int128_t, Signed) was int128_imp.hpp:1889 + BOOST_TEST_EQ(m + a, m128 + a); // operator+(Signed, int128_t) was int128_imp.hpp:1895 + BOOST_TEST_EQ(a / m, a / m128); // operator/(int128_t, Signed) was int128_imp.hpp:2295 + BOOST_TEST_EQ(m / b, m128 / b); // operator/(Signed, int128_t) was int128_imp.hpp:2324 + + // A 128-bit divisor of exactly -2^64 has abs().low == 0; the quotient of any + // 64-bit numerator by it is 0 and must not divide by zero. + const int128_t neg_two_pow_64 {opaque(int128_t{-1, 0})}; + BOOST_TEST_EQ(opaque(std::int64_t{5}) / neg_two_pow_64, (int128_t{0, 0})); // operator/(Signed, int128_t) + BOOST_TEST_EQ(opaque(std::int64_t{-5}) / neg_two_pow_64, (int128_t{0, 0})); + BOOST_TEST_EQ(opaque(std::uint64_t{5}) / neg_two_pow_64, (int128_t{0, 0})); // operator/(Unsigned, int128_t) + + // Two's-complement rollover at the range boundaries: no UB, wraps like __int128. + BOOST_TEST_EQ(int_max + one, int_min); // MAX + 1 -> MIN + BOOST_TEST_EQ(int_min - one, int_max); // MIN - 1 -> MAX + BOOST_TEST_EQ(int_max + opaque(std::int64_t{1}), int_min); // MAX + 1 (scalar) -> MIN + BOOST_TEST_EQ(int_min - opaque(std::int64_t{1}), int_max); // MIN - 1 (scalar) -> MAX + BOOST_TEST_EQ(-int_min, int_min); // -MIN wraps to itself + BOOST_TEST_EQ(int_min * opaque(std::int64_t{-1}), int_min); // MIN * -1 overflows to MIN + BOOST_TEST_EQ(int_max * opaque(std::int64_t{2}), (-int128_t{0, 2})); // MAX * 2 overflows to -2 + + return boost::report_errors(); +} diff --git a/test/test_constexpr_boundaries.cpp b/test/test_constexpr_boundaries.cpp new file mode 100644 index 00000000..f98295b1 --- /dev/null +++ b/test/test_constexpr_boundaries.cpp @@ -0,0 +1,61 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +// Compile-time counterpart of test_boundaries.cpp. Every operation is evaluated +// in a constant expression. Undefined behavior is not permitted in a constant +// expression, so any UB here is a compilation error, and the equalities are +// verified at compile time as well. + +#include +#include +#include + +#ifdef _MSC_VER +# pragma warning(disable : 4307) // integral constant overflow +# pragma warning(disable : 4146) // unary minus operator applied to unsigned type, result still unsigned +#endif + +using boost::int128::int128_t; + +namespace +{ + +constexpr auto int_max {BOOST_INT128_INT128_MAX}; +constexpr auto int_min {BOOST_INT128_INT128_MIN}; +constexpr int128_t one {0, 1}; + +constexpr std::int64_t m {(std::numeric_limits::min)()}; +constexpr int128_t m128 {m}; +constexpr int128_t a {42, 1234}; +constexpr int128_t b {0, 5}; +constexpr int128_t neg_two_pow_64 {-1, 0}; + +// Signed-scalar negation paths (INT64_MIN) must be UB-free and must agree with +// promoting the scalar to int128_t first. +static_assert(a + m == a + m128, "operator+(int128_t, Signed)"); +static_assert(m + a == m128 + a, "operator+(Signed, int128_t)"); +static_assert(a / m == a / m128, "operator/(int128_t, Signed)"); +static_assert(m / b == m128 / b, "operator/(Signed, int128_t)"); + +// A 128-bit divisor of exactly -2^64 has abs().low == 0; the quotient is 0 and +// must not divide by zero. +static_assert(std::int64_t{5} / neg_two_pow_64 == int128_t{0, 0}, "operator/(Signed, int128_t) -2^64"); +static_assert(std::int64_t{-5} / neg_two_pow_64 == int128_t{0, 0}, "operator/(Signed, int128_t) -2^64 negative"); +static_assert(std::uint64_t{5} / neg_two_pow_64 == int128_t{0, 0}, "operator/(Unsigned, int128_t) -2^64"); + +// Two's-complement rollover at the range boundaries: no UB, wraps like __int128. +static_assert(int_max + one == int_min, "MAX + 1 -> MIN"); +static_assert(int_min - one == int_max, "MIN - 1 -> MAX"); +static_assert(int_max + 1 == int_min, "MAX + 1 (scalar) -> MIN"); +static_assert(int_min - 1 == int_max, "MIN - 1 (scalar) -> MAX"); +static_assert(-int_min == int_min, "-MIN wraps to itself"); +static_assert(int_min * -1 == int_min, "MIN * -1 overflows to MIN"); +static_assert(int_max * 2 == -int128_t{0, 2}, "MAX * 2 overflows to -2"); + +} // namespace + +int main() +{ + return 0; +}