Bug 646323 - Rewrite mfbt/Casting.h assertion in modern style, and teach it to deal with floating point values. r=kinetik

This now uses `if constexpr (...)` which is a lot more readable, and still
compiles to almost no assembly instructions, as expected.

Floating point casting assert when casting an integer that's too large to be
represented exactly as a floating point (e.g. UINT64_MAX to double, since double
have less than 64 bytes of mantissa), or when casting a double that's too large
to be represented in a float.

Differential Revision: https://phabricator.services.mozilla.com/D167955
This commit is contained in:
Paul Adenot 2023-02-01 14:05:14 +00:00
Родитель b3be76a29b
Коммит 65406dde19
2 изменённых файлов: 257 добавлений и 123 удалений

Просмотреть файл

@ -12,8 +12,9 @@
#include "mozilla/Assertions.h"
#include <cstring>
#include <limits.h>
#include <type_traits>
#include <limits>
#include <cmath>
namespace mozilla {
@ -65,156 +66,133 @@ inline To BitwiseCast(const From aFrom) {
namespace detail {
enum ToSignedness { ToIsSigned, ToIsUnsigned };
enum FromSignedness { FromIsSigned, FromIsUnsigned };
template <typename T>
constexpr int64_t safe_integer() {
static_assert(std::is_floating_point_v<T>);
return std::pow(2, std::numeric_limits<T>::digits);
}
template <typename From, typename To,
FromSignedness =
std::is_signed_v<From> ? FromIsSigned : FromIsUnsigned,
ToSignedness = std::is_signed_v<To> ? ToIsSigned : ToIsUnsigned>
struct BoundsCheckImpl;
template <typename T>
constexpr uint64_t safe_integer_unsigned() {
static_assert(std::is_floating_point_v<T>);
return std::pow(2, std::numeric_limits<T>::digits);
}
// Implicit conversions on operands to binary operations make this all a bit
// hard to verify. Attempt to ease the pain below by *only* comparing values
// that are obviously the same type (and will undergo no further conversions),
// even when it's not strictly necessary, for explicitness.
// This is working around https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81676,
// fixed in gcc-10
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
template <typename In, typename Out>
bool IsInBounds(In aIn) {
constexpr bool inSigned = std::is_signed_v<In>;
constexpr bool outSigned = std::is_signed_v<Out>;
constexpr bool bothSigned = inSigned && outSigned;
constexpr bool bothUnsigned = !inSigned && !outSigned;
constexpr bool inFloat = std::is_floating_point_v<In>;
constexpr bool outFloat = std::is_floating_point_v<Out>;
constexpr bool bothFloat = inFloat && outFloat;
constexpr bool noneFloat = !inFloat && !outFloat;
constexpr Out outMax = std::numeric_limits<Out>::max();
constexpr Out outMin = std::numeric_limits<Out>::lowest();
enum UUComparison { FromIsBigger, FromIsNotBigger };
// This selects the widest of two types, and is used to cast throughout.
using select_widest = std::conditional_t<(sizeof(In) > sizeof(Out)), In, Out>;
// Unsigned-to-unsigned range check
template <typename From, typename To,
UUComparison =
(sizeof(From) > sizeof(To)) ? FromIsBigger : FromIsNotBigger>
struct UnsignedUnsignedCheck;
template <typename From, typename To>
struct UnsignedUnsignedCheck<From, To, FromIsBigger> {
public:
static bool checkBounds(const From aFrom) { return aFrom <= From(To(-1)); }
};
template <typename From, typename To>
struct UnsignedUnsignedCheck<From, To, FromIsNotBigger> {
public:
static bool checkBounds(const From aFrom) { return true; }
};
template <typename From, typename To>
struct BoundsCheckImpl<From, To, FromIsUnsigned, ToIsUnsigned> {
public:
static bool checkBounds(const From aFrom) {
return UnsignedUnsignedCheck<From, To>::checkBounds(aFrom);
}
};
// Signed-to-unsigned range check
template <typename From, typename To>
struct BoundsCheckImpl<From, To, FromIsSigned, ToIsUnsigned> {
public:
static bool checkBounds(const From aFrom) {
if (aFrom < 0) {
if constexpr (bothFloat) {
if (aIn > select_widest(outMax) || aIn < select_widest(outMin)) {
return false;
}
if (sizeof(To) >= sizeof(From)) {
return true;
}
// Normal casting applies, the floating point number is floored.
if constexpr (inFloat && !outFloat) {
static_assert(sizeof(aIn) <= sizeof(int64_t));
// Check if the input floating point is larger than the output bounds. This
// catches situations where the input is a float larger than the max of the
// output type.
if (aIn < static_cast<double>(outMin) ||
aIn > static_cast<double>(outMax)) {
return false;
}
return aFrom <= From(To(-1));
}
};
// Unsigned-to-signed range check
enum USComparison { FromIsSmaller, FromIsNotSmaller };
template <typename From, typename To,
USComparison =
(sizeof(From) < sizeof(To)) ? FromIsSmaller : FromIsNotSmaller>
struct UnsignedSignedCheck;
template <typename From, typename To>
struct UnsignedSignedCheck<From, To, FromIsSmaller> {
public:
static bool checkBounds(const From aFrom) { return true; }
};
template <typename From, typename To>
struct UnsignedSignedCheck<From, To, FromIsNotSmaller> {
public:
static bool checkBounds(const From aFrom) {
const To MaxValue = To((1ULL << (CHAR_BIT * sizeof(To) - 1)) - 1);
return aFrom <= From(MaxValue);
}
};
template <typename From, typename To>
struct BoundsCheckImpl<From, To, FromIsUnsigned, ToIsSigned> {
public:
static bool checkBounds(const From aFrom) {
return UnsignedSignedCheck<From, To>::checkBounds(aFrom);
}
};
// Signed-to-signed range check
template <typename From, typename To>
struct BoundsCheckImpl<From, To, FromIsSigned, ToIsSigned> {
public:
static bool checkBounds(const From aFrom) {
if (sizeof(From) <= sizeof(To)) {
return true;
// At this point we know that the input can be converted to an integer.
// Check if it's larger than the bounds of the target integer.
if (outSigned) {
int64_t asInteger = static_cast<int64_t>(aIn);
if (asInteger < outMin || asInteger > outMax) {
return false;
}
} else {
uint64_t asInteger = static_cast<uint64_t>(aIn);
if (asInteger < 0 || asInteger > outMax) {
return false;
}
}
const To MaxValue = To((1ULL << (CHAR_BIT * sizeof(To) - 1)) - 1);
const To MinValue = -MaxValue - To(1);
return From(MinValue) <= aFrom && From(aFrom) <= From(MaxValue);
}
};
template <typename From, typename To,
bool TypesAreIntegral =
std::is_integral_v<From>&& std::is_integral_v<To>>
class BoundsChecker;
template <typename From>
class BoundsChecker<From, From, true> {
public:
static bool checkBounds(const From aFrom) { return true; }
};
template <typename From, typename To>
class BoundsChecker<From, To, true> {
public:
static bool checkBounds(const From aFrom) {
return BoundsCheckImpl<From, To>::checkBounds(aFrom);
// Checks if the integer is representable exactly as a floating point value of
// a specific width.
if constexpr (!inFloat && outFloat) {
if constexpr (inSigned) {
if (aIn < -safe_integer<Out>() || aIn > safe_integer<Out>()) {
return false;
}
} else {
if (aIn >= safe_integer_unsigned<Out>()) {
return false;
}
}
}
};
template <typename From, typename To>
inline bool IsInBounds(const From aFrom) {
return BoundsChecker<From, To>::checkBounds(aFrom);
if constexpr (noneFloat) {
if constexpr (bothUnsigned) {
if (aIn > select_widest(outMax)) {
return false;
}
}
if constexpr (bothSigned) {
if (aIn > select_widest(outMax) || aIn < select_widest(outMin)) {
return false;
}
}
if constexpr (inSigned && !outSigned) {
if (aIn < 0 || std::make_unsigned_t<In>(aIn) > outMax) {
return false;
}
}
if constexpr (!inSigned && outSigned) {
if (aIn > select_widest(outMax)) {
return false;
}
}
}
return true;
}
#pragma GCC diagnostic pop
} // namespace detail
/**
* Cast a value of integral type |From| to a value of integral type |To|,
* asserting that the cast will be a safe cast per C++ (that is, that |to| is in
* the range of values permitted for the type |From|).
* Cast a value of type |From| to a value of type |To|, asserting that the cast
* will be a safe cast per C++ (that is, that |to| is in the range of values
* permitted for the type |From|).
* In particular, this will fail if a integer cannot be represented exactly as a
* floating point value, because it's too large.
*/
template <typename To, typename From>
inline To AssertedCast(const From aFrom) {
static_assert(std::is_arithmetic_v<To> && std::is_arithmetic_v<From>);
MOZ_ASSERT((detail::IsInBounds<From, To>(aFrom)));
return static_cast<To>(aFrom);
}
/**
* Cast a value of integral type |From| to a value of integral type |To|,
* release asserting that the cast will be a safe cast per C++ (that is, that
* |to| is in the range of values permitted for the type |From|).
* Cast a value of numeric type |From| to a value of numeric type |To|, release
* asserting that the cast will be a safe cast per C++ (that is, that |to| is in
* the range of values permitted for the type |From|).
* In particular, this will fail if a integer cannot be represented exactly as a
* floating point value, because it's too large.
*/
template <typename To, typename From>
inline To ReleaseAssertedCast(const From aFrom) {
static_assert(std::is_arithmetic_v<To> && std::is_arithmetic_v<From>);
MOZ_RELEASE_ASSERT((detail::IsInBounds<From, To>(aFrom)));
return static_cast<To>(aFrom);
}

Просмотреть файл

@ -5,12 +5,20 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/Casting.h"
#include "mozilla/ThreadSafety.h"
#include <stdint.h>
#include <cstdint>
#include <limits>
#include <type_traits>
using mozilla::AssertedCast;
using mozilla::BitwiseCast;
using mozilla::detail::IsInBounds;
static const uint8_t floatMantissaBitsPlusOne = 24;
static const uint8_t doubleMantissaBitsPlusOne = 53;
template <typename Uint, typename Ulong, bool = (sizeof(Uint) == sizeof(Ulong))>
struct UintUlongBitwiseCast;
@ -88,12 +96,160 @@ static void TestToSmallerSize() {
MOZ_RELEASE_ASSERT((!IsInBounds<int64_t, uint32_t>(int64_t(UINT32_MAX) + 1)));
}
template <typename In, typename Out>
void checkBoundariesFloating(In aEpsilon = {}, Out aIntegerOffset = {}) {
// Check the max value of the input float can't be represented as an integer.
// This is true for all floating point and integer width.
MOZ_RELEASE_ASSERT((!IsInBounds<In, Out>(std::numeric_limits<In>::max())));
// Check that the max value of the integer, as a float, minus an offset that
// depends on the magnitude, can be represented as an integer.
MOZ_RELEASE_ASSERT((IsInBounds<In, Out>(
static_cast<In>(std::numeric_limits<Out>::max() - aIntegerOffset))));
// Check that the max value of the integer, plus a number that depends on the
// magnitude of the number, can't be represented as this integer (because it
// becomes too big).
MOZ_RELEASE_ASSERT((!IsInBounds<In, Out>(
aEpsilon + static_cast<In>(std::numeric_limits<Out>::max()))));
if constexpr (std::is_signed_v<In>) {
// Same for negative numbers.
MOZ_RELEASE_ASSERT(
(!IsInBounds<In, Out>(std::numeric_limits<In>::lowest())));
MOZ_RELEASE_ASSERT((IsInBounds<In, Out>(
static_cast<In>(std::numeric_limits<Out>::lowest()))));
MOZ_RELEASE_ASSERT((!IsInBounds<In, Out>(
static_cast<In>(std::numeric_limits<Out>::lowest()) - aEpsilon)));
} else {
// Check for negative floats and unsigned integer types.
MOZ_RELEASE_ASSERT((!IsInBounds<In, Out>(static_cast<In>(-1))));
}
}
void TestFloatConversion() {
MOZ_RELEASE_ASSERT((!IsInBounds<uint64_t, float>(UINT64_MAX)));
MOZ_RELEASE_ASSERT((!IsInBounds<uint32_t, float>(UINT32_MAX)));
MOZ_RELEASE_ASSERT((IsInBounds<uint16_t, float>(UINT16_MAX)));
MOZ_RELEASE_ASSERT((IsInBounds<uint8_t, float>(UINT8_MAX)));
MOZ_RELEASE_ASSERT((!IsInBounds<int64_t, float>(INT64_MAX)));
MOZ_RELEASE_ASSERT((!IsInBounds<int64_t, float>(INT64_MIN)));
MOZ_RELEASE_ASSERT((!IsInBounds<int32_t, float>(INT32_MAX)));
MOZ_RELEASE_ASSERT((!IsInBounds<int32_t, float>(INT32_MIN)));
MOZ_RELEASE_ASSERT((IsInBounds<int16_t, float>(INT16_MAX)));
MOZ_RELEASE_ASSERT((IsInBounds<int16_t, float>(INT16_MIN)));
MOZ_RELEASE_ASSERT((IsInBounds<int8_t, float>(INT8_MAX)));
MOZ_RELEASE_ASSERT((IsInBounds<int8_t, float>(INT8_MIN)));
MOZ_RELEASE_ASSERT((!IsInBounds<uint64_t, double>(UINT64_MAX)));
MOZ_RELEASE_ASSERT((IsInBounds<uint32_t, double>(UINT32_MAX)));
MOZ_RELEASE_ASSERT((IsInBounds<uint16_t, double>(UINT16_MAX)));
MOZ_RELEASE_ASSERT((IsInBounds<uint8_t, double>(UINT8_MAX)));
MOZ_RELEASE_ASSERT((!IsInBounds<int64_t, double>(INT64_MAX)));
MOZ_RELEASE_ASSERT((!IsInBounds<int64_t, double>(INT64_MIN)));
MOZ_RELEASE_ASSERT((IsInBounds<int32_t, double>(INT32_MAX)));
MOZ_RELEASE_ASSERT((IsInBounds<int32_t, double>(INT32_MIN)));
MOZ_RELEASE_ASSERT((IsInBounds<int16_t, double>(INT16_MAX)));
MOZ_RELEASE_ASSERT((IsInBounds<int16_t, double>(INT16_MIN)));
MOZ_RELEASE_ASSERT((IsInBounds<int8_t, double>(INT8_MAX)));
MOZ_RELEASE_ASSERT((IsInBounds<int8_t, double>(INT8_MIN)));
// Floor check
MOZ_RELEASE_ASSERT((IsInBounds<float, uint64_t>(4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<uint64_t>(4.3f) == 4u));
MOZ_RELEASE_ASSERT((IsInBounds<float, uint32_t>(4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<uint32_t>(4.3f) == 4u));
MOZ_RELEASE_ASSERT((IsInBounds<float, uint16_t>(4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<uint16_t>(4.3f) == 4u));
MOZ_RELEASE_ASSERT((IsInBounds<float, uint8_t>(4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<uint8_t>(4.3f) == 4u));
MOZ_RELEASE_ASSERT((IsInBounds<float, int64_t>(4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<int64_t>(4.3f) == 4u));
MOZ_RELEASE_ASSERT((IsInBounds<float, int32_t>(4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<int32_t>(4.3f) == 4u));
MOZ_RELEASE_ASSERT((IsInBounds<float, int16_t>(4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<int16_t>(4.3f) == 4u));
MOZ_RELEASE_ASSERT((IsInBounds<float, int8_t>(4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<int8_t>(4.3f) == 4u));
MOZ_RELEASE_ASSERT((IsInBounds<float, int64_t>(-4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<int64_t>(-4.3f) == -4));
MOZ_RELEASE_ASSERT((IsInBounds<float, int32_t>(-4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<int32_t>(-4.3f) == -4));
MOZ_RELEASE_ASSERT((IsInBounds<float, int16_t>(-4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<int16_t>(-4.3f) == -4));
MOZ_RELEASE_ASSERT((IsInBounds<float, int8_t>(-4.3)));
MOZ_RELEASE_ASSERT((AssertedCast<int8_t>(-4.3f) == -4));
// Bound check for float to unsigned integer conversion. The parameters are
// espilons and offsets allowing to check boundaries, that depend on the
// magnitude of the numbers.
checkBoundariesFloating<double, uint64_t>(2049.);
checkBoundariesFloating<double, uint32_t>(1.);
checkBoundariesFloating<double, uint16_t>(1.);
checkBoundariesFloating<double, uint8_t>(1.);
// Large number because of the lack of precision of floats at this magnitude
checkBoundariesFloating<float, uint64_t>(1.1e12f);
checkBoundariesFloating<float, uint32_t>(1.f, 128u);
checkBoundariesFloating<float, uint16_t>(1.f);
checkBoundariesFloating<float, uint8_t>(1.f);
checkBoundariesFloating<double, int64_t>(1025.);
checkBoundariesFloating<double, int32_t>(1.);
checkBoundariesFloating<double, int16_t>(1.);
checkBoundariesFloating<double, int8_t>(1.);
// Large number because of the lack of precision of floats at this magnitude
checkBoundariesFloating<float, int64_t>(1.1e12f);
checkBoundariesFloating<float, int32_t>(256.f, 64u);
checkBoundariesFloating<float, int16_t>(1.f);
checkBoundariesFloating<float, int8_t>(1.f);
// Integer to floating point, boundary cases
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, float>(
int64_t(std::pow(2, floatMantissaBitsPlusOne)) + 1)));
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, float>(
int64_t(std::pow(2, floatMantissaBitsPlusOne)))));
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, float>(
int64_t(std::pow(2, floatMantissaBitsPlusOne)) - 1)));
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, float>(
int64_t(-std::pow(2, floatMantissaBitsPlusOne)) - 1)));
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, float>(
int64_t(-std::pow(2, floatMantissaBitsPlusOne)))));
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, float>(
int64_t(-std::pow(2, floatMantissaBitsPlusOne)) + 1)));
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, double>(
uint64_t(std::pow(2, doubleMantissaBitsPlusOne)) + 1)));
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, double>(
uint64_t(std::pow(2, doubleMantissaBitsPlusOne)))));
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, double>(
uint64_t(std::pow(2, doubleMantissaBitsPlusOne)) - 1)));
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, double>(
int64_t(-std::pow(2, doubleMantissaBitsPlusOne)) - 1)));
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, double>(
int64_t(-std::pow(2, doubleMantissaBitsPlusOne)))));
MOZ_RELEASE_ASSERT((IsInBounds<int64_t, double>(
int64_t(-std::pow(2, doubleMantissaBitsPlusOne)) + 1)));
MOZ_RELEASE_ASSERT(!(IsInBounds<uint64_t, double>(UINT64_MAX)));
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, double>(INT64_MAX)));
MOZ_RELEASE_ASSERT(!(IsInBounds<int64_t, double>(INT64_MIN)));
MOZ_RELEASE_ASSERT(
!(IsInBounds<double, float>(std::numeric_limits<double>::max())));
MOZ_RELEASE_ASSERT(
!(IsInBounds<double, float>(-std::numeric_limits<double>::max())));
}
int main() {
TestBitwiseCast();
TestSameSize();
TestToBiggerSize();
TestToSmallerSize();
TestFloatConversion();
return 0;
}