8314258: Add integer_cast for checking conversions don't change the value

Reviewed-by: azafari, stefank, lkorinth
This commit is contained in:
Kim Barrett 2026-03-24 20:32:31 +00:00
parent 2afd7b8b14
commit 18fdbd2404
2 changed files with 444 additions and 0 deletions

View File

@ -0,0 +1,157 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*
*/
#ifndef SHARE_UTILITIES_INTEGERCAST_HPP
#define SHARE_UTILITIES_INTEGERCAST_HPP
#include "cppstdlib/limits.hpp"
#include "cppstdlib/type_traits.hpp"
#include "metaprogramming/enableIf.hpp"
#include "utilities/debug.hpp"
#include "utilities/macros.hpp"
#include <stdint.h>
// Tests whether all values for the From type are within the range of values
// for the To Type. From and To must be integral types. This is used by
// integer_cast to test for tautological conversions.
template<typename From, typename To,
ENABLE_IF(std::is_integral_v<From>),
ENABLE_IF(std::is_integral_v<To>)>
constexpr bool is_always_integer_convertible() {
if constexpr (std::is_signed_v<To> == std::is_signed_v<From>) {
// signed => signed or unsigned => unsigned.
return sizeof(To) >= sizeof(From);
} else if constexpr (std::is_signed_v<From>) {
// signed => unsigned is never tautological, because of negative values.
return false;
} else {
// unsigned => signed.
return sizeof(To) > sizeof(From);
}
}
// Tests whether the value of from is within the range of values for the To
// type. To and From must be integral types. This is used by integer_cast
// to test whether the conversion should be performed.
template<typename To, typename From,
ENABLE_IF(std::is_integral_v<To>),
ENABLE_IF(std::is_integral_v<From>)>
constexpr bool is_integer_convertible(From from) {
if constexpr (is_always_integer_convertible<From, To>()) {
// This clause simplifies direct calls and the implementation below. It
// isn't needed by integer_cast, where a tautological call is discarded.
return true;
} else if constexpr (std::is_unsigned_v<From>) {
// unsigned => signed or unsigned => unsigned.
// Convert To::max to corresponding unsigned for compare.
using U = std::make_unsigned_t<To>;
return from <= static_cast<U>(std::numeric_limits<To>::max());
} else if constexpr (std::is_signed_v<To>) {
// signed => signed.
return ((std::numeric_limits<To>::min() <= from) &&
(from <= std::numeric_limits<To>::max()));
} else {
// signed => unsigned. Convert from to corresponding unsigned for compare.
using U = std::make_unsigned_t<From>;
return (0 <= from) && (static_cast<U>(from) <= std::numeric_limits<To>::max());
}
}
// Convert the from value to the To type, after a debug-only check that the
// value of from is within the range of values for the To type. To and From
// must be integral types.
//
// permit_tautology determines the behavior when a conversion will always
// succeed because the range of values for the From type is enclosed by the
// range of values for the To type (is_always_integer_convertible<From, To>()
// is true). If true, the conversion will be performed as requested. If
// false, a compile-time error is produced. The default is false for 64bit
// platforms, true for 32bit platforms. See integer_cast_permit_tautology as
// the preferred way to override the default and always provide a true value.
//
// Unnecessary integer_casts make code harder to understand. Hence the
// compile-time failure for tautological conversions, to alert that a code
// change is making a integer_cast unnecessary. This can be suppressed on a
// per-call basis, because there are cases where a conversion might only
// sometimes be tautological. For example, the types involved may vary by
// platform. Another case is if the operation is in a template with dependent
// types, with the operation only being tautological for some instantiations.
// Suppressing the tautology check is an alternative to possibly complex
// metaprogramming to only perform the integer_cast when necessary.
//
// Despite that, for 32bit platforms the default is to not reject unnecessary
// integer_casts. This is because 64bit platforms are the primary target, and
// are likely to require conversions in some places. However, some of those
// conversions will be tautological on 32bit platforms, such as size_t => uint.
template<typename To,
bool permit_tautology = LP64_ONLY(false) NOT_LP64(true),
typename From,
ENABLE_IF(std::is_integral_v<To>),
ENABLE_IF(std::is_integral_v<From>)>
constexpr To integer_cast(From from) {
if constexpr (is_always_integer_convertible<From, To>()) {
static_assert(permit_tautology, "tautological integer_cast");
} else {
#ifdef ASSERT
if (!is_integer_convertible<To>(from)) {
if constexpr (std::is_signed_v<From>) {
fatal("integer_cast failed: %jd", static_cast<intmax_t>(from));
} else {
fatal("integer_cast failed: %ju", static_cast<uintmax_t>(from));
}
}
#endif // ASSERT
}
return static_cast<To>(from);
}
// Equivalent to "integer_cast<To, true>(from)", disabling the compile-time
// check for tautological casts. Using this function is prefered to direct
// use of the permit_tautology template parameter for integer_cast, unless the
// choice is computed.
template<typename To, typename From,
ENABLE_IF(std::is_integral_v<To>),
ENABLE_IF(std::is_integral_v<From>)>
constexpr To integer_cast_permit_tautology(From from) {
return integer_cast<To, true>(from);
}
// Convert an enumerator to an integral value via static_cast, after a
// debug-only check that the value is within the range for the destination
// type. This is mostly for compatibility with old code. Class scoped enums
// were used to work around ancient compilers that didn't implement class
// scoped static integral constants properly, and HotSpot code still has many
// examples of this. For others it might be sufficient to provide an explicit
// underlying type and either permit implicit conversions or use
// PrimitiveConversion::cast.
template<typename To, typename From,
ENABLE_IF(std::is_integral_v<To>),
ENABLE_IF(std::is_enum_v<From>)>
constexpr To integer_cast(From from) {
using U = std::underlying_type_t<From>;
return integer_cast<To, true /* permit_tautology */>(static_cast<U>(from));
}
#endif // SHARE_UTILITIES_INTEGERCAST_HPP

View File

@ -0,0 +1,287 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
#include "cppstdlib/limits.hpp"
#include "cppstdlib/type_traits.hpp"
#include "utilities/integerCast.hpp"
#include "utilities/globalDefinitions.hpp"
#include "unittest.hpp"
// Enable gcc warnings to verify we don't get any of these.
// Eventually we plan to have these globally enabled, but not there yet.
#ifdef __GNUC__
#pragma GCC diagnostic warning "-Wconversion"
#pragma GCC diagnostic warning "-Wsign-conversion"
#endif
// Tautology tests for signed -> signed types.
static_assert(is_always_integer_convertible<int32_t, int32_t>());
static_assert(!is_always_integer_convertible<int64_t, int32_t>());
static_assert(is_always_integer_convertible<int32_t, int64_t>());
static_assert(is_always_integer_convertible<int64_t, int64_t>());
// Tautology tests for unsigned -> unsigned types.
static_assert(is_always_integer_convertible<uint32_t, uint32_t>());
static_assert(!is_always_integer_convertible<uint64_t, uint32_t>());
static_assert(is_always_integer_convertible<uint32_t, uint64_t>());
static_assert(is_always_integer_convertible<uint64_t, uint64_t>());
// Tautology tests for signed -> unsigned types.
static_assert(!is_always_integer_convertible<int32_t, uint32_t>());
static_assert(!is_always_integer_convertible<int64_t, uint32_t>());
static_assert(!is_always_integer_convertible<int32_t, uint64_t>());
static_assert(!is_always_integer_convertible<int64_t, uint64_t>());
// Tautology tests for unsigned -> signed types.
static_assert(!is_always_integer_convertible<uint32_t, int32_t>());
static_assert(!is_always_integer_convertible<uint64_t, int32_t>());
static_assert(is_always_integer_convertible<uint32_t, int64_t>());
static_assert(!is_always_integer_convertible<uint64_t, int64_t>());
template<typename T>
struct TestIntegerCastValues {
static TestIntegerCastValues values;
T minus_one = static_cast<T>(-1);
T zero = static_cast<T>(0);
T one = static_cast<T>(1);
T min = std::numeric_limits<T>::min();
T max = std::numeric_limits<T>::max();
};
template<typename T>
TestIntegerCastValues<T> TestIntegerCastValues<T>::values{};
template<typename To, typename From>
struct TestIntegerCastPairedValues {
static TestIntegerCastPairedValues values;
From min = static_cast<From>(std::numeric_limits<To>::min());
From max = static_cast<From>(std::numeric_limits<To>::max());
};
template<typename To, typename From>
TestIntegerCastPairedValues<To, From>
TestIntegerCastPairedValues<To, From>::values{};
//////////////////////////////////////////////////////////////////////////////
// Integer casts between integral types of different sizes.
// Test narrowing to verify checking.
// Test widening to verify no compiler warnings for tautological comparisons.
template<typename To, typename From>
struct TestIntegerCastIntegerValues {
static TestIntegerCastIntegerValues values;
TestIntegerCastValues<To> to;
TestIntegerCastValues<From> from;
TestIntegerCastPairedValues<To, From> to_as_from;
};
template<typename To, typename From>
TestIntegerCastIntegerValues<To, From>
TestIntegerCastIntegerValues<To, From>::values{};
template<typename To, typename From>
static void good_integer_conversion(From from) {
ASSERT_TRUE(is_integer_convertible<To>(from));
EXPECT_EQ(static_cast<To>(from), integer_cast<To>(from));
}
template<typename To, typename From>
static void bad_integer_conversion(From from) {
EXPECT_FALSE(is_integer_convertible<To>(from));
}
// signed -> signed is tautological unless From is wider than To.
TEST(TestIntegerCast, wide_signed_to_narrow_signed_integers) {
using To = int32_t;
using From = int64_t;
using Values = TestIntegerCastIntegerValues<To, From>;
const Values& values = Values::values;
good_integer_conversion<To>(values.from.minus_one);
good_integer_conversion<To>(values.from.zero);
good_integer_conversion<To>(values.from.one);
bad_integer_conversion<To>(values.from.min);
bad_integer_conversion<To>(values.from.max);
good_integer_conversion<To>(values.to_as_from.min);
good_integer_conversion<To>(values.to_as_from.max);
bad_integer_conversion<To>(values.to_as_from.min - 1);
bad_integer_conversion<To>(values.to_as_from.max + 1);
}
// unsigned -> unsigned is tautological unless From is wider than To.
TEST(TestIntegerCast, wide_unsigned_to_narrow_unsigned_integers) {
using To = uint32_t;
using From = uint64_t;
using Values = TestIntegerCastIntegerValues<To, From>;
const Values& values = Values::values;
bad_integer_conversion<To>(values.from.minus_one);
good_integer_conversion<To>(values.from.zero);
good_integer_conversion<To>(values.from.one);
good_integer_conversion<To>(values.from.min);
bad_integer_conversion<To>(values.from.max);
good_integer_conversion<To>(values.to_as_from.min);
good_integer_conversion<To>(values.to_as_from.min);
bad_integer_conversion<To>(values.to_as_from.min - 1);
bad_integer_conversion<To>(values.to_as_from.max + 1);
}
TEST(TestIntegerCast, unsigned_to_signed_same_size_integers) {
using To = int32_t;
using From = uint32_t;
using Values = TestIntegerCastIntegerValues<To, From>;
const Values& values = Values::values;
good_integer_conversion<To>(values.from.zero);
good_integer_conversion<To>(values.from.one);
good_integer_conversion<To>(values.from.min);
bad_integer_conversion<To>(values.from.max);
bad_integer_conversion<To>(values.to_as_from.min);
good_integer_conversion<To>(values.to_as_from.max);
bad_integer_conversion<To>(values.to_as_from.max + 1);
}
// Narrow unsigned to wide signed is tautological.
TEST(TestIntegerCast, wide_unsigned_to_narrow_signed_integers) {
using To = int32_t;
using From = uint64_t;
using Values = TestIntegerCastIntegerValues<To, From>;
const Values& values = Values::values;
bad_integer_conversion<To>(values.from.minus_one);
good_integer_conversion<To>(values.from.zero);
good_integer_conversion<To>(values.from.one);
good_integer_conversion<To>(values.from.min);
bad_integer_conversion<To>(values.from.max);
bad_integer_conversion<To>(values.to_as_from.min);
good_integer_conversion<To>(values.to_as_from.max);
bad_integer_conversion<To>(values.to_as_from.min - 1);
bad_integer_conversion<To>(values.to_as_from.max + 1);
}
TEST(TestIntegerCast, signed_to_unsigned_same_size_integers) {
using To = uint32_t;
using From = int32_t;
using Values = TestIntegerCastIntegerValues<To, From>;
const Values& values = Values::values;
bad_integer_conversion<To>(values.from.minus_one);
good_integer_conversion<To>(values.from.zero);
good_integer_conversion<To>(values.from.one);
bad_integer_conversion<To>(values.from.min);
good_integer_conversion<To>(values.from.max);
good_integer_conversion<To>(values.to_as_from.min);
bad_integer_conversion<To>(values.to_as_from.max);
}
TEST(TestIntegerCast, narrow_signed_to_wide_unsigned_integers) {
using To = uint64_t;
using From = int32_t;
using Values = TestIntegerCastIntegerValues<To, From>;
const Values& values = Values::values;
bad_integer_conversion<To>(values.from.minus_one);
good_integer_conversion<To>(values.from.zero);
good_integer_conversion<To>(values.from.one);
bad_integer_conversion<To>(values.from.min);
good_integer_conversion<To>(values.from.max);
good_integer_conversion<To>(values.to_as_from.min);
bad_integer_conversion<To>(values.to_as_from.max);
}
TEST(TestIntegerCast, wide_signed_to_narrow_unsigned_integers) {
using To = uint32_t;
using From = int64_t;
using Values = TestIntegerCastIntegerValues<To, From>;
const Values& values = Values::values;
bad_integer_conversion<To>(values.from.minus_one);
good_integer_conversion<To>(values.from.zero);
good_integer_conversion<To>(values.from.one);
bad_integer_conversion<To>(values.from.min);
bad_integer_conversion<To>(values.from.max);
good_integer_conversion<To>(values.to_as_from.min);
good_integer_conversion<To>(values.to_as_from.max);
}
TEST(TestIntegerCast, permit_tautology) {
using From = uint32_t;
using To = int64_t;
using Values = TestIntegerCastIntegerValues<To, From>;
const Values& values = Values::values;
static_assert(is_always_integer_convertible<From, To>());
EXPECT_EQ(static_cast<To>(values.from.min),
(integer_cast<To, true>(values.from.min)));
EXPECT_EQ(static_cast<To>(values.from.min),
(integer_cast_permit_tautology<To>(values.from.min)));
EXPECT_EQ(static_cast<To>(values.from.max),
(integer_cast<To, true>(values.from.max)));
EXPECT_EQ(static_cast<To>(values.from.max),
integer_cast_permit_tautology<To>(values.from.max));
}
TEST(TestIntegerCast, check_constexpr) {
using From = int64_t;
using To = int32_t;
constexpr From value = std::numeric_limits<To>::max();
constexpr To converted = integer_cast<To>(value);
EXPECT_EQ(static_cast<To>(value), converted);
}
#ifdef ASSERT
TEST_VM_ASSERT(TestIntegerCast, cast_failure_signed_range) {
using From = int64_t;
using To = int32_t;
using Values = TestIntegerCastIntegerValues<To, From>;
const Values& values = Values::values;
From value = values.from.max;
To expected = static_cast<To>(value); // Narrowing conversion.
EXPECT_FALSE(is_integer_convertible<To>(value));
// Should assert. If it doesn't, then shuld be equal, so fail.
EXPECT_NE(static_cast<To>(value), integer_cast<To>(value));
}
TEST_VM_ASSERT(TestIntegerCast, cast_failure_unsigned_range) {
using From = uint64_t;
using To = uint32_t;
using Values = TestIntegerCastIntegerValues<To, From>;
const Values& values = Values::values;
From value = values.from.max;
To expected = static_cast<To>(value); // Narrowing conversion.
EXPECT_FALSE(is_integer_convertible<To>(value));
// Should assert. If it doesn't, then should be equal, so fail.
EXPECT_NE(static_cast<To>(value), integer_cast<To>(value));
}
#endif // ASSERT