diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d40c210..8029760 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: uses: boostorg/boost-ci/.github/workflows/reusable.yml@master with: exclude_compiler: 'clang-3.5,clang-3.6,clang-3.7,clang-3.8,clang-3.9,clang-4.0,clang-5.0,clang-6.0,clang-7, - clang-8,clang-9,clang-10,clang-11,clang-12 + clang-8,clang-9,clang-10,clang-11,clang-12, gcc-4.7,gcc-4.8,gcc-4.9,gcc-5,gcc-6,gcc-7,gcc-8,gcc-9,gcc-10' # Example of customization: # with: diff --git a/include/boost/safe_numbers.hpp b/include/boost/safe_numbers.hpp index 5a0f0da..8b746cd 100644 --- a/include/boost/safe_numbers.hpp +++ b/include/boost/safe_numbers.hpp @@ -6,6 +6,7 @@ #define BOOST_SAFENUMBERS_HPP #include +#include #include #include #include diff --git a/include/boost/safe_numbers/detail/int128/random.hpp b/include/boost/safe_numbers/detail/int128/random.hpp new file mode 100644 index 0000000..e85824b --- /dev/null +++ b/include/boost/safe_numbers/detail/int128/random.hpp @@ -0,0 +1,96 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifndef BOOST_SAFE_NUMBERS_DETAIL_INT128_RANDOM_HPP +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_RANDOM_HPP + +#include "int128.hpp" + +namespace boost { +namespace random { +namespace traits { + +template +struct make_unsigned_imp; + +template <> +struct make_unsigned_imp +{ + using type = int128::uint128_t; +}; + +template <> +struct make_unsigned_imp +{ + using type = int128::uint128_t; +}; + +template +struct make_unsigned; + +template <> +struct make_unsigned +{ + using type = int128::uint128_t; +}; + +template <> +struct make_unsigned +{ + using type = int128::int128_t; +}; + +template +struct make_unsigned_or_unbounded_imp; + +template <> +struct make_unsigned_or_unbounded_imp +{ + using type = int128::uint128_t; +}; + +template <> +struct make_unsigned_or_unbounded_imp +{ + using type = int128::uint128_t; +}; + +template +struct make_unsigned_or_unbounded; + +template <> +struct make_unsigned_or_unbounded +{ + using type = int128::uint128_t; +}; + +template <> +struct make_unsigned_or_unbounded +{ + using type = int128::uint128_t; +}; + +template +struct is_integral; + +template <> +struct is_integral : std::true_type {}; + +template <> +struct is_integral : std::true_type {}; + +template +struct is_signed; + +template <> +struct is_signed : std::false_type {}; + +template <> +struct is_signed : std::true_type {}; + +} // namespace traits +} // namespace random +} // namespace boost + +#endif //BOOST_SAFE_NUMBERS_DETAIL_INT128_RANDOM_HPP diff --git a/include/boost/safe_numbers/detail/signed_integer_basis.hpp b/include/boost/safe_numbers/detail/signed_integer_basis.hpp new file mode 100644 index 0000000..a8dfff0 --- /dev/null +++ b/include/boost/safe_numbers/detail/signed_integer_basis.hpp @@ -0,0 +1,606 @@ +// Copyright 2025 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifndef BOOST_SAFE_NUMBERS_SIGNED_INTEGER_BASIS_HPP +#define BOOST_SAFE_NUMBERS_SIGNED_INTEGER_BASIS_HPP + +#include +#include +#include +#include + +#ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif // BOOST_SAFE_NUMBERS_BUILD_MODULE + +namespace boost::safe_numbers::detail { + +template +class signed_integer_basis +{ +public: + + // This is exposed to the user so that they can convert back to built-in + using basis_type = BasisType; + +private: + + BasisType basis_ {0U}; + +public: + + constexpr signed_integer_basis() noexcept = default; + + explicit constexpr signed_integer_basis(const BasisType val) noexcept : basis_{val} {} + + template + requires std::is_same_v + explicit constexpr signed_integer_basis(T) noexcept + { + static_assert(dependent_false, "Construction from bool is not allowed"); + } + + template + [[nodiscard]] explicit constexpr operator OtherBasis() const; + + [[nodiscard]] explicit constexpr operator BasisType() const noexcept { return basis_; } + + [[nodiscard]] friend constexpr auto operator<=>(signed_integer_basis lhs, signed_integer_basis rhs) noexcept + -> std::strong_ordering = default; + + [[nodiscard]] constexpr auto operator+() const noexcept -> signed_integer_basis; + + [[nodiscard]] constexpr auto operator-() const -> signed_integer_basis; + + template + constexpr auto operator+=(signed_integer_basis rhs) -> signed_integer_basis&; +}; + +// Helper for diagnostic messages +template +constexpr auto signed_type_name() noexcept -> const char* +{ + if constexpr (std::is_same_v) + { + return "i8"; + } + else if constexpr (std::is_same_v) + { + return "i16"; + } + else if constexpr (std::is_same_v) + { + return "i32"; + } + else if constexpr (std::is_same_v) + { + return "i64"; + } + else + { + return "i128"; + } +} + +template +template +constexpr signed_integer_basis::operator OtherBasis() const +{ + if constexpr (sizeof(OtherBasis) < sizeof(BasisType)) + { + if (basis_ > static_cast(std::numeric_limits::max())) + { + BOOST_THROW_EXCEPTION(std::domain_error(std::string("Overflow in ") + signed_type_name() + " to " + signed_type_name() + " conversion")); + } + else if (basis_ < static_cast(std::numeric_limits::min())) + { + BOOST_THROW_EXCEPTION(std::domain_error(std::string("Underflow in ") + signed_type_name() + " to " + signed_type_name() + " conversion")); + } + } + + return static_cast(basis_); +} + +template +constexpr auto signed_integer_basis::operator+() const noexcept -> signed_integer_basis +{ + return signed_integer_basis{basis_}; +} + +template +constexpr auto signed_integer_basis::operator-() const -> signed_integer_basis +{ + if (basis_ == std::numeric_limits::min()) [[unlikely]] + { + BOOST_THROW_EXCEPTION(std::domain_error(std::string("Overflow in ") + signed_type_name() + " unary minus operator")); + } + + return signed_integer_basis{static_cast(-basis_)}; +} + +// ------------------------------ +// Addition +// ------------------------------ + +namespace impl { + +enum class signed_overflow_status +{ + no_error, + overflow, + underflow +}; + +template +struct make_unsigned_helper +{ + using type = std::make_unsigned_t; +}; + +template <> +struct make_unsigned_helper +{ + using type = int128::uint128_t; +}; + +template +using make_unsigned_helper_t = typename make_unsigned_helper::type; + +// Determines the direction of signed overflow from the lhs operand. +// Signed addition overflow only occurs when both operands share the same sign, +// so the sign of lhs is sufficient to determine direction. +template +constexpr auto classify_signed_overflow(const T lhs) noexcept -> signed_overflow_status +{ + return lhs >= 0 ? signed_overflow_status::overflow : signed_overflow_status::underflow; +} + +#if BOOST_SAFE_NUMBERS_HAS_BUILTIN(__builtin_add_overflow) + +template +auto signed_intrin_add(const T lhs, const T rhs, T& result) -> signed_overflow_status +{ + if (__builtin_add_overflow(lhs, rhs, &result)) + { + return classify_signed_overflow(lhs); + } + + return signed_overflow_status::no_error; +} + +#elif defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X64_INTRIN) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) + +template +auto signed_intrin_add(const T lhs, const T rhs, T& result) -> signed_overflow_status +{ + using unsigned_t = std::make_unsigned_t; + + unsigned_t temp {}; + + if constexpr (std::is_same_v) + { + _addcarry_u8(0, static_cast(lhs), static_cast(rhs), &temp); + } + else if constexpr (std::is_same_v) + { + _addcarry_u16(0, static_cast(lhs), static_cast(rhs), &temp); + } + else if constexpr (std::is_same_v) + { + _addcarry_u32(0, static_cast(lhs), static_cast(rhs), &temp); + } + #if defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) + else if constexpr (std::is_same_v) + { + // x86 does not provide the _addcarry_u64 intrinsic + temp = static_cast(lhs) + static_cast(rhs); + } + #else + else if constexpr (std::is_same_v) + { + _addcarry_u64(0, static_cast(lhs), static_cast(rhs), &temp); + } + #endif + + result = static_cast(temp); + + // _addcarry detects unsigned carry, not signed overflow + // Signed overflow: both operands have the same sign but the result has a different sign + const auto ulhs {static_cast(lhs)}; + const auto urhs {static_cast(rhs)}; + const auto has_overflow {static_cast(((~(ulhs ^ urhs)) & (ulhs ^ temp)) >> std::numeric_limits::digits)}; + + if (has_overflow) + { + return classify_signed_overflow(lhs); + } + + return signed_overflow_status::no_error; +} + +#endif + +template +constexpr auto signed_no_intrin_add(const T lhs, const T rhs, T& result) noexcept -> signed_overflow_status +{ + using unsigned_t = make_unsigned_helper_t; + unsigned_t temp {}; + + if constexpr (std::is_same_v || std::is_same_v) + { + temp = static_cast(static_cast(lhs) + static_cast(rhs)); + } + else + { + temp = static_cast(lhs) + static_cast(rhs); + } + + result = static_cast(temp); + + // Signed overflow: both operands have the same sign but the result has a different sign + const auto ulhs {static_cast(lhs)}; + const auto urhs {static_cast(rhs)}; + const auto has_overflow {static_cast(((~(ulhs ^ urhs)) & (ulhs ^ temp)) >> std::numeric_limits::digits)}; + + if (has_overflow) + { + return classify_signed_overflow(lhs); + } + + return signed_overflow_status::no_error; +} + +template +struct signed_add_helper +{ + [[nodiscard]] static constexpr auto apply(const signed_integer_basis lhs, + const signed_integer_basis rhs) + noexcept(Policy != overflow_policy::throw_exception) + -> signed_integer_basis + { + using result_type = signed_integer_basis; + + const auto lhs_basis {static_cast(lhs)}; + const auto rhs_basis {static_cast(rhs)}; + BasisType result {}; + + auto handle_error = [&result](signed_overflow_status status) + { + if (std::is_constant_evaluated()) + { + if (status == signed_overflow_status::overflow) + { + if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i8 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i16 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i32 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i64 addition"); + } + else + { + throw std::overflow_error("Overflow detected in i128 addition"); + } + } + else + { + if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i8 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i16 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i32 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i64 addition"); + } + else + { + throw std::underflow_error("Underflow detected in i128 addition"); + } + } + } + else + { + if constexpr (Policy == overflow_policy::throw_exception) + { + static_cast(result); + const auto message {std::string(status == signed_overflow_status::overflow ? "Overflow" : "Underflow") + + " detected in " + signed_type_name() + " addition"}; + + if (status == signed_overflow_status::overflow) + { + BOOST_THROW_EXCEPTION(std::overflow_error(message)); + } + else + { + BOOST_THROW_EXCEPTION(std::underflow_error(message)); + } + } + else + { + static_cast(result); + BOOST_SAFE_NUMBERS_UNREACHABLE; + } + } + }; + + #if BOOST_SAFE_NUMBERS_HAS_BUILTIN(__builtin_add_overflow) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X64_INTRIN) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) + + if constexpr (!std::is_same_v) + { + if (!std::is_constant_evaluated()) + { + const auto status {impl::signed_intrin_add(lhs_basis, rhs_basis, result)}; + if (status != signed_overflow_status::no_error) + { + handle_error(status); + } + + return result_type{result}; + } + } + + #endif // BOOST_SAFE_NUMBERS_HAS_BUILTIN(__builtin_add_overflow) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X64_INTRIN) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) + + const auto status {impl::signed_no_intrin_add(lhs_basis, rhs_basis, result)}; + if (status != signed_overflow_status::no_error) + { + handle_error(status); + } + + return result_type{result}; + } +}; + +template +[[nodiscard]] constexpr auto add_impl(const signed_integer_basis lhs, + const signed_integer_basis rhs) +{ + return signed_add_helper::apply(lhs,rhs); +} + +} // namespace impl + +template +[[nodiscard]] constexpr auto operator+(const signed_integer_basis lhs, + const signed_integer_basis rhs) -> signed_integer_basis +{ + if (std::is_constant_evaluated()) + { + BasisType res {}; + const auto status {impl::signed_no_intrin_add(static_cast(lhs), static_cast(rhs), res)}; + if (status == impl::signed_overflow_status::overflow) + { + if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i8 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i16 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i32 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::overflow_error("Overflow detected in i64 addition"); + } + else + { + throw std::overflow_error("Overflow detected in i128 addition"); + } + } + else if (status == impl::signed_overflow_status::underflow) + { + if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i8 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i16 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i32 addition"); + } + else if constexpr (std::is_same_v) + { + throw std::underflow_error("Underflow detected in i64 addition"); + } + else + { + throw std::underflow_error("Underflow detected in i128 addition"); + } + } + + return signed_integer_basis{res}; + } + + return impl::signed_add_helper::apply(lhs, rhs); +} + +} // namespace boost::safe_numbers::detail + +#define BOOST_SAFE_NUMBERS_DEFINE_MIXED_SIGNED_INTEGER_OP(OP_NAME, OP_SYMBOL) \ +template \ + requires (!std::is_same_v) \ +constexpr auto OP_SYMBOL(const boost::safe_numbers::detail::signed_integer_basis, \ + const boost::safe_numbers::detail::signed_integer_basis) \ +{ \ + if constexpr (std::is_same_v) \ + { \ + if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i8 and i16"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i8 and i32"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i8 and i64"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i8 and i128"); \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i8 and unknown type"); \ + } \ + } \ + else if constexpr (std::is_same_v) \ + { \ + if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i16 and i8"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i16 and i32"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i16 and i64"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i16 and i128"); \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i16 and unknown type"); \ + } \ + } \ + else if constexpr (std::is_same_v) \ + { \ + if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i32 and i8"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i32 and i16"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i32 and i64"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i32 and i128"); \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i32 and unknown type"); \ + } \ + } \ + else if constexpr (std::is_same_v) \ + { \ + if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i64 and i8"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i64 and i16"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i64 and i32"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i64 and i128"); \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i64 and unknown type"); \ + } \ + } \ + else if constexpr (std::is_same_v) \ + { \ + if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i128 and i8"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i128 and i16"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i128 and i32"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i128 and i64"); \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between i128 and unknown type"); \ + } \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " on mixed width signed integer types"); \ + } \ + \ + return boost::safe_numbers::detail::signed_integer_basis(0); \ +} + + +namespace boost::safe_numbers::detail { + +BOOST_SAFE_NUMBERS_DEFINE_MIXED_SIGNED_INTEGER_OP("comparison", operator<=>) +BOOST_SAFE_NUMBERS_DEFINE_MIXED_SIGNED_INTEGER_OP("equality", operator==) + +BOOST_SAFE_NUMBERS_DEFINE_MIXED_SIGNED_INTEGER_OP("addition", operator+) + +template +template +constexpr auto signed_integer_basis::operator+=(const signed_integer_basis rhs) + -> signed_integer_basis& +{ + *this = *this + rhs; + return *this; +} + +} // namespace boost::safe_numbers::detail + +#undef BOOST_SAFE_NUMBERS_DEFINE_MIXED_SIGNED_INTEGER_OP + +#endif //BOOST_SAFE_NUMBERS_SIGNED_INTEGER_BASIS_HPP diff --git a/include/boost/safe_numbers/detail/type_traits.hpp b/include/boost/safe_numbers/detail/type_traits.hpp index 6656860..db367d0 100644 --- a/include/boost/safe_numbers/detail/type_traits.hpp +++ b/include/boost/safe_numbers/detail/type_traits.hpp @@ -34,9 +34,18 @@ inline constexpr bool is_fundamental_unsigned_integral_v = impl::is_fundamental_ template concept fundamental_unsigned_integral = is_fundamental_unsigned_integral_v; +template +inline constexpr bool is_fundamental_signed_integral_v = impl::is_fundamental_signed_integral::value; + +template +concept fundamental_signed_integral = is_fundamental_signed_integral_v; + template class unsigned_integer_basis; +template +class signed_integer_basis; + // is_unsigned_library_type (base + unsigned_integer_basis specialization) namespace impl { @@ -44,14 +53,23 @@ namespace impl { template struct is_unsigned_library_type : std::false_type {}; +template +struct is_signed_library_type : std::false_type {}; + template struct is_unsigned_library_type> : std::true_type {}; +template +struct is_signed_library_type> : std::true_type {}; + } // namespace impl template inline constexpr bool is_unsigned_library_type_v = impl::is_unsigned_library_type::value; +template +inline constexpr bool is_signed_library_type_v = impl::is_signed_library_type::value; + // underlying type trait (base + unsigned_integer_basis specialization) namespace impl { @@ -68,6 +86,12 @@ struct underlying> using type = T; }; +template +struct underlying> +{ + using type = T; +}; + } // namespace impl template diff --git a/include/boost/safe_numbers/detail/unsigned_integer_basis.hpp b/include/boost/safe_numbers/detail/unsigned_integer_basis.hpp index cc0a80f..89dc289 100644 --- a/include/boost/safe_numbers/detail/unsigned_integer_basis.hpp +++ b/include/boost/safe_numbers/detail/unsigned_integer_basis.hpp @@ -55,7 +55,7 @@ class unsigned_integer_basis template [[nodiscard]] explicit constexpr operator OtherBasis() const; - explicit constexpr operator BasisType() const noexcept { return basis_;} + [[nodiscard]] explicit constexpr operator BasisType() const noexcept { return basis_;} [[nodiscard]] friend constexpr auto operator<=>(unsigned_integer_basis lhs, unsigned_integer_basis rhs) noexcept -> std::strong_ordering = default; @@ -294,11 +294,10 @@ struct add_helper } }; - #if BOOST_SAFE_NUMBERS_HAS_BUILTIN(__builtin_add_overflow) || BOOST_SAFE_NUMBERS_HAS_BUILTIN(_addcarry_u64) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) + #if BOOST_SAFE_NUMBERS_HAS_BUILTIN(__builtin_add_overflow) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X64_INTRIN) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) if constexpr (!std::is_same_v) { - if (!std::is_constant_evaluated()) { if (impl::unsigned_intrin_add(lhs_basis, rhs_basis, res)) @@ -308,10 +307,9 @@ struct add_helper return result_type{res}; } - } - #endif // BOOST_SAFE_NUMBERS_HAS_BUILTIN(__builtin_add_overflow) || BOOST_SAFE_NUMBERS_HAS_BUILTIN(_addcarry_u64) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) + #endif // BOOST_SAFE_NUMBERS_HAS_BUILTIN(__builtin_add_overflow) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X64_INTRIN) || defined(BOOST_SAFENUMBERS_HAS_WINDOWS_X86_INTRIN) if (impl::unsigned_no_intrin_add(lhs_basis, rhs_basis, res)) { @@ -488,95 +486,134 @@ template } // namespace boost::safe_numbers::detail -#define BOOST_SAFE_NUMBERS_DEFINE_MIXED_UNSIGNED_INTEGER_OP(OP_NAME, OP_SYMBOL) \ -template \ - requires (!std::is_same_v) \ -constexpr auto OP_SYMBOL(const boost::safe_numbers::detail::unsigned_integer_basis, \ - const boost::safe_numbers::detail::unsigned_integer_basis) \ -{ \ - if constexpr (std::is_same_v) \ - { \ - if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u8 and u16"); \ - } \ - else if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u8 and u32"); \ - } \ - else if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u8 and u64"); \ - } \ - else \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u8 and unknown type"); \ - } \ - } \ - else if constexpr (std::is_same_v) \ - { \ - if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u16 and u8"); \ - } \ - else if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u16 and u32"); \ - } \ - else if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u16 and u64"); \ - } \ - else \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u16 and unknown type"); \ - } \ - } \ - else if constexpr (std::is_same_v) \ - { \ - if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u32 and u8"); \ - } \ - else if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u32 and u16"); \ - } \ - else if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u32 and u64"); \ - } \ - else \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u32 and unknown type"); \ - } \ - } \ - else if constexpr (std::is_same_v) \ - { \ - if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u64 and u8"); \ - } \ - else if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u64 and u16"); \ - } \ - else if constexpr (std::is_same_v) \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u64 and u32"); \ - } \ - else \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u64 and unknown type"); \ - } \ - } \ - else \ - { \ - static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " on mixed width unsigned integer types"); \ - } \ - \ - return boost::safe_numbers::detail::unsigned_integer_basis(0); \ +#define BOOST_SAFE_NUMBERS_DEFINE_MIXED_UNSIGNED_INTEGER_OP(OP_NAME, OP_SYMBOL) \ +template \ + requires (!std::is_same_v) \ +constexpr auto OP_SYMBOL(const boost::safe_numbers::detail::unsigned_integer_basis, \ + const boost::safe_numbers::detail::unsigned_integer_basis) \ +{ \ + if constexpr (std::is_same_v) \ + { \ + if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u8 and u16"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u8 and u32"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u8 and u64"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u8 and u128"); \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u8 and unknown type"); \ + } \ + } \ + else if constexpr (std::is_same_v) \ + { \ + if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u16 and u8"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u16 and u32"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u16 and u64"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u16 and u128"); \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u16 and unknown type"); \ + } \ + } \ + else if constexpr (std::is_same_v) \ + { \ + if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u32 and u8"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u32 and u16"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u32 and u64"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u32 and u128"); \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u32 and unknown type"); \ + } \ + } \ + else if constexpr (std::is_same_v) \ + { \ + if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u64 and u8"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u64 and u16"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u64 and u32"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u64 and u128"); \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u64 and unknown type"); \ + } \ + } \ + else if constexpr (std::is_same_v) \ + { \ + if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u128 and u8"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u128 and u16"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u128 and u32"); \ + } \ + else if constexpr (std::is_same_v) \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u128 and u64"); \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " between u128 and unknown type"); \ + } \ + } \ + else \ + { \ + static_assert(boost::safe_numbers::detail::dependent_false, "Can not perform " OP_NAME " on mixed width unsigned integer types"); \ + } \ + \ + return boost::safe_numbers::detail::unsigned_integer_basis(0); \ } namespace boost::safe_numbers::detail { diff --git a/include/boost/safe_numbers/limits.hpp b/include/boost/safe_numbers/limits.hpp index 5711fc3..22a2eaa 100644 --- a/include/boost/safe_numbers/limits.hpp +++ b/include/boost/safe_numbers/limits.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE @@ -90,6 +91,26 @@ template <> class numeric_limits : public boost::safe_numbers::detail::numeric_limits_impl {}; +template <> +class numeric_limits : + public boost::safe_numbers::detail::numeric_limits_impl {}; + +template <> +class numeric_limits : + public boost::safe_numbers::detail::numeric_limits_impl {}; + +template <> +class numeric_limits : + public boost::safe_numbers::detail::numeric_limits_impl {}; + +template <> +class numeric_limits : + public boost::safe_numbers::detail::numeric_limits_impl {}; + +template <> +class numeric_limits : + public boost::safe_numbers::detail::numeric_limits_impl {}; + template class numeric_limits> { diff --git a/include/boost/safe_numbers/signed_integers.hpp b/include/boost/safe_numbers/signed_integers.hpp new file mode 100644 index 0000000..c7fa21e --- /dev/null +++ b/include/boost/safe_numbers/signed_integers.hpp @@ -0,0 +1,33 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifndef BOOST_SAFE_NUMBERS_SIGNED_INTEGERS_HPP +#define BOOST_SAFE_NUMBERS_SIGNED_INTEGERS_HPP + +#include +#include +#include "detail/int128/int128.hpp" +#include "detail/int128/random.hpp" + +#ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE + +#include + +#endif + +namespace boost::safe_numbers { + +BOOST_SAFE_NUMBERS_EXPORT using i8 = detail::signed_integer_basis; + +BOOST_SAFE_NUMBERS_EXPORT using i16 = detail::signed_integer_basis; + +BOOST_SAFE_NUMBERS_EXPORT using i32 = detail::signed_integer_basis; + +BOOST_SAFE_NUMBERS_EXPORT using i64 = detail::signed_integer_basis; + +BOOST_SAFE_NUMBERS_EXPORT using i128 = detail::signed_integer_basis; + +} // namespace boost::safe_numbers + +#endif // BOOST_SAFE_NUMBERS_SIGNED_INTEGERS_HPP diff --git a/test/Jamfile b/test/Jamfile index 67bf4e9..4fd2304 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -50,8 +50,13 @@ project : requirements run quick.cpp ; run test_unsigned_construction.cpp ; +run test_signed_construction.cpp ; compile compile_integer_type_properties.cpp ; run test_unsigned_narrowing_conversions.cpp ; +run test_signed_conversions.cpp ; +run test_signed_comparisons.cpp ; +run test_signed_unary_operators.cpp ; +run test_signed_addition.cpp ; run test_unsigned_addition.cpp ; compile-fail compile_fail_unsigned_addition.cpp ; diff --git a/test/benchmarks/benchmark_boost.cpp b/test/benchmarks/benchmark_boost.cpp index 7c41f2b..1ba2119 100644 --- a/test/benchmarks/benchmark_boost.cpp +++ b/test/benchmarks/benchmark_boost.cpp @@ -17,6 +17,7 @@ # pragma clang diagnostic ignored "-Woverflow" # pragma clang diagnostic ignored "-Wdouble-promotion" # pragma clang diagnostic ignored "-Wdivision-by-zero" +# pragma clang diagnostic ignored "-Wdeprecated-declarations" # if (__clang_major__ >= 10 && !defined(__APPLE__)) || __clang_major__ >= 13 # pragma clang diagnostic ignored "-Wdeprecated-copy" diff --git a/test/test_signed_addition.cpp b/test/test_signed_addition.cpp new file mode 100644 index 0000000..5a5758e --- /dev/null +++ b/test/test_signed_addition.cpp @@ -0,0 +1,322 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wold-style-cast" +# pragma clang diagnostic ignored "-Wundef" +# pragma clang diagnostic ignored "-Wconversion" +# pragma clang diagnostic ignored "-Wsign-conversion" +# pragma clang diagnostic ignored "-Wfloat-equal" +# pragma clang diagnostic ignored "-Wsign-compare" +# pragma clang diagnostic ignored "-Woverflow" + +# if (__clang_major__ >= 10 && !defined(__APPLE__)) || __clang_major__ >= 13 +# pragma clang diagnostic ignored "-Wdeprecated-copy" +# endif + +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wold-style-cast" +# pragma GCC diagnostic ignored "-Wundef" +# pragma GCC diagnostic ignored "-Wconversion" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wfloat-equal" +# pragma GCC diagnostic ignored "-Woverflow" + +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4389) +# pragma warning(disable : 4127) +# pragma warning(disable : 4305) +# pragma warning(disable : 4309) +#endif + +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_COMPARE +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_CONVERSION + +#include + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + +// Ignore [[nodiscard]] on the tests that we know are going to throw +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-result" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-result" +#elif defined(_MSC_VER) +# pragma warning (push) +# pragma warning (disable: 4834) +#endif + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include +#include +#include + +#endif + +using namespace boost::safe_numbers; + +inline std::mt19937_64 rng{42}; +inline constexpr std::size_t N {1024}; + +// ----------------------------------------------- +// Valid addition: values in the safe half-range +// ----------------------------------------------- + +template +void test_valid_addition() +{ + using basis_type = detail::underlying_type_t; + + // Use quarter-range to ensure no overflow when adding two values + boost::random::uniform_int_distribution dist {static_cast(std::numeric_limits::min() / 2), + static_cast(std::numeric_limits::max() / 2)}; + + for (std::size_t i {0}; i < N; ++i) + { + const auto lhs_value {dist(rng)}; + const auto rhs_value {dist(rng)}; + + // For int8_t/int16_t, promotion to int means no overflow in the builtin addition + T ref_value {}; + if constexpr (std::is_same_v || std::is_same_v) + { + ref_value = T{static_cast(static_cast(lhs_value) + static_cast(rhs_value))}; + } + else + { + ref_value = T{static_cast(lhs_value + rhs_value)}; + } + + const T lhs {lhs_value}; + const T rhs {rhs_value}; + const auto res {lhs + rhs}; + + BOOST_TEST(ref_value == res); + } +} + +// ----------------------------------------------- +// Positive overflow: near-max + positive +// ----------------------------------------------- + +template +void test_positive_overflow() +{ + using basis_type = detail::underlying_type_t; + boost::random::uniform_int_distribution dist {basis_type{2}, + std::numeric_limits::max()}; + + for (std::size_t i {0}; i < N; ++i) + { + constexpr auto lhs_value {static_cast(std::numeric_limits::max() - 1)}; + const auto rhs_value {dist(rng)}; + + const T lhs {lhs_value}; + const T rhs {rhs_value}; + + BOOST_TEST_THROWS(lhs + rhs, std::overflow_error); + } +} + +// ----------------------------------------------- +// Negative overflow (underflow): near-min + negative +// ----------------------------------------------- + +template +void test_negative_overflow() +{ + using basis_type = detail::underlying_type_t; + boost::random::uniform_int_distribution dist {std::numeric_limits::min(), + basis_type{-2}}; + + for (std::size_t i {0}; i < N; ++i) + { + constexpr auto lhs_value {static_cast(std::numeric_limits::min() + 1)}; + const auto rhs_value {dist(rng)}; + + const T lhs {lhs_value}; + const T rhs {rhs_value}; + + BOOST_TEST_THROWS(lhs + rhs, std::underflow_error); + } +} + +// ----------------------------------------------- +// Mixed-sign addition: should never overflow +// Positive + negative can never exceed the range +// ----------------------------------------------- + +template +void test_mixed_sign_addition() +{ + using basis_type = detail::underlying_type_t; + boost::random::uniform_int_distribution pos_dist {basis_type{0}, + std::numeric_limits::max()}; + boost::random::uniform_int_distribution neg_dist {std::numeric_limits::min(), + basis_type{-1}}; + + for (std::size_t i {0}; i < N; ++i) + { + const auto pos_value {pos_dist(rng)}; + const auto neg_value {neg_dist(rng)}; + + const T pos {pos_value}; + const T neg {neg_value}; + + // This should never throw + const auto res {pos + neg}; + + // Verify the result + T expected {}; + if constexpr (std::is_same_v || std::is_same_v) + { + expected = T{static_cast(static_cast(pos_value) + static_cast(neg_value))}; + } + else + { + expected = T{static_cast(pos_value + neg_value)}; + } + + BOOST_TEST(res == expected); + } +} + +// ----------------------------------------------- +// Compound assignment: operator+= +// ----------------------------------------------- + +template +void test_plus_equals() +{ + using basis_type = detail::underlying_type_t; + + // Use quarter-range to ensure no overflow + boost::random::uniform_int_distribution dist {static_cast(std::numeric_limits::min() / 2), + static_cast(std::numeric_limits::max() / 2)}; + + for (std::size_t i {0}; i < N; ++i) + { + const auto lhs_value {dist(rng)}; + const auto rhs_value {dist(rng)}; + + const T lhs {lhs_value}; + const T rhs {rhs_value}; + + // operator+ result for reference + const auto expected {lhs + rhs}; + + // operator+= should give the same result + T result {lhs_value}; + result += rhs; + BOOST_TEST(result == expected); + } + + // += with overflow should throw + T near_max {static_cast(std::numeric_limits::max() - 1)}; + BOOST_TEST_THROWS(near_max += T{basis_type{2}}, std::overflow_error); + + // += with underflow should throw + T near_min {static_cast(std::numeric_limits::min() + 1)}; + BOOST_TEST_THROWS(near_min += T{basis_type{-2}}, std::underflow_error); + + // += with zero is identity + T val {basis_type{42}}; + val += T{basis_type{0}}; + BOOST_TEST(val == T{basis_type{42}}); +} + +// ----------------------------------------------- +// Specific boundary cases +// ----------------------------------------------- + +template +void test_boundary_cases() +{ + using basis_type = detail::underlying_type_t; + + // 0 + 0 = 0 + BOOST_TEST(T{basis_type{0}} + T{basis_type{0}} == T{basis_type{0}}); + + // 1 + (-1) = 0 + BOOST_TEST(T{basis_type{1}} + T{basis_type{-1}} == T{basis_type{0}}); + + // max + 0 = max + BOOST_TEST(T{std::numeric_limits::max()} + T{basis_type{0}} == T{std::numeric_limits::max()}); + + // min + 0 = min + BOOST_TEST(T{std::numeric_limits::min()} + T{basis_type{0}} == T{std::numeric_limits::min()}); + + // max + (-max) = 0 + BOOST_TEST(T{std::numeric_limits::max()} + T{static_cast(-std::numeric_limits::max())} == T{basis_type{0}}); + + // max + 1 overflows + BOOST_TEST_THROWS(T{std::numeric_limits::max()} + T{basis_type{1}}, std::overflow_error); + + // min + (-1) underflows + BOOST_TEST_THROWS(T{std::numeric_limits::min()} + T{basis_type{-1}}, std::underflow_error); + + // max + min = -1 (should succeed) + BOOST_TEST(T{std::numeric_limits::max()} + T{std::numeric_limits::min()} == T{basis_type{-1}}); +} + +int main() +{ + test_valid_addition(); + test_valid_addition(); + test_valid_addition(); + test_valid_addition(); + test_valid_addition(); + + test_positive_overflow(); + test_positive_overflow(); + test_positive_overflow(); + test_positive_overflow(); + test_positive_overflow(); + + test_negative_overflow(); + test_negative_overflow(); + test_negative_overflow(); + test_negative_overflow(); + test_negative_overflow(); + + test_mixed_sign_addition(); + test_mixed_sign_addition(); + test_mixed_sign_addition(); + test_mixed_sign_addition(); + test_mixed_sign_addition(); + + test_plus_equals(); + test_plus_equals(); + test_plus_equals(); + test_plus_equals(); + test_plus_equals(); + + test_boundary_cases(); + test_boundary_cases(); + test_boundary_cases(); + test_boundary_cases(); + test_boundary_cases(); + + return boost::report_errors(); +} diff --git a/test/test_signed_comparisons.cpp b/test/test_signed_comparisons.cpp new file mode 100644 index 0000000..8798469 --- /dev/null +++ b/test/test_signed_comparisons.cpp @@ -0,0 +1,107 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wold-style-cast" +# pragma clang diagnostic ignored "-Wundef" +# pragma clang diagnostic ignored "-Wconversion" +# pragma clang diagnostic ignored "-Wsign-conversion" +# pragma clang diagnostic ignored "-Wfloat-equal" +# pragma clang diagnostic ignored "-Wsign-compare" +# pragma clang diagnostic ignored "-Woverflow" + +# if (__clang_major__ >= 10 && !defined(__APPLE__)) || __clang_major__ >= 13 +# pragma clang diagnostic ignored "-Wdeprecated-copy" +# endif + +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wold-style-cast" +# pragma GCC diagnostic ignored "-Wundef" +# pragma GCC diagnostic ignored "-Wconversion" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wfloat-equal" +# pragma GCC diagnostic ignored "-Woverflow" + +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4389) +# pragma warning(disable : 4127) +# pragma warning(disable : 4305) +# pragma warning(disable : 4309) +#endif + +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_COMPARE +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_CONVERSION + +#include + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include +#include +#include +#include +#include + +#endif + +using namespace boost::safe_numbers; + +inline std::mt19937_64 rng{42}; +inline constexpr std::size_t N {1024}; + +template +void test() +{ + using basis_type = detail::underlying_type_t; + + boost::random::uniform_int_distribution distribution(std::numeric_limits::min(), + std::numeric_limits::max()); + + for (std::size_t i {0}; i < N; ++i) + { + const auto lhs {distribution(rng)}; + const auto rhs {distribution(rng)}; + + const T lib_lhs {lhs}; + const T lib_rhs {rhs}; + + BOOST_TEST((lhs > rhs) == (lib_lhs > lib_rhs)); + BOOST_TEST((lhs >= rhs) == (lib_lhs >= lib_rhs)); + BOOST_TEST((lhs == rhs) == (lib_lhs == lib_rhs)); + BOOST_TEST((lhs != rhs) == (lib_lhs != lib_rhs)); + BOOST_TEST((lhs <= rhs) == (lib_lhs <= lib_rhs)); + BOOST_TEST((lhs < rhs) == (lib_lhs < lib_rhs)); + BOOST_TEST((lhs <=> rhs) == (lib_lhs <=> lib_rhs)); + } +} + +int main() +{ + test(); + test(); + test(); + test(); + test(); + + return boost::report_errors(); +} diff --git a/test/test_signed_construction.cpp b/test/test_signed_construction.cpp new file mode 100644 index 0000000..e399f50 --- /dev/null +++ b/test/test_signed_construction.cpp @@ -0,0 +1,105 @@ +// Copyright 2025 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wold-style-cast" +# pragma clang diagnostic ignored "-Wundef" +# pragma clang diagnostic ignored "-Wconversion" +# pragma clang diagnostic ignored "-Wsign-conversion" +# pragma clang diagnostic ignored "-Wfloat-equal" +# pragma clang diagnostic ignored "-Wsign-compare" +# pragma clang diagnostic ignored "-Woverflow" + +# if (__clang_major__ >= 10 && !defined(__APPLE__)) || __clang_major__ >= 13 +# pragma clang diagnostic ignored "-Wdeprecated-copy" +# endif + +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wold-style-cast" +# pragma GCC diagnostic ignored "-Wundef" +# pragma GCC diagnostic ignored "-Wconversion" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wsign-compare" +# pragma GCC diagnostic ignored "-Wfloat-equal" +# pragma GCC diagnostic ignored "-Woverflow" + +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4389) +# pragma warning(disable : 4127) +# pragma warning(disable : 4305) +# pragma warning(disable : 4309) +#endif + +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_COMPARE +#define BOOST_SAFE_NUMBERS_DETAIL_INT128_ALLOW_SIGN_CONVERSION + +#include + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#elif defined(_MSC_VER) +# pragma warning(pop) +#endif + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include +#include +#include +#include + +#endif + +inline std::mt19937_64 rng{42}; +inline constexpr std::size_t N {1024}; + +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wclass-memaccess" +#endif + +template +void test() +{ + using BasisType = typename T::basis_type; + + boost::random::uniform_int_distribution dist {std::numeric_limits::min(), + std::numeric_limits::max()}; + + for (std::size_t i {}; i < N; ++i) + { + const auto basis_value {dist(rng)}; + const T value {basis_value}; + + BasisType bits; + std::memcpy(&bits, &value, sizeof(T)); + + BOOST_TEST_EQ(basis_value, bits); + } +} + +int main() +{ + using namespace boost::safe_numbers; + + test(); + test(); + test(); + test(); + test(); + + return boost::report_errors(); +} \ No newline at end of file diff --git a/test/test_signed_conversions.cpp b/test/test_signed_conversions.cpp new file mode 100644 index 0000000..de7fa46 --- /dev/null +++ b/test/test_signed_conversions.cpp @@ -0,0 +1,492 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// Ignore [[nodiscard]] on the tests that we know are going to throw +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-result" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-result" +#elif defined(_MSC_VER) +# pragma warning (push) +# pragma warning (disable: 4834) +#endif + +#include + +#include +#include +#include + +using namespace boost::safe_numbers; + +// The conversion operator OtherBasis() converts to raw signed integral types +// (std::int8_t, std::int16_t, etc.), not to other library types. + +// ----------------------------------------------- +// Widening conversions (should always succeed) +// ----------------------------------------------- + +void test_widening_i8_to_larger() +{ + const i8 pos {100}; + BOOST_TEST_EQ(static_cast(pos), static_cast(100)); + BOOST_TEST_EQ(static_cast(pos), static_cast(100)); + BOOST_TEST_EQ(static_cast(pos), static_cast(100)); + + const i8 neg {-100}; + BOOST_TEST_EQ(static_cast(neg), static_cast(-100)); + BOOST_TEST_EQ(static_cast(neg), static_cast(-100)); + BOOST_TEST_EQ(static_cast(neg), static_cast(-100)); + + const i8 zero {0}; + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); + + const i8 max8 {127}; + BOOST_TEST_EQ(static_cast(max8), static_cast(127)); + BOOST_TEST_EQ(static_cast(max8), static_cast(127)); + BOOST_TEST_EQ(static_cast(max8), static_cast(127)); + + const i8 min8 {-128}; + BOOST_TEST_EQ(static_cast(min8), static_cast(-128)); + BOOST_TEST_EQ(static_cast(min8), static_cast(-128)); + BOOST_TEST_EQ(static_cast(min8), static_cast(-128)); +} + +void test_widening_i16_to_larger() +{ + const i16 a {32000}; + BOOST_TEST_EQ(static_cast(a), static_cast(32000)); + BOOST_TEST_EQ(static_cast(a), static_cast(32000)); + + const i16 neg {-32000}; + BOOST_TEST_EQ(static_cast(neg), static_cast(-32000)); + BOOST_TEST_EQ(static_cast(neg), static_cast(-32000)); + + const i16 zero {0}; + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); +} + +void test_widening_i32_to_i64() +{ + const i32 a {2'000'000'000}; + BOOST_TEST_EQ(static_cast(a), static_cast(2'000'000'000)); + + const i32 neg {-2'000'000'000}; + BOOST_TEST_EQ(static_cast(neg), static_cast(-2'000'000'000)); + + const i32 zero {0}; + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); +} + +// ----------------------------------------------- +// Same-width conversions (identity, always succeed) +// ----------------------------------------------- + +void test_same_width() +{ + const i8 a {42}; + BOOST_TEST_EQ(static_cast(a), static_cast(42)); + + const i8 a_neg {-42}; + BOOST_TEST_EQ(static_cast(a_neg), static_cast(-42)); + + const i16 b {1000}; + BOOST_TEST_EQ(static_cast(b), static_cast(1000)); + + const i16 b_neg {-1000}; + BOOST_TEST_EQ(static_cast(b_neg), static_cast(-1000)); + + const i32 c {100000}; + BOOST_TEST_EQ(static_cast(c), static_cast(100000)); + + const i32 c_neg {-100000}; + BOOST_TEST_EQ(static_cast(c_neg), static_cast(-100000)); + + const i64 d {1'000'000'000LL}; + BOOST_TEST_EQ(static_cast(d), static_cast(1'000'000'000LL)); + + const i64 d_neg {-1'000'000'000LL}; + BOOST_TEST_EQ(static_cast(d_neg), static_cast(-1'000'000'000LL)); +} + +// ----------------------------------------------- +// Narrowing conversions that fit (should succeed) +// ----------------------------------------------- + +void test_narrowing_i16_to_i8_fits() +{ + const i16 a {0}; + BOOST_TEST_EQ(static_cast(a), static_cast(0)); + + const i16 b {127}; + BOOST_TEST_EQ(static_cast(b), static_cast(127)); + + const i16 c {-128}; + BOOST_TEST_EQ(static_cast(c), static_cast(-128)); + + const i16 d {42}; + BOOST_TEST_EQ(static_cast(d), static_cast(42)); + + const i16 e {-42}; + BOOST_TEST_EQ(static_cast(e), static_cast(-42)); +} + +void test_narrowing_i32_to_i8_fits() +{ + const i32 a {0}; + BOOST_TEST_EQ(static_cast(a), static_cast(0)); + + const i32 b {127}; + BOOST_TEST_EQ(static_cast(b), static_cast(127)); + + const i32 c {-128}; + BOOST_TEST_EQ(static_cast(c), static_cast(-128)); +} + +void test_narrowing_i32_to_i16_fits() +{ + const i32 a {0}; + BOOST_TEST_EQ(static_cast(a), static_cast(0)); + + const i32 b {32767}; + BOOST_TEST_EQ(static_cast(b), static_cast(32767)); + + const i32 c {-32768}; + BOOST_TEST_EQ(static_cast(c), static_cast(-32768)); +} + +void test_narrowing_i64_to_i8_fits() +{ + const i64 a {0}; + BOOST_TEST_EQ(static_cast(a), static_cast(0)); + + const i64 b {127}; + BOOST_TEST_EQ(static_cast(b), static_cast(127)); + + const i64 c {-128}; + BOOST_TEST_EQ(static_cast(c), static_cast(-128)); +} + +void test_narrowing_i64_to_i16_fits() +{ + const i64 a {32767}; + BOOST_TEST_EQ(static_cast(a), static_cast(32767)); + + const i64 b {-32768}; + BOOST_TEST_EQ(static_cast(b), static_cast(-32768)); +} + +void test_narrowing_i64_to_i32_fits() +{ + const i64 a {2'147'483'647LL}; + BOOST_TEST_EQ(static_cast(a), static_cast(2'147'483'647)); + + const i64 b {-2'147'483'648LL}; + BOOST_TEST_EQ(static_cast(b), static_cast(-2'147'483'648LL)); + + const i64 c {0}; + BOOST_TEST_EQ(static_cast(c), static_cast(0)); +} + +void test_narrowing_i128_to_smaller_fits() +{ + using int128_t = boost::int128::int128_t; + + const i128 a {int128_t{0, 127}}; + BOOST_TEST_EQ(static_cast(a), static_cast(127)); + + const i128 b {int128_t{0, 32767}}; + BOOST_TEST_EQ(static_cast(b), static_cast(32767)); + + const i128 c {int128_t{0, 2'147'483'647}}; + BOOST_TEST_EQ(static_cast(c), static_cast(2'147'483'647)); + + const i128 d {int128_t{0, static_cast(INT64_MAX)}}; + BOOST_TEST_EQ(static_cast(d), INT64_MAX); + + const i128 zero {int128_t{0, 0}}; + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); + BOOST_TEST_EQ(static_cast(zero), static_cast(0)); +} + +// ----------------------------------------------- +// Narrowing conversions - positive overflow (should throw) +// ----------------------------------------------- + +void test_narrowing_i16_to_i8_positive_overflow() +{ + // 128 > 127 (i8 max) + const i16 a {128}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + // Max i16 value + const i16 b {32767}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); + + // Just above i8 max + const i16 c {200}; + BOOST_TEST_THROWS((static_cast(c)), std::domain_error); +} + +void test_narrowing_i32_to_i8_positive_overflow() +{ + const i32 a {128}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const i32 b {100000}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_i32_to_i16_positive_overflow() +{ + // 32768 > 32767 (i16 max) + const i32 a {32768}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const i32 b {2'147'483'647}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_i64_to_i8_positive_overflow() +{ + const i64 a {128}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const i64 b {1'000'000}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_i64_to_i16_positive_overflow() +{ + const i64 a {32768}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const i64 b {INT64_MAX}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_i64_to_i32_positive_overflow() +{ + // 2147483648 > INT32_MAX + const i64 a {2'147'483'648LL}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const i64 b {INT64_MAX}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_i128_to_smaller_positive_overflow() +{ + using int128_t = boost::int128::int128_t; + + // Large i128 value that overflows all smaller types + const i128 big {int128_t{1, 0}}; + BOOST_TEST_THROWS((static_cast(big)), std::domain_error); + BOOST_TEST_THROWS((static_cast(big)), std::domain_error); + BOOST_TEST_THROWS((static_cast(big)), std::domain_error); + BOOST_TEST_THROWS((static_cast(big)), std::domain_error); + + // Just above i8 max in i128 + const i128 over_i8 {int128_t{0, 128}}; + BOOST_TEST_THROWS((static_cast(over_i8)), std::domain_error); + + // Just above i16 max in i128 + const i128 over_i16 {int128_t{0, 32768}}; + BOOST_TEST_THROWS((static_cast(over_i16)), std::domain_error); + + // Just above i32 max in i128 + const i128 over_i32 {int128_t{0, 2'147'483'648ULL}}; + BOOST_TEST_THROWS((static_cast(over_i32)), std::domain_error); + + // Just above i64 max in i128 + const i128 over_i64 {int128_t{0, static_cast(INT64_MAX) + 1}}; + BOOST_TEST_THROWS((static_cast(over_i64)), std::domain_error); +} + +// ----------------------------------------------- +// Narrowing conversions - negative overflow (should throw) +// ----------------------------------------------- + +void test_narrowing_i16_to_i8_negative_overflow() +{ + // -129 < -128 (i8 min) + const i16 a {-129}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + // Min i16 value + const i16 b {-32768}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); + + // Well below i8 min + const i16 c {-200}; + BOOST_TEST_THROWS((static_cast(c)), std::domain_error); +} + +void test_narrowing_i32_to_i8_negative_overflow() +{ + const i32 a {-129}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const i32 b {-100000}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_i32_to_i16_negative_overflow() +{ + // -32769 < -32768 (i16 min) + const i32 a {-32769}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const i32 b {-2'147'483'647 - 1}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_i64_to_i8_negative_overflow() +{ + const i64 a {-129}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const i64 b {-1'000'000}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_i64_to_i16_negative_overflow() +{ + const i64 a {-32769}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const i64 b {INT64_MIN}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_i64_to_i32_negative_overflow() +{ + // -2147483649 < INT32_MIN + const i64 a {-2'147'483'649LL}; + BOOST_TEST_THROWS((static_cast(a)), std::domain_error); + + const i64 b {INT64_MIN}; + BOOST_TEST_THROWS((static_cast(b)), std::domain_error); +} + +void test_narrowing_i128_to_smaller_negative_overflow() +{ + using int128_t = boost::int128::int128_t; + + // Large negative i128 value + const i128 big_neg {int128_t{-1, 0}}; + BOOST_TEST_THROWS((static_cast(big_neg)), std::domain_error); + BOOST_TEST_THROWS((static_cast(big_neg)), std::domain_error); + BOOST_TEST_THROWS((static_cast(big_neg)), std::domain_error); + + // Just below i8 min in i128 + const i128 under_i8 {int128_t{-1, static_cast(-129)}}; + BOOST_TEST_THROWS((static_cast(under_i8)), std::domain_error); + + // Just below i16 min in i128 + const i128 under_i16 {int128_t{-1, static_cast(-32769)}}; + BOOST_TEST_THROWS((static_cast(under_i16)), std::domain_error); + + // Just below i32 min in i128 + const i128 under_i32 {int128_t{-1, static_cast(-2'147'483'649LL)}}; + BOOST_TEST_THROWS((static_cast(under_i32)), std::domain_error); +} + +// ----------------------------------------------- +// Boundary values (exact min/max fits, min-1/max+1 throws) +// ----------------------------------------------- + +void test_narrowing_boundaries() +{ + // i16 -> i8: 127 fits, 128 does not; -128 fits, -129 does not + const i16 max_fits {127}; + BOOST_TEST_EQ(static_cast(max_fits), static_cast(127)); + + const i16 max_no_fit {128}; + BOOST_TEST_THROWS((static_cast(max_no_fit)), std::domain_error); + + const i16 min_fits {-128}; + BOOST_TEST_EQ(static_cast(min_fits), static_cast(-128)); + + const i16 min_no_fit {-129}; + BOOST_TEST_THROWS((static_cast(min_no_fit)), std::domain_error); + + // i32 -> i16: 32767 fits, 32768 does not; -32768 fits, -32769 does not + const i32 i16_max_fits {32767}; + BOOST_TEST_EQ(static_cast(i16_max_fits), static_cast(32767)); + + const i32 i16_max_no_fit {32768}; + BOOST_TEST_THROWS((static_cast(i16_max_no_fit)), std::domain_error); + + const i32 i16_min_fits {-32768}; + BOOST_TEST_EQ(static_cast(i16_min_fits), static_cast(-32768)); + + const i32 i16_min_no_fit {-32769}; + BOOST_TEST_THROWS((static_cast(i16_min_no_fit)), std::domain_error); + + // i64 -> i32: INT32_MAX fits, INT32_MAX+1 does not; INT32_MIN fits, INT32_MIN-1 does not + const i64 i32_max_fits {2'147'483'647LL}; + BOOST_TEST_EQ(static_cast(i32_max_fits), static_cast(2'147'483'647)); + + const i64 i32_max_no_fit {2'147'483'648LL}; + BOOST_TEST_THROWS((static_cast(i32_max_no_fit)), std::domain_error); + + const i64 i32_min_fits {-2'147'483'648LL}; + BOOST_TEST_EQ(static_cast(i32_min_fits), static_cast(-2'147'483'648LL)); + + const i64 i32_min_no_fit {-2'147'483'649LL}; + BOOST_TEST_THROWS((static_cast(i32_min_no_fit)), std::domain_error); +} + +int main() +{ + // Widening (always succeed) + test_widening_i8_to_larger(); + test_widening_i16_to_larger(); + test_widening_i32_to_i64(); + + // Same-width + test_same_width(); + + // Narrowing that fits (succeed) + test_narrowing_i16_to_i8_fits(); + test_narrowing_i32_to_i8_fits(); + test_narrowing_i32_to_i16_fits(); + test_narrowing_i64_to_i8_fits(); + test_narrowing_i64_to_i16_fits(); + test_narrowing_i64_to_i32_fits(); + test_narrowing_i128_to_smaller_fits(); + + // Narrowing - positive overflow (throw) + test_narrowing_i16_to_i8_positive_overflow(); + test_narrowing_i32_to_i8_positive_overflow(); + test_narrowing_i32_to_i16_positive_overflow(); + test_narrowing_i64_to_i8_positive_overflow(); + test_narrowing_i64_to_i16_positive_overflow(); + test_narrowing_i64_to_i32_positive_overflow(); + test_narrowing_i128_to_smaller_positive_overflow(); + + // Narrowing - negative overflow (throw) + test_narrowing_i16_to_i8_negative_overflow(); + test_narrowing_i32_to_i8_negative_overflow(); + test_narrowing_i32_to_i16_negative_overflow(); + test_narrowing_i64_to_i8_negative_overflow(); + test_narrowing_i64_to_i16_negative_overflow(); + test_narrowing_i64_to_i32_negative_overflow(); + test_narrowing_i128_to_smaller_negative_overflow(); + + // Boundary values + test_narrowing_boundaries(); + + return boost::report_errors(); +} diff --git a/test/test_signed_unary_operators.cpp b/test/test_signed_unary_operators.cpp new file mode 100644 index 0000000..84ec934 --- /dev/null +++ b/test/test_signed_unary_operators.cpp @@ -0,0 +1,179 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +// Ignore [[nodiscard]] on the tests that we know are going to throw +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-result" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-result" +#elif defined(_MSC_VER) +# pragma warning (push) +# pragma warning (disable: 4834) +#endif + +#include + +#include +#include +#include + +using namespace boost::safe_numbers; + +// ----------------------------------------------- +// Unary plus: +x == x for all values +// ----------------------------------------------- + +template +void test_unary_plus() +{ + using basis = typename T::basis_type; + + const T zero {basis{0}}; + BOOST_TEST(+zero == zero); + + const T one {basis{1}}; + BOOST_TEST(+one == one); + + const T neg_one {basis{-1}}; + BOOST_TEST(+neg_one == neg_one); + + const T max_val {std::numeric_limits::max()}; + BOOST_TEST(+max_val == max_val); + + const T min_val {std::numeric_limits::min()}; + BOOST_TEST(+min_val == min_val); + + const T pos {basis{42}}; + BOOST_TEST(+pos == pos); + + const T neg {basis{-42}}; + BOOST_TEST(+neg == neg); +} + +// ----------------------------------------------- +// Unary minus: basic negation +// ----------------------------------------------- + +template +void test_unary_minus_basic() +{ + using basis = typename T::basis_type; + + // -0 == 0 + const T zero {basis{0}}; + BOOST_TEST(-zero == zero); + + // -1 == T{-1} + const T one {basis{1}}; + const T neg_one {basis{-1}}; + BOOST_TEST(-one == neg_one); + BOOST_TEST(-neg_one == one); + + // -42 == T{-42} + const T pos {basis{42}}; + const T neg {basis{-42}}; + BOOST_TEST(-pos == neg); + BOOST_TEST(-neg == pos); + + // Double negation: -(-x) == x + const T val {basis{100}}; + BOOST_TEST(-(-val) == val); + + const T neg_val {basis{-100}}; + BOOST_TEST(-(-neg_val) == neg_val); +} + +// ----------------------------------------------- +// Unary minus: max value can be negated safely +// ----------------------------------------------- + +template +void test_unary_minus_max() +{ + using basis = typename T::basis_type; + + // Negating max should succeed: e.g., -127 is valid for int8_t + const T max_val {std::numeric_limits::max()}; + const T expected {static_cast(-std::numeric_limits::max())}; + BOOST_TEST(-max_val == expected); +} + +// ----------------------------------------------- +// Unary minus: min value cannot be negated (UB / overflow) +// Negating INT_MIN is undefined behavior because the result +// does not fit in the signed type. +// ----------------------------------------------- + +template +void test_unary_minus_min_throws() +{ + using basis = typename T::basis_type; + + // Negating min must throw: e.g., -(-128) = 128, which overflows int8_t + const T min_val {std::numeric_limits::min()}; + BOOST_TEST_THROWS((-min_val), std::domain_error); +} + +// ----------------------------------------------- +// Unary minus: values near boundaries +// ----------------------------------------------- + +template +void test_unary_minus_near_boundaries() +{ + using basis = typename T::basis_type; + + // min + 1 can be negated safely + const T near_min {static_cast(std::numeric_limits::min() + 1)}; + const T expected {static_cast(std::numeric_limits::max())}; + BOOST_TEST(-near_min == expected); + + // max can be negated safely (gives min + 1) + const T max_val {std::numeric_limits::max()}; + BOOST_TEST(-max_val == near_min); +} + +int main() +{ + // Unary plus + test_unary_plus(); + test_unary_plus(); + test_unary_plus(); + test_unary_plus(); + test_unary_plus(); + + // Unary minus basic + test_unary_minus_basic(); + test_unary_minus_basic(); + test_unary_minus_basic(); + test_unary_minus_basic(); + test_unary_minus_basic(); + + // Unary minus max (should succeed) + test_unary_minus_max(); + test_unary_minus_max(); + test_unary_minus_max(); + test_unary_minus_max(); + test_unary_minus_max(); + + // Unary minus min (should throw) + test_unary_minus_min_throws(); + test_unary_minus_min_throws(); + test_unary_minus_min_throws(); + test_unary_minus_min_throws(); + test_unary_minus_min_throws(); + + // Near boundary values + test_unary_minus_near_boundaries(); + test_unary_minus_near_boundaries(); + test_unary_minus_near_boundaries(); + test_unary_minus_near_boundaries(); + test_unary_minus_near_boundaries(); + + return boost::report_errors(); +}