From 18fdbd2404b3b6b23ba8457e4c42674ffc3fffa4 Mon Sep 17 00:00:00 2001 From: Kim Barrett Date: Tue, 24 Mar 2026 20:32:31 +0000 Subject: [PATCH] 8314258: Add integer_cast for checking conversions don't change the value Reviewed-by: azafari, stefank, lkorinth --- src/hotspot/share/utilities/integerCast.hpp | 157 ++++++++++ .../gtest/utilities/test_integerCast.cpp | 287 ++++++++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 src/hotspot/share/utilities/integerCast.hpp create mode 100644 test/hotspot/gtest/utilities/test_integerCast.cpp diff --git a/src/hotspot/share/utilities/integerCast.hpp b/src/hotspot/share/utilities/integerCast.hpp new file mode 100644 index 00000000000..0715cab18d5 --- /dev/null +++ b/src/hotspot/share/utilities/integerCast.hpp @@ -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 + +// 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), + ENABLE_IF(std::is_integral_v)> +constexpr bool is_always_integer_convertible() { + if constexpr (std::is_signed_v == std::is_signed_v) { + // signed => signed or unsigned => unsigned. + return sizeof(To) >= sizeof(From); + } else if constexpr (std::is_signed_v) { + // 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), + ENABLE_IF(std::is_integral_v)> +constexpr bool is_integer_convertible(From from) { + if constexpr (is_always_integer_convertible()) { + // 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) { + // unsigned => signed or unsigned => unsigned. + // Convert To::max to corresponding unsigned for compare. + using U = std::make_unsigned_t; + return from <= static_cast(std::numeric_limits::max()); + } else if constexpr (std::is_signed_v) { + // signed => signed. + return ((std::numeric_limits::min() <= from) && + (from <= std::numeric_limits::max())); + } else { + // signed => unsigned. Convert from to corresponding unsigned for compare. + using U = std::make_unsigned_t; + return (0 <= from) && (static_cast(from) <= std::numeric_limits::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() +// 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), + ENABLE_IF(std::is_integral_v)> +constexpr To integer_cast(From from) { + if constexpr (is_always_integer_convertible()) { + static_assert(permit_tautology, "tautological integer_cast"); + } else { +#ifdef ASSERT + if (!is_integer_convertible(from)) { + if constexpr (std::is_signed_v) { + fatal("integer_cast failed: %jd", static_cast(from)); + } else { + fatal("integer_cast failed: %ju", static_cast(from)); + } + } +#endif // ASSERT + } + return static_cast(from); +} + +// Equivalent to "integer_cast(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), + ENABLE_IF(std::is_integral_v)> +constexpr To integer_cast_permit_tautology(From from) { + return integer_cast(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), + ENABLE_IF(std::is_enum_v)> +constexpr To integer_cast(From from) { + using U = std::underlying_type_t; + return integer_cast(static_cast(from)); +} + +#endif // SHARE_UTILITIES_INTEGERCAST_HPP diff --git a/test/hotspot/gtest/utilities/test_integerCast.cpp b/test/hotspot/gtest/utilities/test_integerCast.cpp new file mode 100644 index 00000000000..87d35384c2c --- /dev/null +++ b/test/hotspot/gtest/utilities/test_integerCast.cpp @@ -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()); +static_assert(!is_always_integer_convertible()); +static_assert(is_always_integer_convertible()); +static_assert(is_always_integer_convertible()); + +// Tautology tests for unsigned -> unsigned types. +static_assert(is_always_integer_convertible()); +static_assert(!is_always_integer_convertible()); +static_assert(is_always_integer_convertible()); +static_assert(is_always_integer_convertible()); + +// Tautology tests for signed -> unsigned types. +static_assert(!is_always_integer_convertible()); +static_assert(!is_always_integer_convertible()); +static_assert(!is_always_integer_convertible()); +static_assert(!is_always_integer_convertible()); + +// Tautology tests for unsigned -> signed types. +static_assert(!is_always_integer_convertible()); +static_assert(!is_always_integer_convertible()); +static_assert(is_always_integer_convertible()); +static_assert(!is_always_integer_convertible()); + +template +struct TestIntegerCastValues { + static TestIntegerCastValues values; + + T minus_one = static_cast(-1); + T zero = static_cast(0); + T one = static_cast(1); + T min = std::numeric_limits::min(); + T max = std::numeric_limits::max(); +}; + +template +TestIntegerCastValues TestIntegerCastValues::values{}; + +template +struct TestIntegerCastPairedValues { + static TestIntegerCastPairedValues values; + + From min = static_cast(std::numeric_limits::min()); + From max = static_cast(std::numeric_limits::max()); +}; + +template +TestIntegerCastPairedValues +TestIntegerCastPairedValues::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 +struct TestIntegerCastIntegerValues { + static TestIntegerCastIntegerValues values; + + TestIntegerCastValues to; + TestIntegerCastValues from; + TestIntegerCastPairedValues to_as_from; +}; + +template +TestIntegerCastIntegerValues +TestIntegerCastIntegerValues::values{}; + +template +static void good_integer_conversion(From from) { + ASSERT_TRUE(is_integer_convertible(from)); + EXPECT_EQ(static_cast(from), integer_cast(from)); +} + +template +static void bad_integer_conversion(From from) { + EXPECT_FALSE(is_integer_convertible(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; + const Values& values = Values::values; + + good_integer_conversion(values.from.minus_one); + good_integer_conversion(values.from.zero); + good_integer_conversion(values.from.one); + bad_integer_conversion(values.from.min); + bad_integer_conversion(values.from.max); + good_integer_conversion(values.to_as_from.min); + good_integer_conversion(values.to_as_from.max); + bad_integer_conversion(values.to_as_from.min - 1); + bad_integer_conversion(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; + const Values& values = Values::values; + + bad_integer_conversion(values.from.minus_one); + good_integer_conversion(values.from.zero); + good_integer_conversion(values.from.one); + good_integer_conversion(values.from.min); + bad_integer_conversion(values.from.max); + good_integer_conversion(values.to_as_from.min); + good_integer_conversion(values.to_as_from.min); + bad_integer_conversion(values.to_as_from.min - 1); + bad_integer_conversion(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; + const Values& values = Values::values; + + good_integer_conversion(values.from.zero); + good_integer_conversion(values.from.one); + good_integer_conversion(values.from.min); + bad_integer_conversion(values.from.max); + bad_integer_conversion(values.to_as_from.min); + good_integer_conversion(values.to_as_from.max); + bad_integer_conversion(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; + const Values& values = Values::values; + + bad_integer_conversion(values.from.minus_one); + good_integer_conversion(values.from.zero); + good_integer_conversion(values.from.one); + good_integer_conversion(values.from.min); + bad_integer_conversion(values.from.max); + bad_integer_conversion(values.to_as_from.min); + good_integer_conversion(values.to_as_from.max); + bad_integer_conversion(values.to_as_from.min - 1); + bad_integer_conversion(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; + const Values& values = Values::values; + + bad_integer_conversion(values.from.minus_one); + good_integer_conversion(values.from.zero); + good_integer_conversion(values.from.one); + bad_integer_conversion(values.from.min); + good_integer_conversion(values.from.max); + good_integer_conversion(values.to_as_from.min); + bad_integer_conversion(values.to_as_from.max); +} + +TEST(TestIntegerCast, narrow_signed_to_wide_unsigned_integers) { + using To = uint64_t; + using From = int32_t; + using Values = TestIntegerCastIntegerValues; + const Values& values = Values::values; + + bad_integer_conversion(values.from.minus_one); + good_integer_conversion(values.from.zero); + good_integer_conversion(values.from.one); + bad_integer_conversion(values.from.min); + good_integer_conversion(values.from.max); + good_integer_conversion(values.to_as_from.min); + bad_integer_conversion(values.to_as_from.max); +} + +TEST(TestIntegerCast, wide_signed_to_narrow_unsigned_integers) { + using To = uint32_t; + using From = int64_t; + using Values = TestIntegerCastIntegerValues; + const Values& values = Values::values; + + bad_integer_conversion(values.from.minus_one); + good_integer_conversion(values.from.zero); + good_integer_conversion(values.from.one); + bad_integer_conversion(values.from.min); + bad_integer_conversion(values.from.max); + good_integer_conversion(values.to_as_from.min); + good_integer_conversion(values.to_as_from.max); +} + +TEST(TestIntegerCast, permit_tautology) { + using From = uint32_t; + using To = int64_t; + using Values = TestIntegerCastIntegerValues; + const Values& values = Values::values; + + static_assert(is_always_integer_convertible()); + EXPECT_EQ(static_cast(values.from.min), + (integer_cast(values.from.min))); + EXPECT_EQ(static_cast(values.from.min), + (integer_cast_permit_tautology(values.from.min))); + EXPECT_EQ(static_cast(values.from.max), + (integer_cast(values.from.max))); + EXPECT_EQ(static_cast(values.from.max), + integer_cast_permit_tautology(values.from.max)); +} + +TEST(TestIntegerCast, check_constexpr) { + using From = int64_t; + using To = int32_t; + constexpr From value = std::numeric_limits::max(); + constexpr To converted = integer_cast(value); + EXPECT_EQ(static_cast(value), converted); +} + +#ifdef ASSERT + +TEST_VM_ASSERT(TestIntegerCast, cast_failure_signed_range) { + using From = int64_t; + using To = int32_t; + using Values = TestIntegerCastIntegerValues; + const Values& values = Values::values; + + From value = values.from.max; + To expected = static_cast(value); // Narrowing conversion. + EXPECT_FALSE(is_integer_convertible(value)); + // Should assert. If it doesn't, then shuld be equal, so fail. + EXPECT_NE(static_cast(value), integer_cast(value)); +} + +TEST_VM_ASSERT(TestIntegerCast, cast_failure_unsigned_range) { + using From = uint64_t; + using To = uint32_t; + using Values = TestIntegerCastIntegerValues; + const Values& values = Values::values; + + From value = values.from.max; + To expected = static_cast(value); // Narrowing conversion. + EXPECT_FALSE(is_integer_convertible(value)); + // Should assert. If it doesn't, then should be equal, so fail. + EXPECT_NE(static_cast(value), integer_cast(value)); +} + +#endif // ASSERT