Repository navigation
GH-46557: [C++][R] Check overflow in float to int casts that allow truncation - #52469
advitrocks9 wants to merge 5 commits into
Conversation
|
|
|
|
|
|
| } else if (!options.allow_int_overflow) { | ||
| RETURN_NOT_OK(CheckFloatToInt<WasOutOfRange>(batch[0], *out)); |
| return Status::Invalid("Float value ", val, " out of range converting to ", | ||
| *output.type); |
|
Thanks for picking up this PR @advitrocks9! I've triggered a Copilot review and asked one of our C++ maintainers to take a look at this. |
|
Fixed copilot's concerns |
dmitry-chirkov-dremio
left a comment
There was a problem hiding this comment.
The WasOutOfRange/Checker approach looks useful. Could we also use the range check before conversion on the checked float-to-integer paths, including when allow_float_truncate is false?
The current order still performs an undefined conversion before reporting the error. Adding ARROW_DISABLE_UBSAN to the half-float path suppresses that check rather than avoiding the invalid conversion. Please also cover null slots containing out-of-range values.
There may be an opportunity to share the validation loop with CheckIntegersInRange, but I would leave that cleanup and the similar Gandiva casts to separate work.
|
Thanks! I've committed the fixes. For allow_float_truncate=false I kept the round trip, since an input-only check was 1.6-2x slower. I also kept the half float exemption for null slots and unchecked casts, same as float and double. |
|
|
Against what baseline? "The before" state or the 4th commit? |
| 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]"}) { |
There was a problem hiding this comment.
Can we also test 127.5 and -128.5 when truncation is disabled?
| CheckCastFails(ArrayFromJSON(from, "[256.0]"), opts); | ||
| CheckCastFails(ArrayFromJSON(from, "[-1.0]"), opts); |
| auto opts = CastOptions::Safe(int64()); | ||
| opts.allow_float_truncate = true; | ||
| CheckCastFails(ArrayFromJSON(from, "[9223372036854775808.0]"), opts); | ||
| CheckCast(ArrayFromJSON(from, "[-9223372036854775808.0]"), |
There was a problem hiding this comment.
What about 9223372036854775807, 9223372036854775807.5, -9223372036854775808.5?
| EXPECT_RAISES_WITH_MESSAGE_THAT(Invalid, ::testing::HasSubstr("Float value 1.5"), | ||
| Cast(ArrayFromJSON(float16(), "[1.5]"), opts)); | ||
| opts.allow_float_truncate = true; | ||
| EXPECT_RAISES_WITH_MESSAGE_THAT(Invalid, ::testing::HasSubstr("Float value inf out"), |
| template <typename InType, typename OutType, typename InT = typename InType::c_type, | ||
| typename OutT = typename OutType::c_type> | ||
| struct WasOutOfRange { | ||
| static constexpr auto kMin = static_cast<InT>(std::numeric_limits<OutT>::min()); |
There was a problem hiding this comment.
These seems to assume that OutT is integral, can we add a static_assert about that?
| if constexpr (std::is_same_v<Checker<InType, OutType>, | ||
| WasOutOfRange<InType, OutType>>) { |
There was a problem hiding this comment.
Can you please add a relevant method on the various checkers? It doesn't make sense to put type specialization here.
| auto out_at = [&](int64_t i) { | ||
| if constexpr (std::is_same_v<Checker<InType, OutType>, | ||
| WasOutOfRange<InType, OutType>>) { | ||
| return OutT{}; | ||
| } else { | ||
| return out_data[i]; | ||
| } | ||
| }; |
| Status CastFloatingToInteger(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) { | ||
| const auto& options = checked_cast<const CastState*>(ctx->state())->options; | ||
| if (options.allow_float_truncate && !options.allow_int_overflow) { | ||
| RETURN_NOT_OK(CheckFloatToInt<WasOutOfRange>(batch[0], *out)); |
There was a problem hiding this comment.
Why check before while allow_float_truncate checks after the conversion?
|
I've dismissed myself as a review so I don't get notifications on this, but once we're happy with the C++, mind either pinging me to take a look at the R side of things or splitting it out into a follow-up @advitrocks9? |


Rationale for this change
With
allow_float_truncate=trueandallow_int_overflow=false, a float to int cast never checks the range, so out of range values silently come back as garbage. Casting1e308,3e9orNaNfrom float64 to int32 returns-2147483648.What changes are included in this PR?
Adds a
WasOutOfRangecheck for that case, which rejects out of range values before they are converted. R's%/%passesallow_int_overflow = TRUE, since it casts rows it then masks.Are these changes tested?
Cast.FloatingToIntOverflowcovers float16, float32 and float64 to int8, uint8, int64 and uint64, with and without truncation, including an out of range value in a null slot. It fails without the fix.Are there any user-facing changes?
These casts raise on out of range, NaN and infinite values, including R's
as.integer().This PR contains a "Critical Fix". These casts silently returned wrong values.
allow_int_overflowoption ignored whenallow_float_truncateset to True #46557