diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index c3f34d35..ddb68e3c 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -16,6 +16,7 @@ ** xref:examples.adoc#examples_mixed_sign[Mixed Signedness Arithmetic] ** xref:examples.adoc#examples_boost_math_random[Boost Math and Random Integration] ** xref:examples.adoc#examples_boost_charconv[Boost.Charconv Integration] +** xref:examples.adoc#examples_boost_container_hash[Boost.ContainerHash and Boost.Unordered Integration] ** xref:examples.adoc#examples_to_string[String Conversion (to_string)] ** xref:examples.adoc#examples_fmt_format[pass:[{fmt}] Integration] ** xref:examples.adoc#examples_cstdlib[`` support (Combined div and mod)] diff --git a/doc/modules/ROOT/pages/api_reference.adoc b/doc/modules/ROOT/pages/api_reference.adoc index b7c27d6b..f00c9a2d 100644 --- a/doc/modules/ROOT/pages/api_reference.adoc +++ b/doc/modules/ROOT/pages/api_reference.adoc @@ -311,6 +311,27 @@ Listed by analogous STL header. | xref:utilities.adoc#checked[`ckd_mul`] | Checked multiplication (C23 `` contract) + +| xref:utilities.adoc#comparison[`cmp_equal`] +| Signedness-safe equality (C++26 `` contract) + +| xref:utilities.adoc#comparison[`cmp_not_equal`] +| Signedness-safe inequality + +| xref:utilities.adoc#comparison[`cmp_less`] +| Signedness-safe less-than + +| xref:utilities.adoc#comparison[`cmp_greater`] +| Signedness-safe greater-than + +| xref:utilities.adoc#comparison[`cmp_less_equal`] +| Signedness-safe less-than-or-equal + +| xref:utilities.adoc#comparison[`cmp_greater_equal`] +| Signedness-safe greater-than-or-equal + +| xref:utilities.adoc#in_range[`in_range`] +| Whether a value is representable in a target integer type |=== [#api_macros] diff --git a/doc/modules/ROOT/pages/utilities.adoc b/doc/modules/ROOT/pages/utilities.adoc index ec539f85..4250f169 100644 --- a/doc/modules/ROOT/pages/utilities.adoc +++ b/doc/modules/ROOT/pages/utilities.adoc @@ -199,3 +199,97 @@ ckd_add(uint128_t{5}, int128_t{-3}): overflow=false, result=2 ckd_mul(20, 20): overflow=true, wrapped=144 ---- ==== + +[#comparison] +== Integer Comparison + +`cmp_equal`, `cmp_not_equal`, `cmp_less`, `cmp_greater`, `cmp_less_equal`, and `cmp_greater_equal` are the C++26 https://eel.is/c++draft/utility.intcmp[integer comparison functions] extended to the library and builtin 128-bit types. +They are available in C++14 and later. + +Unlike the built-in relational operators, which follow the usual arithmetic conversions, these functions compare the true mathematical values of their operands regardless of signedness. +When a `uint128_t` is compared with an `int128_t`, the operators convert the signed operand to unsigned, so `int128_t{-1} < uint128_t{0}` is reported as `false` and `(std::numeric_limits::max)() == int128_t{-1}` as `true`. +The `cmp_*` functions report the mathematically correct answer in both cases. + +Throughout this section a *128-bit type* is `int128_t`, `uint128_t`, or a builtin 128-bit type where the platform provides one (`+__int128+` and its unsigned counterpart, or `std::_Signed128` and `std::_Unsigned128` on MSVC). + +[source, c++] +---- +namespace boost { +namespace int128 { + +// Each function participates in overload resolution only when both operands are +// permitted and at least one is a 128-bit type. The other operand may be a +// 128-bit type or any standard or extended integer type other than bool, a +// character type, or std::byte. + +template +BOOST_INT128_HOST_DEVICE constexpr bool cmp_equal(T a, U b) noexcept; +template +BOOST_INT128_HOST_DEVICE constexpr bool cmp_not_equal(T a, U b) noexcept; +template +BOOST_INT128_HOST_DEVICE constexpr bool cmp_less(T a, U b) noexcept; +template +BOOST_INT128_HOST_DEVICE constexpr bool cmp_greater(T a, U b) noexcept; +template +BOOST_INT128_HOST_DEVICE constexpr bool cmp_less_equal(T a, U b) noexcept; +template +BOOST_INT128_HOST_DEVICE constexpr bool cmp_greater_equal(T a, U b) noexcept; + +} // namespace int128 +} // namespace boost +---- + +`cmp_equal(a, b)` returns `true` when `a` and `b` are mathematically equal, and `cmp_less(a, b)` returns `true` when `a` is mathematically less than `b`. +The remaining four functions are defined in terms of these two exactly as in the standard: `cmp_not_equal` is `!cmp_equal(a, b)`, `cmp_greater(a, b)` is `cmp_less(b, a)`, `cmp_less_equal(a, b)` is `!cmp_less(b, a)`, and `cmp_greater_equal(a, b)` is `!cmp_less(a, b)`. + +Every operand pair in which at least one operand is a 128-bit type is supported, including a `uint128_t` (or an unsigned builtin 128-bit value) against a signed `int128_t` (or a signed builtin one). +Following the standard, a `bool`, a character type (`char`, `wchar_t`, `char8_t`, `char16_t`, `char32_t`), or `std::byte` is not a permitted operand, and such a call is ill-formed. + +[#in_range] +== Range Checking + +`in_range(t)` returns `true` when the value `t` is representable in the type `R`, that is, when `R` can hold `t` without the value changing. +It is equivalent to `cmp_greater_equal(t, (std::numeric_limits::min)()) && cmp_less_equal(t, (std::numeric_limits::max)())`, so the range check is itself signedness-safe. + +[source, c++] +---- +namespace boost { +namespace int128 { + +template +BOOST_INT128_HOST_DEVICE constexpr bool in_range(Integer t) noexcept; + +} // namespace int128 +} // namespace boost +---- + +The target type `R` and the type of `t` may each be a builtin integer or a 128-bit type; at least one of them is a 128-bit type (use `std::in_range` when neither is). +For example, `in_range(int128_t{-1})` is `false` because a negative value is not representable in an unsigned type, and `in_range((std::numeric_limits::max)())` is `false` because the value is too large. + +.This https://github.com/cppalliance/int128/blob/develop/examples/integer_comparison.cpp[example] demonstrates signedness-safe comparison and range checking with the library 128-bit types +==== +[source, c++] +---- +include::example$integer_comparison.cpp[] +---- + +.Expected Output +[listing] +---- +=== Signedness-safe comparison === +UINT128_MAX == int128_t{-1} (operator): true +cmp_equal(UINT128_MAX, int128_t{-1}): false +int128_t{-1} < uint128_t{0} (operator): false +cmp_less(int128_t{-1}, uint128_t{0}): true + +=== Mixed with builtin integers === +cmp_less(int128_t{-5}, 0u): true +cmp_equal(uint128_t{42}, 42): true + +=== in_range === +in_range(int128_t{200}): false +in_range(int128_t{-1}): false +in_range(UINT128_MAX): false +in_range(UINT128_MAX): false +---- +==== diff --git a/examples/integer_comparison.cpp b/examples/integer_comparison.cpp new file mode 100644 index 00000000..0c390f98 --- /dev/null +++ b/examples/integer_comparison.cpp @@ -0,0 +1,54 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +// Individual headers + +#include +#include + +// Or you can do a single header + +// #include + +#include +#include +#include + +int main() +{ + using boost::int128::uint128_t; + using boost::int128::int128_t; + using boost::int128::cmp_equal; + using boost::int128::cmp_less; + using boost::int128::in_range; + + std::cout << std::boolalpha; + + constexpr auto u_max {(std::numeric_limits::max)()}; + + // The built-in relational operators follow the usual arithmetic conversions, + // so comparing a uint128_t with a negative int128_t converts the negative + // value to a huge unsigned one and gives the wrong answer. The cmp_* family + // compares the true mathematical values instead. + std::cout << "=== Signedness-safe comparison ===" << std::endl; + std::cout << "UINT128_MAX == int128_t{-1} (operator): " << (u_max == int128_t{-1}) << std::endl; + std::cout << "cmp_equal(UINT128_MAX, int128_t{-1}): " << cmp_equal(u_max, int128_t{-1}) << std::endl; + std::cout << "int128_t{-1} < uint128_t{0} (operator): " << (int128_t{-1} < uint128_t{0}) << std::endl; + std::cout << "cmp_less(int128_t{-1}, uint128_t{0}): " << cmp_less(int128_t{-1}, uint128_t{0}) << std::endl; + + // Either operand may be a builtin integer of any width and signedness. + std::cout << "\n=== Mixed with builtin integers ===" << std::endl; + std::cout << "cmp_less(int128_t{-5}, 0u): " << cmp_less(int128_t{-5}, 0U) << std::endl; + std::cout << "cmp_equal(uint128_t{42}, 42): " << cmp_equal(uint128_t{42}, 42) << std::endl; + + // in_range(v) reports whether v is representable in the target type R. + // R and the type of v may each be a builtin integer or a 128-bit type. + std::cout << "\n=== in_range ===" << std::endl; + std::cout << "in_range(int128_t{200}): " << in_range(int128_t{200}) << std::endl; + std::cout << "in_range(int128_t{-1}): " << in_range(int128_t{-1}) << std::endl; + std::cout << "in_range(UINT128_MAX): " << in_range(u_max) << std::endl; + std::cout << "in_range(UINT128_MAX): " << in_range(u_max) << std::endl; + + return 0; +} diff --git a/include/boost/int128/utilities.hpp b/include/boost/int128/utilities.hpp index 01140c96..5a0f10f2 100644 --- a/include/boost/int128/utilities.hpp +++ b/include/boost/int128/utilities.hpp @@ -11,6 +11,7 @@ #ifndef BOOST_INT128_BUILD_MODULE +#include #include #include #include @@ -432,6 +433,160 @@ BOOST_INT128_HOST_DEVICE constexpr bool ckd_mul(T1* result, const T2 a, const T3 return detail::ckd_overflows(product_magnitude, product_negative, exceeds_width); } +namespace detail { + +// See: https://eel.is/c++draft/utility.intcmp +// [Note 1: These function templates cannot be used to compare byte, char, char8_t, char16_t, char32_t, wchar_t, and bool. end note] +template +struct valid_comparison_type +{ + static constexpr bool value = std::is_integral::value && + !std::is_same::value && + !std::is_same::value && + !std::is_same::value && + !std::is_same::value && + !std::is_same::value + #if defined(__cpp_char8_t) + && !std::is_same::value + #endif + #if defined(__cpp_lib_byte) && __cpp_lib_byte >= 201603L + && !std::is_same::value + #endif + ; +}; + +template +BOOST_INT128_INLINE_CONSTEXPR bool is_valid_comparison_type_v = valid_comparison_type::value; + +// Allow the builtins to be used when available +template +BOOST_INT128_INLINE_CONSTEXPR bool is_int128_type_v = std::is_same::value || + std::is_same::value + #if defined(BOOST_INT128_HAS_INT128) || defined(BOOST_INT128_HAS_MSVC_INT128) + || std::is_same::value + || std::is_same::value + #endif + ; + +template +BOOST_INT128_INLINE_CONSTEXPR bool is_valid_comparison_operand_v = is_valid_comparison_type_v || + is_int128_type_v; + +// Maps the builtin 128-bit types onto the library equivalents +template +struct comparison_canonical +{ + using type = T; +}; + +#if defined(BOOST_INT128_HAS_INT128) || defined(BOOST_INT128_HAS_MSVC_INT128) + +template <> +struct comparison_canonical +{ + using type = int128_t; +}; + +template <> +struct comparison_canonical +{ + using type = uint128_t; +}; + +#endif + +template +using comparison_canonical_t = typename comparison_canonical::type; + +template +BOOST_INT128_HOST_DEVICE constexpr comparison_canonical_t canonical_comparison_operand(const T value) noexcept +{ + return static_cast>(value); +} + +// Mathematical equality of two integers regardless of their signedness, via the +// same (sign, magnitude) decomposition. +template +BOOST_INT128_HOST_DEVICE constexpr bool cmp_equal_impl(const T lhs, const U rhs) noexcept +{ + const auto a {ckd_decompose(canonical_comparison_operand(lhs))}; + const auto b {ckd_decompose(canonical_comparison_operand(rhs))}; + + return (a.negative == b.negative) && (a.magnitude == b.magnitude); +} + +// Mathematical less-than of two integers regardless of their signedness, via the +// same (sign, magnitude) decomposition. +template +BOOST_INT128_HOST_DEVICE constexpr bool cmp_less_impl(const T lhs, const U rhs) noexcept +{ + const auto a {ckd_decompose(canonical_comparison_operand(lhs))}; + const auto b {ckd_decompose(canonical_comparison_operand(rhs))}; + + if (a.negative != b.negative) + { + return a.negative; + } + + return a.negative ? (a.magnitude > b.magnitude) : (a.magnitude < b.magnitude); +} + +template +BOOST_INT128_INLINE_CONSTEXPR bool enable_comparison_v = is_valid_comparison_operand_v && + is_valid_comparison_operand_v && + (is_int128_type_v || is_int128_type_v); + +} // namespace detail + +// C++26 integer comparison functions (https://eel.is/c++draft/utility.intcmp) +// extended to the library and builtin 128-bit types and available from C++14. + +BOOST_INT128_EXPORT template , bool> = true> +BOOST_INT128_HOST_DEVICE constexpr bool cmp_equal(const T lhs, const U rhs) noexcept +{ + return detail::cmp_equal_impl(lhs, rhs); +} + +BOOST_INT128_EXPORT template , bool> = true> +BOOST_INT128_HOST_DEVICE constexpr bool cmp_not_equal(const T lhs, const U rhs) noexcept +{ + return !detail::cmp_equal_impl(lhs, rhs); +} + +BOOST_INT128_EXPORT template , bool> = true> +BOOST_INT128_HOST_DEVICE constexpr bool cmp_less(const T lhs, const U rhs) noexcept +{ + return detail::cmp_less_impl(lhs, rhs); +} + +BOOST_INT128_EXPORT template , bool> = true> +BOOST_INT128_HOST_DEVICE constexpr bool cmp_greater(const T lhs, const U rhs) noexcept +{ + return detail::cmp_less_impl(rhs, lhs); +} + +BOOST_INT128_EXPORT template , bool> = true> +BOOST_INT128_HOST_DEVICE constexpr bool cmp_less_equal(const T lhs, const U rhs) noexcept +{ + return !detail::cmp_less_impl(rhs, lhs); +} + +BOOST_INT128_EXPORT template , bool> = true> +BOOST_INT128_HOST_DEVICE constexpr bool cmp_greater_equal(const T lhs, const U rhs) noexcept +{ + return !detail::cmp_less_impl(lhs, rhs); +} + +// Whether t is representable in the target type R. +BOOST_INT128_EXPORT template , bool> = true> +BOOST_INT128_HOST_DEVICE constexpr bool in_range(const T t) noexcept +{ + using limits = std::numeric_limits>; + + return !detail::cmp_less_impl(t, (limits::min)()) && + !detail::cmp_less_impl((limits::max)(), t); +} + } // namespace int128 } // namespace boost diff --git a/test/Jamfile b/test/Jamfile index acc2f74d..e1b478d8 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -90,6 +90,7 @@ run test_powm.cpp ; run test_ipow.cpp ; run test_isqrt.cpp ; run test_ckd.cpp ; +run test_integer_comparison.cpp ; run test_format.cpp ; run test_fmt_format.cpp ; @@ -113,6 +114,7 @@ run ../examples/construction.cpp ; run ../examples/bit.cpp ; run ../examples/saturating_arithmetic.cpp ; run ../examples/checked_arithmetic.cpp ; +run ../examples/integer_comparison.cpp ; run ../examples/mixed_type_arithmetic.cpp ; run ../examples/stream.cpp ; run ../examples/basic_arithmetic.cpp ; @@ -157,3 +159,4 @@ compile compile_tests/utilities_compile.cpp ; compile-fail compile_tests/literals_overflow_fail.cpp ; compile-fail compile_tests/literals_base_prefix_fail.cpp ; compile-fail compile_tests/literals_invalid_fail.cpp ; +compile-fail compile_tests/utilities_comparison_fail.cpp ; diff --git a/test/compile_tests/utilities_comparison_fail.cpp b/test/compile_tests/utilities_comparison_fail.cpp new file mode 100644 index 00000000..5053bbf6 --- /dev/null +++ b/test/compile_tests/utilities_comparison_fail.cpp @@ -0,0 +1,13 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +// The C++26 integer comparison functions reject plain char (and the other +// character types, bool, and std::byte). This must fail to compile. + +#include + +int main() +{ + return boost::int128::cmp_equal(boost::int128::uint128_t{1}, 'a') ? 1 : 0; +} diff --git a/test/test_integer_comparison.cpp b/test/test_integer_comparison.cpp new file mode 100644 index 00000000..1eb01ec3 --- /dev/null +++ b/test/test_integer_comparison.cpp @@ -0,0 +1,398 @@ +// 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 +#include +#include + +using boost::int128::int128_t; +using boost::int128::uint128_t; +using boost::int128::cmp_equal; +using boost::int128::cmp_not_equal; +using boost::int128::cmp_less; +using boost::int128::cmp_greater; +using boost::int128::cmp_less_equal; +using boost::int128::cmp_greater_equal; +using boost::int128::in_range; + +// +// Known-answer tests: hand-computed expected results that need no oracle and so +// run on every platform, including those without a native 128-bit type. +// +void test_known_answers() +{ + constexpr auto u_max {(std::numeric_limits::max)()}; + constexpr auto i_max {(std::numeric_limits::max)()}; + constexpr auto i_min {(std::numeric_limits::min)()}; + + // The signature case: a large unsigned value against a negative signed one. + // The built-in operators would convert -1 to 2^128-1 and report equal; the + // cmp_* functions compare the true mathematical values. + BOOST_TEST(!cmp_equal(u_max, int128_t{-1})); + BOOST_TEST(cmp_not_equal(u_max, int128_t{-1})); + BOOST_TEST(cmp_greater(u_max, int128_t{-1})); + BOOST_TEST(!cmp_less(u_max, int128_t{-1})); + BOOST_TEST(cmp_less(int128_t{-1}, u_max)); + BOOST_TEST(cmp_greater_equal(u_max, int128_t{-1})); + BOOST_TEST(cmp_less_equal(int128_t{-1}, u_max)); + + // uint128_t vs builtin + BOOST_TEST(cmp_equal(uint128_t{5}, 5)); + BOOST_TEST(cmp_equal(uint128_t{5}, 5U)); + BOOST_TEST(!cmp_equal(uint128_t{5}, -5)); + BOOST_TEST(cmp_not_equal(uint128_t{5}, -5)); + BOOST_TEST(cmp_greater(uint128_t{5}, -5)); + BOOST_TEST(!cmp_less(uint128_t{0}, -1)); + BOOST_TEST(cmp_greater(uint128_t{0}, -1)); + BOOST_TEST(cmp_less(uint128_t{5}, 10)); + BOOST_TEST(cmp_less_equal(uint128_t{5}, 5)); + BOOST_TEST(cmp_greater_equal(uint128_t{5}, 5)); + + // int128_t vs builtin, both signs + BOOST_TEST(cmp_less(int128_t{-5}, 0U)); + BOOST_TEST(cmp_less(int128_t{-5}, -3)); + BOOST_TEST(cmp_greater(int128_t{-3}, -5)); + BOOST_TEST(cmp_equal(int128_t{-5}, -5)); + BOOST_TEST(!cmp_equal(int128_t{-5}, 5U)); + BOOST_TEST(cmp_less(int128_t{-1}, 0U)); + BOOST_TEST(cmp_greater_equal(int128_t{5}, -5)); + BOOST_TEST(cmp_less_equal(int128_t{-5}, 5U)); + + // builtin on the left + BOOST_TEST(cmp_less(-1, u_max)); + BOOST_TEST(!cmp_greater(-1, uint128_t{0})); + BOOST_TEST(cmp_less(-1, uint128_t{0})); + BOOST_TEST(!cmp_equal(-1, u_max)); + BOOST_TEST(cmp_greater(5, int128_t{-5})); + BOOST_TEST(cmp_equal(5U, uint128_t{5})); + + // same-type 128-bit + BOOST_TEST(cmp_equal(uint128_t{7}, uint128_t{7})); + BOOST_TEST(cmp_not_equal(uint128_t{7}, uint128_t{8})); + BOOST_TEST(cmp_less(uint128_t{7}, uint128_t{8})); + BOOST_TEST(cmp_equal(int128_t{-7}, int128_t{-7})); + BOOST_TEST(cmp_less(int128_t{-7}, int128_t{-3})); + BOOST_TEST(cmp_greater(int128_t{-3}, int128_t{-7})); + + // extremes across the two 128-bit types + BOOST_TEST(cmp_less(i_max, u_max)); + BOOST_TEST(cmp_greater(u_max, i_max)); + BOOST_TEST(cmp_less(i_min, uint128_t{0})); + BOOST_TEST(cmp_greater(uint128_t{0}, i_min)); + BOOST_TEST(cmp_equal(uint128_t{i_max}, i_max)); + BOOST_TEST(!cmp_equal(u_max, i_max)); + + // zero comparisons + BOOST_TEST(cmp_equal(uint128_t{0}, 0)); + BOOST_TEST(cmp_equal(int128_t{0}, 0U)); + BOOST_TEST(cmp_equal(int128_t{0}, uint128_t{0})); + BOOST_TEST(cmp_greater_equal(uint128_t{0}, int128_t{0})); + BOOST_TEST(cmp_less_equal(uint128_t{0}, int128_t{0})); +} + +// +// in_range: the value and the target type R may each be a builtin integer or a +// library 128-bit type. +// +void test_in_range() +{ + constexpr auto u_max {(std::numeric_limits::max)()}; + constexpr auto i_max {(std::numeric_limits::max)()}; + constexpr auto i_min {(std::numeric_limits::min)()}; + + // library value -> narrow builtin target + BOOST_TEST(in_range(int128_t{127})); + BOOST_TEST(!in_range(int128_t{128})); + BOOST_TEST(in_range(int128_t{-128})); + BOOST_TEST(!in_range(int128_t{-129})); + BOOST_TEST(!in_range(int128_t{-1})); + BOOST_TEST(in_range(uint128_t{255})); + BOOST_TEST(!in_range(uint128_t{256})); + + // library value -> wide builtin target + BOOST_TEST(!in_range(u_max)); + BOOST_TEST(in_range(uint128_t{(std::numeric_limits::max)()})); + BOOST_TEST(!in_range(i_max)); + BOOST_TEST(in_range(int128_t{(std::numeric_limits::max)()})); + BOOST_TEST(!in_range(u_max)); + + // builtin value -> library target (every builtin fits, except negatives in + // an unsigned target) + BOOST_TEST(!in_range(-1)); + BOOST_TEST(in_range(5)); + BOOST_TEST(in_range(-5)); + BOOST_TEST(in_range((std::numeric_limits::min)())); + BOOST_TEST(in_range((std::numeric_limits::max)())); + + // library value -> library target + BOOST_TEST(!in_range(u_max)); + BOOST_TEST(in_range(uint128_t{i_max})); + BOOST_TEST(!in_range(int128_t{-1})); + BOOST_TEST(in_range(i_max)); + BOOST_TEST(in_range(u_max)); + BOOST_TEST(in_range(i_min)); +} + +// +// Type-constraint tests: the standard permits only genuine signed and unsigned +// integer types. bool, the character types, std::byte, and non-integers are not +// permitted operands. +// +void test_constraints() +{ + using boost::int128::detail::is_valid_comparison_type_v; + + static_assert(is_valid_comparison_type_v, "int is permitted"); + static_assert(is_valid_comparison_type_v, "unsigned is permitted"); + static_assert(is_valid_comparison_type_v, "signed char is permitted"); + static_assert(is_valid_comparison_type_v, "unsigned char is permitted"); + static_assert(is_valid_comparison_type_v, "long long is permitted"); + static_assert(is_valid_comparison_type_v, "uint64_t is permitted"); + + static_assert(!is_valid_comparison_type_v, "bool is not permitted"); + static_assert(!is_valid_comparison_type_v, "char is not permitted"); + static_assert(!is_valid_comparison_type_v, "char16_t is not permitted"); + static_assert(!is_valid_comparison_type_v, "char32_t is not permitted"); + static_assert(!is_valid_comparison_type_v, "wchar_t is not permitted"); + static_assert(!is_valid_comparison_type_v, "double is not permitted"); + static_assert(!is_valid_comparison_type_v, "int128_t is handled by its own overloads"); + static_assert(!is_valid_comparison_type_v, "uint128_t is handled by its own overloads"); + + #if defined(__cpp_char8_t) + static_assert(!is_valid_comparison_type_v, "char8_t is not permitted"); + #endif +} + +// +// constexpr usability of every function. +// +void test_constexpr() +{ + constexpr auto u_max {(std::numeric_limits::max)()}; + + static_assert(cmp_equal(uint128_t{5}, 5), ""); + static_assert(cmp_not_equal(u_max, int128_t{-1}), ""); + static_assert(cmp_less(int128_t{-1}, uint128_t{0}), ""); + static_assert(cmp_greater(uint128_t{0}, int128_t{-1}), ""); + static_assert(cmp_less_equal(int128_t{-1}, u_max), ""); + static_assert(cmp_greater_equal(uint128_t{5}, int128_t{5}), ""); + static_assert(!cmp_equal(u_max, int128_t{-1}), ""); + + static_assert(in_range(int128_t{127}), ""); + static_assert(!in_range(int128_t{-1}), ""); + static_assert(in_range(5), ""); + static_assert(!in_range(u_max), ""); +} + +#if defined(BOOST_INT128_HAS_INT128) || defined(BOOST_INT128_HAS_MSVC_INT128) + +// +// Oracle-based randomized testing. Each operand is reduced to (sign, magnitude) +// using native 128-bit casts, and the comparison is decided with native +// unsigned 128-bit compares, which are an independent ground truth for the +// (sign, magnitude) decomposition the library performs internally. +// +// Every call below this point is qualified. MSVC's builtin 128-bit types are +// classes in namespace std rather than fundamental types, so an unqualified call +// with one of them as an operand also finds the C++20 std::cmp_* and std::in_range +// by ADL and is ambiguous. __int128 is a fundamental type and has no associated +// namespace, so the qualification only matters on MSVC, but it is what pins the +// call to the library implementation under test on every platform. +// +using boost::int128::detail::builtin_i128; +using boost::int128::detail::builtin_u128; + +static void ref_sign_mag(builtin_i128 v, bool& neg, builtin_u128& mag) noexcept +{ + neg = v < 0; + // v + 1 first so the negation is exact even at the minimum value. + mag = neg ? static_cast(-(v + 1)) + 1U : static_cast(v); +} + +static void ref_sign_mag(builtin_u128 v, bool& neg, builtin_u128& mag) noexcept +{ + neg = false; + mag = v; +} + +static void ref_sign_mag(uint128_t v, bool& neg, builtin_u128& mag) noexcept +{ + neg = false; + mag = static_cast(v); +} + +static void ref_sign_mag(int128_t v, bool& neg, builtin_u128& mag) noexcept +{ + ref_sign_mag(static_cast(v), neg, mag); +} + +template ::value && std::is_signed::value, int> = 0> +static void ref_sign_mag(T v, bool& neg, builtin_u128& mag) noexcept +{ + ref_sign_mag(static_cast(v), neg, mag); +} + +template ::value && std::is_unsigned::value, int> = 0> +static void ref_sign_mag(T v, bool& neg, builtin_u128& mag) noexcept +{ + ref_sign_mag(static_cast(v), neg, mag); +} + +static bool truth_equal(bool na, builtin_u128 ma, bool nb, builtin_u128 mb) noexcept +{ + return na == nb && ma == mb; +} + +static bool truth_less(bool na, builtin_u128 ma, bool nb, builtin_u128 mb) noexcept +{ + if (na != nb) + { + return na; + } + + return na ? (ma > mb) : (ma < mb); +} + +template +static void check_pair(A a, B b) +{ + bool na {}; bool nb {}; + builtin_u128 ma {}; builtin_u128 mb {}; + ref_sign_mag(a, na, ma); + ref_sign_mag(b, nb, mb); + + const bool eq {truth_equal(na, ma, nb, mb)}; + const bool a_lt_b {truth_less(na, ma, nb, mb)}; + const bool b_lt_a {truth_less(nb, mb, na, ma)}; + + BOOST_TEST_EQ(boost::int128::cmp_equal(a, b), eq); + BOOST_TEST_EQ(boost::int128::cmp_not_equal(a, b), !eq); + BOOST_TEST_EQ(boost::int128::cmp_less(a, b), a_lt_b); + BOOST_TEST_EQ(boost::int128::cmp_greater(a, b), b_lt_a); + BOOST_TEST_EQ(boost::int128::cmp_less_equal(a, b), !b_lt_a); + BOOST_TEST_EQ(boost::int128::cmp_greater_equal(a, b), !a_lt_b); +} + +void test_random_oracle() +{ + std::mt19937_64 rng {42}; + std::uniform_int_distribution u_dist {0, UINT64_MAX}; + std::uniform_int_distribution i_dist {INT64_MIN, INT64_MAX}; + + constexpr std::size_t N {4096}; + + for (std::size_t i {0}; i < N; ++i) + { + const std::uint64_t hi {u_dist(rng)}; + const std::uint64_t lo {u_dist(rng)}; + const std::uint64_t hi2 {u_dist(rng)}; + const std::uint64_t lo2 {u_dist(rng)}; + + const uint128_t ua {hi, lo}; + const uint128_t ub {hi2, lo2}; + const int128_t ia {static_cast(hi), lo}; + const int128_t ib {static_cast(hi2), lo2}; + + const std::int64_t si {i_dist(rng)}; + const std::uint64_t su {u_dist(rng)}; + + const builtin_u128 bua {static_cast(ua)}; + const builtin_i128 bia {static_cast(ia)}; + + // library 128-bit vs library 128-bit, every signedness combination + check_pair(ua, ub); + check_pair(ia, ib); + check_pair(ua, ib); + check_pair(ia, ub); + + // library 128-bit vs builtin integer + check_pair(ua, si); + check_pair(ua, su); + check_pair(ia, si); + check_pair(ia, su); + + // builtin integer vs library 128-bit + check_pair(si, ua); + check_pair(su, ua); + check_pair(si, ia); + check_pair(su, ia); + + // builtin 128-bit types in every position + check_pair(bua, bia); + check_pair(bia, bua); + check_pair(bua, ib); + check_pair(ia, bua); + check_pair(bia, si); + check_pair(su, bia); + } +} + +// +// Known-answer coverage for the builtin/extended 128-bit types. +// +void test_builtin_128() +{ + const builtin_u128 bu_max {static_cast(-1)}; // 2^128 - 1 + const builtin_i128 bi_neg1 {-1}; + const builtin_i128 bi_5 {5}; + const builtin_u128 bu_5 {5}; + + constexpr auto u_max {(std::numeric_limits::max)()}; + constexpr auto i_max {(std::numeric_limits::max)()}; + + // trait classification + static_assert(boost::int128::detail::is_int128_type_v, "builtin signed is a 128-bit type"); + static_assert(boost::int128::detail::is_int128_type_v, "builtin unsigned is a 128-bit type"); + static_assert(boost::int128::detail::is_valid_comparison_operand_v, "builtin signed is an operand"); + static_assert(boost::int128::detail::is_valid_comparison_operand_v, "builtin unsigned is an operand"); + + // library 128-bit vs builtin 128-bit + BOOST_TEST(!boost::int128::cmp_equal(u_max, bi_neg1)); + BOOST_TEST(!boost::int128::cmp_equal(bu_max, int128_t{-1})); + BOOST_TEST(boost::int128::cmp_less(bi_neg1, uint128_t{0})); + BOOST_TEST(boost::int128::cmp_greater(uint128_t{0}, bi_neg1)); + BOOST_TEST(boost::int128::cmp_equal(uint128_t{bu_max}, bu_max)); + + // builtin 128-bit vs builtin 128-bit, cross signedness + BOOST_TEST(!boost::int128::cmp_equal(bu_max, bi_neg1)); + BOOST_TEST(boost::int128::cmp_less(bi_neg1, bu_max)); + BOOST_TEST(boost::int128::cmp_greater(bu_max, bi_neg1)); + BOOST_TEST(boost::int128::cmp_equal(bi_5, bu_5)); + BOOST_TEST(boost::int128::cmp_not_equal(bi_neg1, bu_5)); + + // builtin 128-bit vs standard integer + BOOST_TEST(boost::int128::cmp_less(bi_neg1, 5U)); + BOOST_TEST(boost::int128::cmp_equal(bi_5, 5)); + BOOST_TEST(boost::int128::cmp_equal(bu_5, 5)); + BOOST_TEST(boost::int128::cmp_greater(5, bi_neg1)); + BOOST_TEST(boost::int128::cmp_less_equal(bi_neg1, 0U)); + + // in_range with a builtin 128-bit target and/or value + BOOST_TEST(!boost::int128::in_range(u_max)); + BOOST_TEST(!boost::int128::in_range(int128_t{-1})); + BOOST_TEST(!boost::int128::in_range(bu_max)); + BOOST_TEST(boost::int128::in_range(bi_neg1)); + BOOST_TEST(boost::int128::in_range(bi_5)); + BOOST_TEST(boost::int128::in_range(i_max)); +} + +#endif // builtin 128-bit types + +int main() +{ + test_known_answers(); + test_in_range(); + test_constraints(); + test_constexpr(); + + #if defined(BOOST_INT128_HAS_INT128) || defined(BOOST_INT128_HAS_MSVC_INT128) + test_random_oracle(); + test_builtin_128(); + #endif + + return boost::report_errors(); +}