-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
Add and document explicit-rep conversion checkers
The core enabling feature is the new "static cast checkers" library target. For each source and destination type, this target provides separate checks for overflow and truncation when static casting. We continue with our usual policy of treating floating point types as "value preserving". I did initially explore the possibility of treating large integer inputs as "truncating" when converted to a floating point type that can't represent them exactly. However, I found this made the library somewhat harder to reason about, for questionable benefit. Additionally, I think it is fair to assume that users intentionally entering the floating point domain have already accepted a kind of "magnitude based reasoning", and trying to split hairs about preserving exact integer values just felt too cute. With these static cast checkers in hand, the explicit-rep runtime conversion checkers become simple. We check the static cast to the common type, the unit conversion, and the final narrowing static cast to the destination type. To figure out how to write all these functions, I used some "fuzz-ish" utilities, which generated random values of various integral and floating point types, and performed various explicit-rep conversions. I checked that the round-trip conversion changed the value if-and-only-if `is_conversion_lossy` was true. After also taking intermediate sign flips into account (to handle some signed/unsigned conversion edge cases), I got to a point where integral-to-integral conversions always gave the right result. This gives me confidence in the overall approach. When floating point values came into the picture, I wasn't able to design a fully satisfactory policy to avoid both false positives and false negatives. However, I did get to a point where the kinds of errors I saw were ones I found acceptable, relating to "the usual floating point error". This was also what led me to embrace the policy of simply treating floating point destination types as value-preserving, consistent with the rest of the library. This machinery is not ready for prime time, but I may post it as an abandoned draft PR for posterity. I also updated the documentation, including making the floating point policy explicit. Fixes #110.
Showing
7 changed files
with
576 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
// Copyright 2024 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 <cmath> | ||
#include <exception> | ||
#include <limits> | ||
#include <type_traits> | ||
|
||
namespace au { | ||
namespace detail { | ||
|
||
template <typename Source, typename Dest> | ||
struct StaticCastChecker; | ||
|
||
template <typename Dest, typename Source> | ||
constexpr bool will_static_cast_overflow(Source x) { | ||
return StaticCastChecker<Source, Dest>::will_static_cast_overflow(x); | ||
} | ||
|
||
template <typename Dest, typename Source> | ||
constexpr bool will_static_cast_truncate(Source x) { | ||
return StaticCastChecker<Source, Dest>::will_static_cast_truncate(x); | ||
} | ||
|
||
//////////////////////////////////////////////////////////////////////////////////////////////////// | ||
// Implementation details below. | ||
//////////////////////////////////////////////////////////////////////////////////////////////////// | ||
|
||
//////////////////////////////////////////////////////////////////////////////////////////////////// | ||
// Overflow checking: | ||
|
||
// Earlier enum values have higher priority than later ones. | ||
enum class OverflowSituation { | ||
DEST_BOUNDS_CONTAIN_SOURCE_BOUNDS, | ||
UNSIGNED_TO_INTEGRAL, | ||
SIGNED_TO_UNSIGNED, | ||
SIGNED_TO_SIGNED, | ||
FLOAT_TO_ANYTHING, | ||
UNEXPLORED, | ||
}; | ||
|
||
template <typename Source, typename Dest> | ||
constexpr OverflowSituation categorize_overflow_situation() { | ||
static_assert(std::is_arithmetic<Source>::value && std::is_arithmetic<Dest>::value, | ||
"Only arithmetic types are supported so far."); | ||
|
||
if (std::is_integral<Source>::value && std::is_integral<Dest>::value) { | ||
if ((std::is_signed<Source>::value == std::is_signed<Dest>::value) && | ||
(sizeof(Source) <= sizeof(Dest))) { | ||
return OverflowSituation::DEST_BOUNDS_CONTAIN_SOURCE_BOUNDS; | ||
} | ||
|
||
if (std::is_unsigned<Source>::value) { | ||
return OverflowSituation::UNSIGNED_TO_INTEGRAL; | ||
} | ||
|
||
return std::is_unsigned<Dest>::value ? OverflowSituation::SIGNED_TO_UNSIGNED | ||
: OverflowSituation::SIGNED_TO_SIGNED; | ||
} | ||
|
||
if (std::is_integral<Source>::value && std::is_floating_point<Dest>::value) { | ||
// For any integral-to-floating-point situation, `Dest` should always fully contain | ||
// `Source`. This code simply double checks our assumption. | ||
return ((static_cast<long double>(std::numeric_limits<Dest>::max()) >= | ||
static_cast<long double>(std::numeric_limits<Source>::max())) && | ||
(static_cast<long double>(std::numeric_limits<Dest>::lowest()) <= | ||
static_cast<long double>(std::numeric_limits<Source>::lowest()))) | ||
? OverflowSituation::DEST_BOUNDS_CONTAIN_SOURCE_BOUNDS | ||
: OverflowSituation::UNEXPLORED; | ||
} | ||
|
||
if (std::is_floating_point<Source>::value && std::is_integral<Dest>::value) { | ||
return OverflowSituation::FLOAT_TO_ANYTHING; | ||
} | ||
|
||
if (std::is_floating_point<Source>::value && std::is_floating_point<Dest>::value) { | ||
return (sizeof(Source) <= sizeof(Dest)) | ||
? OverflowSituation::DEST_BOUNDS_CONTAIN_SOURCE_BOUNDS | ||
: OverflowSituation::FLOAT_TO_ANYTHING; | ||
} | ||
|
||
return OverflowSituation::UNEXPLORED; | ||
} | ||
|
||
template <typename Source, typename Dest, OverflowSituation Cat> | ||
struct StaticCastOverflowImpl; | ||
|
||
template <typename Source, typename Dest> | ||
struct StaticCastOverflowImpl<Source, Dest, OverflowSituation::DEST_BOUNDS_CONTAIN_SOURCE_BOUNDS> { | ||
static constexpr bool will_static_cast_overflow(Source) { return false; } | ||
}; | ||
|
||
template <typename Source, typename Dest> | ||
struct StaticCastOverflowImpl<Source, Dest, OverflowSituation::UNSIGNED_TO_INTEGRAL> { | ||
static constexpr bool will_static_cast_overflow(Source x) { | ||
// Note that we know that the max value of `Dest` can fit into `Source`, because otherwise, | ||
// this would have been categorized as `DEST_BOUNDS_CONTAIN_SOURCE_BOUNDS` rather than | ||
// `UNSIGNED_TO_INTEGRAL`. | ||
return x > static_cast<Source>(std::numeric_limits<Dest>::max()); | ||
} | ||
}; | ||
|
||
template <typename Source, typename Dest> | ||
struct StaticCastOverflowImpl<Source, Dest, OverflowSituation::SIGNED_TO_UNSIGNED> { | ||
static constexpr bool will_static_cast_overflow(Source x) { | ||
return (x < 0) || | ||
(static_cast<std::make_unsigned_t<Source>>(x) > | ||
static_cast<std::make_unsigned_t<Source>>(std::numeric_limits<Dest>::max())); | ||
} | ||
}; | ||
|
||
template <typename Source, typename Dest> | ||
struct StaticCastOverflowImpl<Source, Dest, OverflowSituation::SIGNED_TO_SIGNED> { | ||
static constexpr bool will_static_cast_overflow(Source x) { | ||
return (x < static_cast<Source>(std::numeric_limits<Dest>::lowest())) || | ||
(x > static_cast<Source>(std::numeric_limits<Dest>::max())); | ||
} | ||
}; | ||
|
||
template <typename Source, typename Dest> | ||
struct StaticCastOverflowImpl<Source, Dest, OverflowSituation::FLOAT_TO_ANYTHING> { | ||
static constexpr bool will_static_cast_overflow(Source x) { | ||
// It's pretty safe to assume that `Source` can hold the limits of `Dest`, because otherwise | ||
// this would have been categorized as `DEST_BOUNDS_CONTAIN_SOURCE_BOUNDS` rather than | ||
// `FLOAT_TO_ANYTHING`. | ||
return (x < static_cast<Source>(std::numeric_limits<Dest>::lowest())) || | ||
(x > static_cast<Source>(std::numeric_limits<Dest>::max())); | ||
} | ||
}; | ||
|
||
//////////////////////////////////////////////////////////////////////////////////////////////////// | ||
// Truncation checking: | ||
|
||
enum class TruncationSituation { | ||
CANNOT_TRUNCATE, | ||
FLOAT_TO_INTEGRAL, | ||
UNEXPLORED, | ||
}; | ||
|
||
template <typename Source, typename Dest> | ||
constexpr TruncationSituation categorize_truncation_situation() { | ||
static_assert(std::is_arithmetic<Source>::value && std::is_arithmetic<Dest>::value, | ||
"Only arithmetic types are supported so far."); | ||
|
||
if (std::is_same<Source, Dest>::value) { | ||
return TruncationSituation::CANNOT_TRUNCATE; | ||
} | ||
|
||
if (std::is_floating_point<Dest>::value) { | ||
// We explicitly treat floating point destinations as value-preserving, as does the rest of | ||
// the library. This isn't strictly true, but if a user is going into the floating point | ||
// domain, we assume they are OK with the usual floating point errors. | ||
return TruncationSituation::CANNOT_TRUNCATE; | ||
} | ||
|
||
if (std::is_integral<Source>::value) { | ||
return TruncationSituation::CANNOT_TRUNCATE; | ||
} | ||
|
||
if (std::is_floating_point<Source>::value && std::is_integral<Dest>::value) { | ||
return TruncationSituation::FLOAT_TO_INTEGRAL; | ||
} | ||
|
||
return TruncationSituation::UNEXPLORED; | ||
} | ||
|
||
template <typename Source, typename Dest, TruncationSituation Cat> | ||
struct StaticCastTruncateImpl; | ||
|
||
template <typename Source, typename Dest> | ||
struct StaticCastTruncateImpl<Source, Dest, TruncationSituation::CANNOT_TRUNCATE> { | ||
static constexpr bool will_static_cast_truncate(Source) { return false; } | ||
}; | ||
|
||
template <typename Source, typename Dest> | ||
struct StaticCastTruncateImpl<Source, Dest, TruncationSituation::FLOAT_TO_INTEGRAL> { | ||
static constexpr bool will_static_cast_truncate(Source x) { return std::trunc(x) != x; } | ||
}; | ||
|
||
//////////////////////////////////////////////////////////////////////////////////////////////////// | ||
// Main implementation: | ||
|
||
template <typename Source, typename Dest> | ||
struct StaticCastChecker | ||
: StaticCastOverflowImpl<Source, Dest, categorize_overflow_situation<Source, Dest>()>, | ||
StaticCastTruncateImpl<Source, Dest, categorize_truncation_situation<Source, Dest>()> {}; | ||
|
||
} // namespace detail | ||
} // namespace au |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// Copyright 2024 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/static_cast_checkers.hh" | ||
|
||
#include "gtest/gtest.h" | ||
|
||
namespace au { | ||
namespace detail { | ||
|
||
TEST(WillStaticCastOverflow, DependsOnValueForUnsignedToNonContainingSigned) { | ||
EXPECT_FALSE(will_static_cast_overflow<int8_t>(uint8_t{127})); | ||
EXPECT_TRUE(will_static_cast_overflow<int8_t>(uint8_t{128})); | ||
} | ||
|
||
TEST(WillStaticCastOverflow, AlwaysFalseForUnsignedToContainingSigned) { | ||
EXPECT_FALSE(will_static_cast_overflow<int>(uint8_t{124})); | ||
EXPECT_FALSE(will_static_cast_overflow<int>(uint8_t{125})); | ||
} | ||
|
||
TEST(WillStaticCastOverflow, ChecksLimitForNonContainingSameSignedness) { | ||
EXPECT_FALSE(will_static_cast_overflow<int8_t>(127)); | ||
EXPECT_TRUE(will_static_cast_overflow<int8_t>(128)); | ||
} | ||
|
||
TEST(WillStaticCastOverflow, TrueForNegativeInputAndUnsignedDestination) { | ||
EXPECT_TRUE(will_static_cast_overflow<uint8_t>(-1)); | ||
EXPECT_TRUE(will_static_cast_overflow<unsigned int>(int8_t{-1})); | ||
} | ||
|
||
TEST(WillStaticCastOverflow, FalseWhenDestBoundsContainsSourceBounds) { | ||
EXPECT_FALSE(will_static_cast_overflow<float>(std::numeric_limits<uint64_t>::max())); | ||
} | ||
|
||
TEST(WillStaticCastOverflow, DependsOnTypeLimitsForFloatToInt) { | ||
EXPECT_TRUE(will_static_cast_overflow<uint8_t>(-0.0001)); | ||
EXPECT_FALSE(will_static_cast_overflow<uint8_t>(0.0000)); | ||
EXPECT_FALSE(will_static_cast_overflow<uint8_t>(0.0001)); | ||
|
||
EXPECT_FALSE(will_static_cast_overflow<uint8_t>(254.9999)); | ||
EXPECT_FALSE(will_static_cast_overflow<uint8_t>(255.0000)); | ||
EXPECT_TRUE(will_static_cast_overflow<uint8_t>(255.0001)); | ||
} | ||
|
||
TEST(WillStaticCastOverflow, TrueForReallyBigDoubleGoingToFloat) { | ||
EXPECT_TRUE(will_static_cast_overflow<float>(1e200)); | ||
} | ||
|
||
TEST(WillStaticCastTruncate, IntToFloatFalseForIntTypeThatCanFitInFloat) { | ||
EXPECT_FALSE(will_static_cast_truncate<float>(uint8_t{124})); | ||
EXPECT_FALSE(will_static_cast_truncate<double>(124)); | ||
|
||
static_assert(std::numeric_limits<double>::digits >= std::numeric_limits<int32_t>::digits, ""); | ||
EXPECT_FALSE(will_static_cast_truncate<double>(std::numeric_limits<int32_t>::max())); | ||
EXPECT_FALSE(will_static_cast_truncate<double>(std::numeric_limits<int32_t>::max() - 1)); | ||
|
||
static_assert(std::numeric_limits<double>::digits >= std::numeric_limits<uint32_t>::digits, ""); | ||
EXPECT_FALSE(will_static_cast_truncate<double>(std::numeric_limits<uint32_t>::max())); | ||
EXPECT_FALSE(will_static_cast_truncate<double>(std::numeric_limits<uint32_t>::max() - 1)); | ||
} | ||
|
||
TEST(WillStaticCastTruncate, IntToFloatFalseByConvention) { | ||
static_assert(std::numeric_limits<float>::radix == 2, "Test assumes binary"); | ||
|
||
constexpr auto first_unrepresentable = (1 << std::numeric_limits<float>::digits) + 1; | ||
EXPECT_FALSE(will_static_cast_truncate<float>(first_unrepresentable - 2)); | ||
EXPECT_FALSE(will_static_cast_truncate<float>(first_unrepresentable - 1)); | ||
|
||
// This is actually non-representable, but we call it "non-truncating" by convention. | ||
EXPECT_FALSE(will_static_cast_truncate<float>(first_unrepresentable + 0)); | ||
|
||
EXPECT_FALSE(will_static_cast_truncate<float>(first_unrepresentable + 1)); | ||
|
||
// This is actually non-representable, but we call it "non-truncating" by convention. | ||
EXPECT_FALSE(will_static_cast_truncate<float>(first_unrepresentable + 2)); | ||
} | ||
|
||
TEST(WillStaticCastTruncate, AutomaticallyFalseForIntegralToIntegral) { | ||
EXPECT_FALSE(will_static_cast_truncate<int8_t>(uint8_t{127})); | ||
EXPECT_FALSE(will_static_cast_truncate<int8_t>(uint8_t{128})); | ||
EXPECT_FALSE(will_static_cast_truncate<int8_t>(128)); | ||
EXPECT_FALSE(will_static_cast_truncate<int8_t>(uint64_t{9876543210u})); | ||
} | ||
|
||
TEST(WillStaticCastTruncate, TrueForFloatToIntIffInputHasAFractionalPart) { | ||
EXPECT_TRUE(will_static_cast_truncate<uint8_t>(-0.1)); | ||
EXPECT_FALSE(will_static_cast_truncate<uint8_t>(0.0)); | ||
EXPECT_TRUE(will_static_cast_truncate<uint8_t>(0.1)); | ||
|
||
EXPECT_TRUE(will_static_cast_truncate<uint8_t>(254.9)); | ||
EXPECT_FALSE(will_static_cast_truncate<uint8_t>(255.0)); | ||
EXPECT_TRUE(will_static_cast_truncate<uint8_t>(255.1)); | ||
} | ||
|
||
TEST(WillStaticCastTruncate, IgnoresLimitsOfDestinationType) { | ||
// Yes, this would be lossy, but we would chalk it up to "overflow", not "truncation". | ||
EXPECT_FALSE(will_static_cast_truncate<uint8_t>(9999999.0)); | ||
} | ||
|
||
} // namespace detail | ||
} // namespace au |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters