Repository navigation
GH-46557: [C++][R] Check overflow in float to int casts that allow truncation #52469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d490bd6
d255ed4
a7a29ee
95a2688
f2a1baa
f65ceaa
236d1d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,8 @@ | |
|
|
||
| // Implementation of casting to integer, floating point, or decimal types | ||
|
|
||
| #include <cmath> | ||
|
|
||
| #include "arrow/array/builder_primitive.h" | ||
| #include "arrow/compute/kernels/common_internal.h" | ||
| #include "arrow/compute/kernels/scalar_cast_internal.h" | ||
|
|
@@ -72,6 +74,11 @@ struct WasTruncated { | |
| static bool CheckMaybeNull(OutT out_val, InT in_val, bool is_valid) { | ||
| return is_valid && static_cast<InT>(out_val) != in_val; | ||
| } | ||
|
|
||
| static Status Error(InT in_val, const DataType& out_type) { | ||
| return Status::Invalid("Float value ", in_val, " was truncated converting to ", | ||
| out_type); | ||
| } | ||
| }; | ||
|
|
||
| // Half float to int | ||
|
|
@@ -85,18 +92,60 @@ struct WasTruncated<HalfFloatType, OutType> { | |
| static bool CheckMaybeNull(OutT out_val, uint16_t in_val, bool is_valid) { | ||
| return is_valid && static_cast<float>(out_val) != Float16::FromBits(in_val).ToFloat(); | ||
| } | ||
|
|
||
| static Status Error(uint16_t in_val, const DataType& out_type) { | ||
| return WasTruncated<FloatType, OutType>::Error(Float16::FromBits(in_val).ToFloat(), | ||
| out_type); | ||
| } | ||
| }; | ||
|
|
||
| // InType is a floating point type we are planning to cast to integer | ||
| template <typename InType, typename OutType, typename InT = typename InType::c_type, | ||
| typename OutT = typename OutType::c_type> | ||
| ARROW_DISABLE_UBSAN("float-cast-overflow") | ||
| Status CheckFloatTruncation(const ArraySpan& input, const ArraySpan& output) { | ||
| auto GetErrorMessage = [&](InT val) { | ||
| return Status::Invalid("Float value ", val, " was truncated converting to ", | ||
| *output.type); | ||
| }; | ||
| struct WasOutOfRange { | ||
| static_assert(std::is_integral_v<OutT>); | ||
|
|
||
| static constexpr auto kMin = static_cast<InT>(std::numeric_limits<OutT>::min()); | ||
| static constexpr auto kMaxPlusOne = | ||
| static_cast<InT>(std::numeric_limits<OutT>::max() / 2 + 1) * 2; | ||
|
|
||
| static bool Check(OutT, InT in_val) { | ||
| return !(std::trunc(in_val) >= kMin) | !(in_val < kMaxPlusOne); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a lot of negatives here, can you make this simpler and more readable? |
||
| } | ||
|
|
||
| static bool CheckMaybeNull(OutT out_val, InT in_val, bool is_valid) { | ||
| return is_valid && Check(out_val, in_val); | ||
| } | ||
|
|
||
| static Status Error(InT in_val, const DataType& out_type) { | ||
| return Status::Invalid("Float value ", in_val, " out of range converting to ", | ||
| out_type); | ||
| } | ||
| }; | ||
|
|
||
| template <typename OutType> | ||
| struct WasOutOfRange<HalfFloatType, OutType> { | ||
| using OutT = typename OutType::c_type; | ||
| static bool Check(OutT out_val, uint16_t in_val) { | ||
| return WasOutOfRange<FloatType, OutType>::Check(out_val, | ||
| Float16::FromBits(in_val).ToFloat()); | ||
| } | ||
|
|
||
| static bool CheckMaybeNull(OutT out_val, uint16_t in_val, bool is_valid) { | ||
| return is_valid && Check(out_val, in_val); | ||
| } | ||
|
|
||
| static Status Error(uint16_t in_val, const DataType& out_type) { | ||
| return WasOutOfRange<FloatType, OutType>::Error(Float16::FromBits(in_val).ToFloat(), | ||
| out_type); | ||
| } | ||
| }; | ||
|
|
||
| // InType is a floating point type we are planning to cast to integer | ||
| template <template <typename...> class Checker, typename InType, typename OutType, | ||
| typename InT = typename InType::c_type, | ||
| typename OutT = typename OutType::c_type> | ||
| ARROW_DISABLE_UBSAN("float-cast-overflow") | ||
| Status CheckFloatToIntValues(const ArraySpan& input, const ArraySpan& output) { | ||
| const InT* in_data = input.GetValues<InT>(1); | ||
| const OutT* out_data = output.GetValues<OutT>(1); | ||
|
|
||
|
|
@@ -110,29 +159,28 @@ Status CheckFloatTruncation(const ArraySpan& input, const ArraySpan& output) { | |
| if (block.popcount == block.length) { | ||
| // Fast path: branchless | ||
| for (int64_t i = 0; i < block.length; ++i) { | ||
| block_out_of_bounds |= | ||
| WasTruncated<InType, OutType>::Check(out_data[i], in_data[i]); | ||
| block_out_of_bounds |= Checker<InType, OutType>::Check(out_data[i], in_data[i]); | ||
| } | ||
| } else if (block.popcount > 0) { | ||
| // Indices have nulls, must only boundscheck non-null values | ||
| for (int64_t i = 0; i < block.length; ++i) { | ||
| block_out_of_bounds |= WasTruncated<InType, OutType>::CheckMaybeNull( | ||
| block_out_of_bounds |= Checker<InType, OutType>::CheckMaybeNull( | ||
| out_data[i], in_data[i], bit_util::GetBit(bitmap, offset_position + i)); | ||
| } | ||
| } | ||
| if (ARROW_PREDICT_FALSE(block_out_of_bounds)) { | ||
| if (input.GetNullCount() > 0) { | ||
| for (int64_t i = 0; i < block.length; ++i) { | ||
| if (WasTruncated<InType, OutType>::CheckMaybeNull( | ||
| if (Checker<InType, OutType>::CheckMaybeNull( | ||
| out_data[i], in_data[i], | ||
| bit_util::GetBit(bitmap, offset_position + i))) { | ||
| return GetErrorMessage(in_data[i]); | ||
| return Checker<InType, OutType>::Error(in_data[i], *output.type); | ||
| } | ||
| } | ||
| } else { | ||
| for (int64_t i = 0; i < block.length; ++i) { | ||
| if (WasTruncated<InType, OutType>::Check(out_data[i], in_data[i])) { | ||
| return GetErrorMessage(in_data[i]); | ||
| if (Checker<InType, OutType>::Check(out_data[i], in_data[i])) { | ||
| return Checker<InType, OutType>::Error(in_data[i], *output.type); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -145,41 +193,42 @@ Status CheckFloatTruncation(const ArraySpan& input, const ArraySpan& output) { | |
| return Status::OK(); | ||
| } | ||
|
|
||
| template <typename InType> | ||
| Status CheckFloatToIntTruncationImpl(const ArraySpan& input, const ArraySpan& output) { | ||
| template <template <typename...> class Checker, typename InType> | ||
| Status CheckFloatToIntImpl(const ArraySpan& input, const ArraySpan& output) { | ||
| switch (output.type->id()) { | ||
| case Type::INT8: | ||
| return CheckFloatTruncation<InType, Int8Type>(input, output); | ||
| return CheckFloatToIntValues<Checker, InType, Int8Type>(input, output); | ||
| case Type::INT16: | ||
| return CheckFloatTruncation<InType, Int16Type>(input, output); | ||
| return CheckFloatToIntValues<Checker, InType, Int16Type>(input, output); | ||
| case Type::INT32: | ||
| return CheckFloatTruncation<InType, Int32Type>(input, output); | ||
| return CheckFloatToIntValues<Checker, InType, Int32Type>(input, output); | ||
| case Type::INT64: | ||
| return CheckFloatTruncation<InType, Int64Type>(input, output); | ||
| return CheckFloatToIntValues<Checker, InType, Int64Type>(input, output); | ||
| case Type::UINT8: | ||
| return CheckFloatTruncation<InType, UInt8Type>(input, output); | ||
| return CheckFloatToIntValues<Checker, InType, UInt8Type>(input, output); | ||
| case Type::UINT16: | ||
| return CheckFloatTruncation<InType, UInt16Type>(input, output); | ||
| return CheckFloatToIntValues<Checker, InType, UInt16Type>(input, output); | ||
| case Type::UINT32: | ||
| return CheckFloatTruncation<InType, UInt32Type>(input, output); | ||
| return CheckFloatToIntValues<Checker, InType, UInt32Type>(input, output); | ||
| case Type::UINT64: | ||
| return CheckFloatTruncation<InType, UInt64Type>(input, output); | ||
| return CheckFloatToIntValues<Checker, InType, UInt64Type>(input, output); | ||
| default: | ||
| break; | ||
| } | ||
| DCHECK(false); | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| Status CheckFloatToIntTruncation(const ExecValue& input, const ExecResult& output) { | ||
| template <template <typename...> class Checker> | ||
| Status CheckFloatToInt(const ExecValue& input, const ExecResult& output) { | ||
| switch (input.type()->id()) { | ||
| case Type::FLOAT: | ||
| return CheckFloatToIntTruncationImpl<FloatType>(input.array, *output.array_span()); | ||
| return CheckFloatToIntImpl<Checker, FloatType>(input.array, *output.array_span()); | ||
| case Type::DOUBLE: | ||
| return CheckFloatToIntTruncationImpl<DoubleType>(input.array, *output.array_span()); | ||
| return CheckFloatToIntImpl<Checker, DoubleType>(input.array, *output.array_span()); | ||
| case Type::HALF_FLOAT: | ||
| return CheckFloatToIntTruncationImpl<HalfFloatType>(input.array, | ||
| *output.array_span()); | ||
| return CheckFloatToIntImpl<Checker, HalfFloatType>(input.array, | ||
| *output.array_span()); | ||
| default: | ||
| break; | ||
| } | ||
|
|
@@ -192,7 +241,9 @@ Status CastFloatingToInteger(KernelContext* ctx, const ExecSpan& batch, ExecResu | |
| CastNumberToNumberUnsafe(batch[0].type()->id(), out->type()->id(), batch[0].array, | ||
| out->array_span_mutable()); | ||
| if (!options.allow_float_truncate) { | ||
| RETURN_NOT_OK(CheckFloatToIntTruncation(batch[0], *out)); | ||
| RETURN_NOT_OK(CheckFloatToInt<WasTruncated>(batch[0], *out)); | ||
| } else if (!options.allow_int_overflow) { | ||
| RETURN_NOT_OK(CheckFloatToInt<WasOutOfRange>(batch[0], *out)); | ||
| } | ||
| return Status::OK(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -398,6 +398,66 @@ TEST(Cast, FloatingToInt) { | |
| } | ||
| } | ||
|
|
||
| TEST(Cast, FloatingToIntOverflow) { | ||
| for (auto from : {float16(), float32(), float64()}) { | ||
| for (bool allow_float_truncate : {false, true}) { | ||
| auto opts = CastOptions::Safe(int8()); | ||
| opts.allow_float_truncate = allow_float_truncate; | ||
| for (auto json : {"[128.0]", "[-129.0]", "[NaN]", "[Inf]", "[-Inf]"}) { | ||
|
advitrocks9 marked this conversation as resolved.
Resolved
|
||
| CheckCastFails(ArrayFromJSON(from, json), opts); | ||
| } | ||
| CheckCast(ArrayFromJSON(from, "[127.0, null, -128.0]"), | ||
| ArrayFromJSON(int8(), "[127, null, -128]"), opts); | ||
| CheckCast(TweakValidityBit(ArrayFromJSON(from, "[1000.0, 1.0]"), 0, false), | ||
| ArrayFromJSON(int8(), "[null, 1]"), opts); | ||
|
|
||
| opts.to_type = uint8(); | ||
| CheckCastFails(ArrayFromJSON(from, "[256.0]"), opts); | ||
| CheckCastFails(ArrayFromJSON(from, "[-1.0]"), opts); | ||
|
advitrocks9 marked this conversation as resolved.
Resolved
|
||
| CheckCast(ArrayFromJSON(from, "[255.0, null, 0.0]"), | ||
| ArrayFromJSON(uint8(), "[255, null, 0]"), opts); | ||
| } | ||
|
|
||
| auto opts = CastOptions::Safe(int8()); | ||
| CheckCastFails(ArrayFromJSON(from, "[127.5]"), opts); | ||
| CheckCastFails(ArrayFromJSON(from, "[-128.5]"), opts); | ||
| opts.allow_float_truncate = true; | ||
| CheckCast(ArrayFromJSON(from, "[127.5, -128.5]"), | ||
| ArrayFromJSON(int8(), "[127, -128]"), opts); | ||
|
|
||
| opts = CastOptions::Safe(uint8()); | ||
| CheckCastFails(ArrayFromJSON(from, "[255.5]"), opts); | ||
| CheckCastFails(ArrayFromJSON(from, "[-0.5]"), opts); | ||
| opts.allow_float_truncate = true; | ||
| CheckCast(ArrayFromJSON(from, "[255.5, -0.5]"), ArrayFromJSON(uint8(), "[255, 0]"), | ||
| opts); | ||
| } | ||
|
|
||
| for (auto from : {float32(), float64()}) { | ||
| auto opts = CastOptions::Safe(int64()); | ||
| opts.allow_float_truncate = true; | ||
| for (auto json : {"[9223372036854775807]", "[9223372036854775807.5]", | ||
| "[9223372036854775808.0]"}) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use only the exact float representations? Otherwise the test is confusing. So: keep Also, add |
||
| CheckCastFails(ArrayFromJSON(from, json), opts); | ||
| } | ||
| CheckCast(ArrayFromJSON(from, "[-9223372036854775808.0, -9223372036854775808.5]"), | ||
| ArrayFromJSON(int64(), "[-9223372036854775808, -9223372036854775808]"), | ||
| opts); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same question here. Also test |
||
| opts.to_type = uint64(); | ||
| CheckCastFails(ArrayFromJSON(from, "[18446744073709551616.0]"), opts); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also -1.0? |
||
| } | ||
|
|
||
| auto opts = CastOptions::Safe(int8()); | ||
| EXPECT_RAISES_WITH_MESSAGE_THAT( | ||
| Invalid, | ||
| ::testing::HasSubstr("Float value 1.500000 was truncated converting to int8"), | ||
| Cast(ArrayFromJSON(float16(), "[1.5]"), opts)); | ||
| opts.allow_float_truncate = true; | ||
| EXPECT_RAISES_WITH_MESSAGE_THAT( | ||
| Invalid, ::testing::HasSubstr("Float value inf out of range converting to int8"), | ||
| Cast(ArrayFromJSON(float16(), "[Inf]"), opts)); | ||
|
Comment on lines
+450
to
+458
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why these tests? float16 is already tested above. |
||
| } | ||
|
|
||
| TEST(Cast, FloatingToFloating) { | ||
| for (auto from : {float16(), float32(), float64()}) { | ||
| for (auto to : {float16(), float32(), float64()}) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.