From a771f57962d5a9cb1a48b572637b608c6aec24a9 Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Mon, 18 Dec 2023 16:17:57 -0500 Subject: [PATCH] Add overflow helpers for applying rational to int (#212) C++'s integer promotion rules for small types make this specific use case --- applying a rational magnitude to an integral type --- really interesting! The product type can be bigger than the type of the two inputs, so the overflow might not happen when you think it should. _Moreover,_ the fact that we subsequently divide by a denominator means that we can _re-enter_ the valid range and still produce a correct result! The only way I know how to deal with something this complicated is to make a separate target that does only that one thing. Happily, it's still the case that there is some maximum and minimum value for each type that will not overflow for a particular rational magnitude. The new helper target exists to figure out what those limits are. I exposed this limitation by adding a conversion between meters and yards in `uint16_t`. This is a great test case because the unit ratio between meters and yards is very close to 1 (it's 1143 to 1250), meaning the denominator division "rescues" the great majority of cases. Moreover, the multiplicative factor (either 1143 or 1250 depending on direction) is far less than the ratio between `uint16_t` and its promoted type, assuming the latter is equivalent to `int32_t`. So, despite the fact that we're multiplying by a number which is pretty large relative to the type, we almost never actually overflow! I fixed up the test case so that if it _does_ fail (say, we add some new unit conversion that exposes a new logic error), it will be very easy to understand why. --- au/BUILD.bazel | 22 +- au/apply_magnitude.hh | 31 +- au/apply_magnitude_test.cc | 42 +- au/apply_rational_magnitude_to_integral.hh | 251 ++++++++++++ ...ply_rational_magnitude_to_integral_test.cc | 376 ++++++++++++++++++ au/quantity_test.cc | 38 +- 6 files changed, 743 insertions(+), 17 deletions(-) create mode 100644 au/apply_rational_magnitude_to_integral.hh create mode 100644 au/apply_rational_magnitude_to_integral_test.cc diff --git a/au/BUILD.bazel b/au/BUILD.bazel index ce69e047..7e945707 100644 --- a/au/BUILD.bazel +++ b/au/BUILD.bazel @@ -113,7 +113,10 @@ cc_test( cc_library( name = "apply_magnitude", hdrs = ["apply_magnitude.hh"], - deps = [":magnitude"], + deps = [ + ":apply_rational_magnitude_to_integral", + ":magnitude", + ], ) cc_test( @@ -128,6 +131,23 @@ cc_test( ], ) +cc_library( + name = "apply_rational_magnitude_to_integral", + hdrs = ["apply_rational_magnitude_to_integral.hh"], + deps = [":magnitude"], +) + +cc_test( + name = "apply_rational_magnitude_to_integral_test", + size = "small", + srcs = ["apply_rational_magnitude_to_integral_test.cc"], + deps = [ + ":apply_rational_magnitude_to_integral", + ":testing", + "@com_google_googletest//:gtest_main", + ], +) + cc_library( name = "chrono_interop", hdrs = ["chrono_interop.hh"], diff --git a/au/apply_magnitude.hh b/au/apply_magnitude.hh index 515a3da5..e0c580e9 100644 --- a/au/apply_magnitude.hh +++ b/au/apply_magnitude.hh @@ -14,6 +14,7 @@ #pragma once +#include "au/apply_rational_magnitude_to_integral.hh" #include "au/magnitude.hh" namespace au { @@ -132,6 +133,28 @@ struct ApplyMagnitudeImpl { } }; +template +struct RationalOverflowChecker; +template +struct RationalOverflowChecker { + static constexpr bool would_overflow(const T &x) { + static_assert(std::is_signed::value, + "Mismatched instantiation (should never be done manually)"); + const bool safe = (x <= MaxNonOverflowingValue::value()) && + (x >= MinNonOverflowingValue::value()); + return !safe; + } +}; +template +struct RationalOverflowChecker { + static constexpr bool would_overflow(const T &x) { + static_assert(!std::is_signed::value, + "Mismatched instantiation (should never be done manually)"); + const bool safe = (x <= MaxNonOverflowingValue::value()); + return !safe; + } +}; + // Applying a (non-integer, non-inverse-integer) rational, for any integral type T. template struct ApplyMagnitudeImpl { @@ -141,13 +164,13 @@ struct ApplyMagnitudeImpl { "Mismatched instantiation (should never be done manually)"); constexpr T operator()(const T &x) { - return x * get_value(numerator(Mag{})) / get_value(denominator(Mag{})); + using P = PromotedType; + return static_cast(x * get_value

(numerator(Mag{})) / + get_value

(denominator(Mag{}))); } static constexpr bool would_overflow(const T &x) { - constexpr auto mag_value_result = get_value_result(numerator(Mag{})); - return OverflowChecker:: - would_product_overflow(x, mag_value_result.value); + return RationalOverflowChecker::value>::would_overflow(x); } static constexpr bool would_truncate(const T &x) { diff --git a/au/apply_magnitude_test.cc b/au/apply_magnitude_test.cc index 0bfbae15..1f6fad24 100644 --- a/au/apply_magnitude_test.cc +++ b/au/apply_magnitude_test.cc @@ -90,6 +90,28 @@ TEST(ApplyMagnitude, MultipliesThenDividesForRationalMagnitudeOnInteger) { EXPECT_THAT(apply_magnitude(5, three_halves), SameTypeAndValue(7)); } +TEST(ApplyMagnitude, SupportsNumeratorThatFitsInPromotedTypeButNotOriginalType) { + using T = uint16_t; + using P = PromotedType; + ASSERT_TRUE((std::is_same::value)) + << "This test fails on architectures where `uint16_t` doesn't get promoted to `int32_t`"; + + // Choose a magnitude whose effect will basically be to divide by 2. (We make the denominator + // slightly _smaller_ than twice the numerator, rather than slightly _larger_, so that the + // division will end up on the "high" side of the target, and truncation will bring it down very + // slightly instead of going down a full integer.) + auto roughly_one_half = mag<100'000'000>() / mag<199'999'999>(); + + // The whole point of this test case is to apply a magnitude whose numerator fits in the + // promoted type, but does not fit in the target type itself. + ASSERT_EQ(get_value_result

(numerator(roughly_one_half)).outcome, + MagRepresentationOutcome::OK); + ASSERT_EQ(get_value_result(numerator(roughly_one_half)).outcome, + MagRepresentationOutcome::ERR_CANNOT_FIT); + + EXPECT_THAT(apply_magnitude(T{18}, roughly_one_half), SameTypeAndValue(T{9})); +} + TEST(ApplyMagnitude, MultipliesSingleNumberForRationalMagnitudeOnFloatingPoint) { // Helper similar to `std::transform`, but with more convenient interfaces. auto apply = [](std::vector vals, auto fun) { @@ -196,10 +218,8 @@ TEST(WouldOverflow, AlwaysFalseForIntegerDivide) { } TEST(WouldOverflow, UsesNumeratorWhenApplyingRationalMagnitudeToIntegralType) { - auto TWO_THIRDS = mag<2>() / mag<3>(); - { - using ApplyTwoThirdsToI32 = ApplyMagnitudeT; + using ApplyTwoThirdsToI32 = ApplyMagnitudeT() / mag<3>())>; EXPECT_TRUE(ApplyTwoThirdsToI32::would_overflow(2'147'483'647)); EXPECT_TRUE(ApplyTwoThirdsToI32::would_overflow(1'073'741'824)); @@ -215,14 +235,18 @@ TEST(WouldOverflow, UsesNumeratorWhenApplyingRationalMagnitudeToIntegralType) { } { - using ApplyTwoThirdsToU8 = ApplyMagnitudeT; + using ApplyRoughlyOneThirdToU8 = + ApplyMagnitudeT() / mag<300'000'001>())>; + + ASSERT_TRUE((std::is_same::value)) + << "This test fails on architectures where `uint8_t` doesn't get promoted to `int32_t`"; - EXPECT_TRUE(ApplyTwoThirdsToU8::would_overflow(255)); - EXPECT_TRUE(ApplyTwoThirdsToU8::would_overflow(128)); + EXPECT_TRUE(ApplyRoughlyOneThirdToU8::would_overflow(255)); + EXPECT_TRUE(ApplyRoughlyOneThirdToU8::would_overflow(22)); - EXPECT_FALSE(ApplyTwoThirdsToU8::would_overflow(127)); - EXPECT_FALSE(ApplyTwoThirdsToU8::would_overflow(1)); - EXPECT_FALSE(ApplyTwoThirdsToU8::would_overflow(0)); + EXPECT_FALSE(ApplyRoughlyOneThirdToU8::would_overflow(21)); + EXPECT_FALSE(ApplyRoughlyOneThirdToU8::would_overflow(1)); + EXPECT_FALSE(ApplyRoughlyOneThirdToU8::would_overflow(0)); } } diff --git a/au/apply_rational_magnitude_to_integral.hh b/au/apply_rational_magnitude_to_integral.hh new file mode 100644 index 00000000..111d0beb --- /dev/null +++ b/au/apply_rational_magnitude_to_integral.hh @@ -0,0 +1,251 @@ +// Copyright 2023 Aurora Operations, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "au/magnitude.hh" +#include "au/stdx/utility.hh" + +// This file exists to analyze one single calculation: `x * N / D`, where `x` is +// some integral type, and `N` and `D` are the numerator and denominator of a +// rational magnitude (and hence, are automatically in lowest terms), +// represented in that same type. We want to answer one single question: will +// this calculation overflow at any stage? +// +// Importantly, we need to produce correct answers even when `N` and/or `D` +// _cannot be represented_ in that type (because they would overflow). We also +// need to handle subtleties around integer promotion, where the type of `x * x` +// can be different from the type of `x` when those types are small. +// +// The goal for the final solution we produce is to be as fast and efficient as +// the best such function that an expert C++ engineer could produce by hand, for +// every combination of integral type and numerator and denominator magnitudes. + +namespace au { +namespace detail { + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// `PromotedType` is the result type for arithmetic operations involving `T`. Of course, this is +// normally just `T`, but integer promotion for small integral types can change this. +// +template +struct PromotedTypeImpl { + using type = decltype(std::declval() * std::declval()); + + static_assert(std::is_same::type>::value, + "We explicitly assume that promoted types are not again promotable"); +}; +template +using PromotedType = typename PromotedTypeImpl::type; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// `clamp_to_range_of(x)` returns `x` if it is in the range of `T`, and otherwise returns the +// maximum value representable in `T` if `x` is too large, or the minimum value representable in `T` +// if `x` is too small. +// + +template +constexpr T clamp_to_range_of(U x) { + return stdx::cmp_greater(x, std::numeric_limits::max()) + ? std::numeric_limits::max() + : (stdx::cmp_less(x, std::numeric_limits::lowest()) + ? std::numeric_limits::lowest() + : static_cast(x)); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// `is_known_to_be_less_than_one(MagT)` is true if the magnitude `MagT` is purely rational; its +// numerator is representable in `std::uintmax_t`; and, it is less than 1. +// + +template +constexpr bool is_known_to_be_less_than_one(Magnitude m) { + using MagT = Magnitude; + static_assert(is_rational(m), "Magnitude must be rational"); + + constexpr auto num_result = get_value_result(numerator(MagT{})); + static_assert(num_result.outcome == MagRepresentationOutcome::OK, + "Magnitude must be representable in std::uintmax_t"); + + constexpr auto den_result = get_value_result(denominator(MagT{})); + static_assert( + den_result.outcome == MagRepresentationOutcome::OK || + den_result.outcome == MagRepresentationOutcome::ERR_CANNOT_FIT, + "Magnitude must either be representable in std::uintmax_t, or fail due to overflow"); + + return den_result.outcome == MagRepresentationOutcome::OK ? num_result.value < den_result.value + : true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// `MaxNonOverflowingValue` is the maximum value of type `T` that can have `MagT` applied +// as numerator-and-denominator without overflowing. We require that `T` is some integral +// arithmetic type, and that `MagT` is a rational magnitude that is neither purely integral nor +// purely inverse-integral. +// +// If you are trying to understand these helpers, we suggest starting at the bottom with +// `MaxNonOverflowingValue`, and reading upwards. +// + +// +// Branch based on whether `MagT` is less than 1. +// +template +struct MaxNonOverflowingValueImplWhenNumFits; + +// If `MagT` is less than 1, then we only need to check for the limiting value where the _numerator +// multiplication step alone_ would overflow. +template +struct MaxNonOverflowingValueImplWhenNumFits { + using P = PromotedType; + + static constexpr T value() { + return clamp_to_range_of(std::numeric_limits

::max() / + get_value

(numerator(MagT{}))); + } +}; + +// If `MagT` is greater than 1, then we have two opportunities for overflow: the numerator +// multiplication step can overflow the promoted type; or, the denominator division step can fail to +// restore it to the original type's range. +template +struct MaxNonOverflowingValueImplWhenNumFits { + using P = PromotedType; + + static constexpr T value() { + constexpr auto num = get_value

(numerator(MagT{})); + constexpr auto den = get_value

(denominator(MagT{})); + constexpr auto t_max = std::numeric_limits::max(); + constexpr auto p_max = std::numeric_limits

::max(); + constexpr auto limit_to_avoid = (den > p_max / t_max) ? p_max : t_max * den; + return clamp_to_range_of(limit_to_avoid / num); + } +}; + +// +// Branch based on whether the numerator of `MagT` can fit in the promoted type of `T`. +// +template +struct MaxNonOverflowingValueImpl; + +// If the numerator fits in the promoted type of `T`, delegate further based on whether the +// denominator is bigger. +template +struct MaxNonOverflowingValueImpl + : MaxNonOverflowingValueImplWhenNumFits {}; + +// If `MagT` can't be represented in the promoted type of `T`, then the result is 0. +template +struct MaxNonOverflowingValueImpl { + static constexpr T value() { return T{0}; } +}; + +template +struct ValidateTypeAndMagnitude { + static_assert(std::is_integral::value, "Only designed for integral types"); + static_assert(is_rational(MagT{}), "Magnitude must be rational"); + static_assert(!is_integer(MagT{}), "Magnitude must not be purely integral"); + static_assert(!is_integer(inverse(MagT{})), "Magnitude must not be purely inverse-integral"); +}; + +template +struct MaxNonOverflowingValue + : ValidateTypeAndMagnitude, + MaxNonOverflowingValueImpl>(numerator(MagT{})).outcome> {}; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// +// `MinNonOverflowingValue` is the minimum (i.e., most-negative) value of type `T` that can +// have `MagT` applied as numerator-and-denominator without overflowing (i.e., becoming too-negative +// to represent). We require that `T` is some integral arithmetic type, and that `MagT` is a +// rational magnitude that is neither purely integral nor purely inverse-integral. +// +// If you are trying to understand these helpers, we suggest starting at the bottom with +// `MinNonOverflowingValue`, and reading upwards. +// + +// +// Branch based on whether `MagT` is less than 1. +// +template +struct MinNonOverflowingValueImplWhenNumFits; + +// If `MagT` is less than 1, then we only need to check for the limiting value where the _numerator +// multiplication step alone_ would overflow. +template +struct MinNonOverflowingValueImplWhenNumFits { + using P = PromotedType; + + static constexpr T value() { + return clamp_to_range_of(std::numeric_limits

::lowest() / + get_value

(numerator(MagT{}))); + } +}; + +// If `MagT` is greater than 1, then we have two opportunities for overflow: the numerator +// multiplication step can overflow the promoted type; or, the denominator division step can fail to +// restore it to the original type's range. +template +struct MinNonOverflowingValueImplWhenNumFits { + using P = PromotedType; + + static constexpr T value() { + constexpr auto num = get_value

(numerator(MagT{})); + constexpr auto den = get_value

(denominator(MagT{})); + constexpr auto t_min = std::numeric_limits::lowest(); + constexpr auto p_min = std::numeric_limits

::lowest(); + constexpr auto limit_to_avoid = (den > p_min / t_min) ? p_min : t_min * den; + return clamp_to_range_of(limit_to_avoid / num); + } +}; + +// +// Branch based on whether the denominator of `MagT` can fit in the promoted type of `T`. +// +template +struct MinNonOverflowingValueImpl; + +// If the numerator fits in the promoted type of `T`, delegate further based on whether the +// denominator is bigger. +template +struct MinNonOverflowingValueImpl + : MinNonOverflowingValueImplWhenNumFits {}; + +// If the numerator can't be represented in the promoted type of `T`, then the result is 0. +template +struct MinNonOverflowingValueImpl { + static constexpr T value() { return T{0}; } +}; + +template +struct MinNonOverflowingValue + : ValidateTypeAndMagnitude, + MinNonOverflowingValueImpl>(numerator(MagT{})).outcome> { + static_assert(std::is_signed::value, "Only designed for signed types"); + static_assert(std::is_signed>::value, + "We assume the promoted type is also signed"); +}; + +} // namespace detail +} // namespace au diff --git a/au/apply_rational_magnitude_to_integral_test.cc b/au/apply_rational_magnitude_to_integral_test.cc new file mode 100644 index 00000000..65ef663a --- /dev/null +++ b/au/apply_rational_magnitude_to_integral_test.cc @@ -0,0 +1,376 @@ +// Copyright 2023 Aurora Operations, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "au/apply_rational_magnitude_to_integral.hh" + +#include "au/testing.hh" +#include "gtest/gtest.h" + +using ::testing::AllOf; +using ::testing::Lt; +using ::testing::StaticAssertTypeEq; + +namespace au { +namespace detail { +namespace { + +template +constexpr void ensure_relevant_kind_of_magnitude(Magnitude m) { + static_assert(is_rational(m), "Magnitude must be rational"); + static_assert(!is_integer(m), "Magnitude must not be purely integer"); + static_assert(!is_integer(ONE / m), "Magnitude must not be purely inverse-integer"); +} + +TEST(PromotedType, IdentityForInt) { + // `int` does not undergo type promotion. + StaticAssertTypeEq>(); +} + +TEST(PromotedType, PromotesUint8TIntoLargerType) { + using PromotedU8 = PromotedType; + + // Technically, this need not be true on every conceivable architecture. However, it is true on + // the vast majority that are used in practice. Moreover, the failure mode if it's not is + // simply that a test would fail when run on some obscure architecture, and the failure would + // direct the user to this comment. This doesn't affect the actual library usage one way or + // another. + ASSERT_FALSE((std::is_same::value)); + + EXPECT_GT(sizeof(PromotedU8), sizeof(uint8_t)); +} + +TEST(IsKnownToBeLessThanOne, ProducesExpectedResultsForMagnitudesThatCanFitInUintmax) { + EXPECT_TRUE(is_known_to_be_less_than_one(mag<1>() / mag<2>())); + EXPECT_TRUE(is_known_to_be_less_than_one(mag<999'999>() / mag<1'000'000>())); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Test cases for maximum non-overflowing value. +// +// What are the canonical representative situations for the overflow boundary of `x * N / D`, where +// `x` has type `T`, `TM` is the maximum value of type `T`, and `PM` is the maximum value of type +// `PromotedType`? We list them here. Note that they're listed in a kind of "if/else-if" order: +// each number's condition assumes that all of the previous conditions are false. (So for a test +// case to fall into category "3", it can't be in category "1" or "2".) +// +// 1. `N` cannot fit inside `PromotedType`. This means any nonzero value always overflows: the +// max is 0. +// +// 2. `N < D` (whether or not `D` can fit inside `PromotedType`). This means that the final +// result will be smaller than the input, and therefore will always fit inside `T` --- as long as +// the intermediate calculation `(x * N)` doesn't overflow. So the maximum non-overflowing value +// is `min(PM / N, TM)`. +// +// 3. `N > D` (all other cases). The only step that can overflow is the multiplication with `N`. +// There are two conditions we must meet to avoid overflow. First, we must avoid going over the +// limit of the promoted type. Second, we must ensure that the product is within the limits of +// the original type after dividing by `D`. So the maximum non-overflowing value is the smaller +// of `PM` and `TM * D`, divided by `N`. (We'll have to be careful that the `TM * D` itself +// doesn't overflow! We'll cap it at `PM` if it does.) + +enum class IsPromotable { NO, YES }; +enum class NumFitsInPromotedType { NO, YES }; +enum class DenFitsInPromotedType { NO, YES }; +struct TestSpec { + IsPromotable is_promotable; + NumFitsInPromotedType num_fits; + DenFitsInPromotedType den_fits; +}; + +template +void validate_spec(TestSpec spec) { + using PromotedT = PromotedType; + const bool is_promotable = !std::is_same::value; + const bool is_expected_to_be_promotable = (spec.is_promotable == IsPromotable::YES); + ASSERT_EQ(is_promotable, is_expected_to_be_promotable) + << "Expected a type that " << (is_expected_to_be_promotable ? "is" : "is not") + << " promotable; got a type that " << (is_promotable ? "is" : "is not"); + + using Num = decltype(numerator(MagT{})); + constexpr auto num_value_result = get_value_result(Num{}); + const bool is_num_representable = (num_value_result.outcome == MagRepresentationOutcome::OK); + const bool is_num_expected_to_be_representable = (spec.num_fits == NumFitsInPromotedType::YES); + ASSERT_EQ(is_num_representable, is_num_expected_to_be_representable) + << "Expected numerator " << (is_num_expected_to_be_representable ? "to be" : "not to be") + << " representable in promoted type; it " << (is_num_representable ? "is" : "is not"); + + using Den = decltype(denominator(MagT{})); + constexpr auto den_value_result = get_value_result(Den{}); + const bool is_den_representable = (den_value_result.outcome == MagRepresentationOutcome::OK); + const bool is_den_expected_to_be_representable = (spec.den_fits == DenFitsInPromotedType::YES); + ASSERT_EQ(is_den_representable, is_den_expected_to_be_representable) + << "Expected denominator " << (is_den_expected_to_be_representable ? "to be" : "not to be") + << " representable in promoted type; it " << (is_den_representable ? "is" : "is not"); +} + +template +void populate_max_non_overflowing_value(TestSpec spec, MagT, T &result_out) { + validate_spec(spec); + result_out = MaxNonOverflowingValue::value(); +} + +TEST(MaxNonOverflowingValue, AlwaysZeroIfNumCannotFitInPromotedType) { + // Case "1" above. + constexpr auto huge = pow<400>(mag<10>()) / mag<3>(); + + { + int max_int = 123; + populate_max_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::NO, DenFitsInPromotedType::YES}, + huge, + max_int); + EXPECT_EQ(max_int, 0); + } + { + uint16_t max_u16 = 123u; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::NO, DenFitsInPromotedType::YES}, + huge, + max_u16); + EXPECT_EQ(max_u16, 0); + } +} + +TEST(MaxNonOverflowingValue, IsMaxTDividedByNWhenTIsNotPromotableAndDenomOverflows) { + // Case "2" above. a) Overflowing denominator, non-promotable types only. + constexpr auto huge_denom = mag<3>() / pow<400>(mag<10>()); + + { + int max_int = 0; + populate_max_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + huge_denom, + max_int); + EXPECT_EQ(max_int, std::numeric_limits::max() / 3); + } + + { + uint64_t max_u64 = 0; + populate_max_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + huge_denom, + max_u64); + EXPECT_EQ(max_u64, std::numeric_limits::max() / 3); + } +} + +TEST(MaxNonOverflowingValue, + IsSmallerOfMaxPromotedTDividedByNAndMaxTWhenTIsPromotableAndDenomOverflows) { + // Case "2" above. b) Overflowing denominator, promotable types. + { + int8_t max_i8 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + mag<3>() / pow<400>(mag<10>()), + max_i8); + EXPECT_EQ(max_i8, 127); + } + + { + uint16_t max_u16 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + mag<1'000'000>() / pow<400>(mag<11>()), + max_u16); + + ASSERT_TRUE((std::is_same, int32_t>::value)) + << "This test will fail on architectures where uint16_t is not promoted to `int32_t`"; + EXPECT_EQ(max_u16, 2'147); + } +} + +TEST(MaxNonOverflowingValue, + IsSmallerOfMaxPromotedTDividedByNAndMaxTWhenTIsPromotableAndNLessThanD) { + // Case "2" above. c) Non-overflowing denominator. + + { + uint8_t max_u8 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<3>() / mag<10>(), + max_u8); + EXPECT_EQ(max_u8, 255); + } + + { + uint16_t max_u16 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000'000>() / pow<6>(mag<11>()), + max_u16); + EXPECT_EQ(max_u16, 2'147); + } +} + +TEST(MaxNonOverflowingValue, IsPromotedMaxOverNWhenNIsLargeAndDIsSlightlySmaller) { + // Case 3 above (partial). + // + // When `N / D` is very slightly larger than 1, then (PM / N) can be the most constraining. + // We'll use a promoted type just to make this interesting by making PM different from TM. + uint16_t max_u16 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000'000>() / mag<999'999>(), + max_u16); + ASSERT_TRUE((std::is_same, int32_t>::value)) + << "This test will fail on architectures where `uint16_t` is not promoted to `int32_t`"; + EXPECT_EQ(max_u16, 2'147); +} + +TEST(MaxNonOverflowingValue, IsTMaxOverNTimesDWhenMoreConstrainingThanPMaxOverN) { + // Case 3 above (partial). + // + // The goal is to engineer a test case where `(TM * D) / N` is more constraining than `PM / N`. + // Obviously, if `TM = PM`, then this can never be; therefore, we need to use a promotable type. + int16_t max_int16 = 0; + populate_max_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000>() / mag<3>(), + max_int16); + ASSERT_TRUE((std::is_same, int32_t>::value)) + << "This test will fail on architectures where `int16_t` is not promoted to `int32_t`"; + ASSERT_EQ(98, 32'768 * 3 / 1'000); + EXPECT_EQ(max_int16, 98); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Test cases for minimum (i.e. most-negative) non-overflowing value. +// +// The test cases are the mirror image of the above, except that we only check signed types. + +template +void populate_min_non_overflowing_value(TestSpec spec, MagT, T &result_out) { + validate_spec(spec); + result_out = MinNonOverflowingValue::value(); +} + +TEST(MinNonOverflowingValue, AlwaysZeroIfNumCannotFitInPromotedType) { + constexpr auto huge = pow<400>(mag<10>()) / mag<3>(); + + { + int min_int = 123; + populate_min_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::NO, DenFitsInPromotedType::YES}, + huge, + min_int); + EXPECT_EQ(min_int, 0); + } + + { + int16_t min_i16 = 123; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::NO, DenFitsInPromotedType::YES}, + huge, + min_i16); + EXPECT_EQ(min_i16, 0); + } +} + +TEST(MinNonOverflowingValue, IsMinTDividedByNWhenTIsNotPromotableAndDenomOverflows) { + constexpr auto huge_denom = mag<3>() / pow<400>(mag<10>()); + + { + int min_int = 0; + populate_min_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + huge_denom, + min_int); + EXPECT_EQ(min_int, std::numeric_limits::lowest() / 3); + } + + { + int64_t min_i64 = 0; + populate_min_non_overflowing_value( + {IsPromotable::NO, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + huge_denom, + min_i64); + EXPECT_EQ(min_i64, std::numeric_limits::lowest() / 3); + } +} + +TEST(MinNonOverflowingValue, + IsSmallerOfMinPromotedTDividedByNAndMinTWhenTIsPromotableAndDenomOverflows) { + { + int8_t min_i8 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + mag<3>() / pow<400>(mag<10>()), + min_i8); + EXPECT_EQ(min_i8, -128); + } + + { + int16_t min_i16 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::NO}, + mag<1'000'000>() / pow<400>(mag<11>()), + min_i16); + + ASSERT_TRUE((std::is_same, int32_t>::value)) + << "This test will fail on architectures where `int16_t` is not promoted to `int32_t`"; + EXPECT_EQ(min_i16, -2'147); + } +} + +TEST(MinNonOverflowingValue, + IsSmallerOfMinPromotedTDividedByNAndMinTWhenTIsPromotableAndNLessThanD) { + + { + int8_t min_i8 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<3>() / mag<10>(), + min_i8); + EXPECT_EQ(min_i8, -128); + } + + { + int16_t min_i16 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000'000>() / pow<6>(mag<11>()), + min_i16); + EXPECT_EQ(min_i16, -2'147); + } +} + +TEST(MinNonOverflowingValue, IsPromotedMinOverNWhenNIsLargeAndDIsSlightlySmaller) { + // When `N / D` is very slightly larger than 1, then (PM / N) can be the most constraining. + // We'll use a promoted type just to make this interesting by making PM different from TM. + int16_t min_i16 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000'000>() / mag<999'999>(), + min_i16); + + ASSERT_TRUE((std::is_same, int32_t>::value)) + << "This test will fail on architectures where `int16_t` is not promoted to `int32_t`"; + EXPECT_EQ(min_i16, -2'147); +} + +TEST(MinNonOverflowingValue, IsTMinOverNTimesDWhenMoreConstrainingThanPMinOverN) { + // The goal is to engineer a test case where `(TM * D) / N` is more constraining than `PM / N`. + // Obviously, if `TM = PM`, then this can never be; therefore, we need to use a promotable type. + int16_t min_int16 = 0; + populate_min_non_overflowing_value( + {IsPromotable::YES, NumFitsInPromotedType::YES, DenFitsInPromotedType::YES}, + mag<1'000>() / mag<3>(), + min_int16); + ASSERT_EQ(-98, -32'768 * 3 / 1'000); + EXPECT_EQ(min_int16, -98); +} + +} // namespace +} // namespace detail +} // namespace au diff --git a/au/quantity_test.cc b/au/quantity_test.cc index feebdf94..553ce3eb 100644 --- a/au/quantity_test.cc +++ b/au/quantity_test.cc @@ -33,10 +33,16 @@ constexpr auto miles = QuantityMaker{}; struct Inches : decltype(Feet{} / mag<12>()) {}; constexpr auto inches = QuantityMaker{}; -struct Yards : decltype(Feet{} * mag<3>()) {}; +struct Yards : decltype(Feet{} * mag<3>()) { + static constexpr const char label[] = "yd"; +}; +constexpr const char Yards::label[]; constexpr auto yards = QuantityMaker{}; -struct Meters : decltype(Inches{} * mag<100>() / mag<254>() * mag<100>()) {}; +struct Meters : decltype(Inches{} * mag<100>() / mag<254>() * mag<100>()) { + static constexpr const char label[] = "m"; +}; +constexpr const char Meters::label[]; static constexpr QuantityMaker meters{}; static_assert(are_units_quantity_equivalent(Centi{} * mag<254>(), Inches{} * mag<100>()), "Double-check this ad hoc definition of meters"); @@ -750,7 +756,29 @@ TEST(IsConversionLossy, CorrectlyDiscriminatesBetweenLossyAndLosslessConversions ASSERT_FALSE(is_inverse_lossy); } - EXPECT_EQ(is_lossy, did_value_change); + std::string reason{}; + if (is_lossy) { + const bool truncates = will_conversion_truncate(original, target_units); + const bool overflows = will_conversion_overflow(original, target_units); + ASSERT_TRUE(truncates || overflows); + reason = std::string{" ("} + [&] { + if (truncates && overflows) { + return "truncates and overflows"; + } else if (truncates) { + return "truncates"; + } else if (overflows) { + return "overflows"; + } else { + return ""; + } + }() + ")"; + } + + EXPECT_EQ(is_lossy, did_value_change) + << "Conversion " << (is_lossy ? "is" : "is not") << " lossy" << reason + << ", but round-trip conversion " << (did_value_change ? "did" : "did not") + << " change the value. original: " << original << ", converted: " << converted + << ", round_trip: " << round_trip; } }; @@ -759,6 +787,10 @@ TEST(IsConversionLossy, CorrectlyDiscriminatesBetweenLossyAndLosslessConversions // Feet-to-inches tests overflow. test_round_trip_for_every_uint16_value(feet, inches); + + // Yards-to-meters (and vice versa) tests truncation and overflow. + test_round_trip_for_every_uint16_value(yards, meters); + test_round_trip_for_every_uint16_value(meters, yards); } TEST(AreQuantityTypesEquivalent, RequiresSameRepAndEquivalentUnits) {