Skip to content

GH-46557: [C++][R] Check overflow in float to int casts that allow truncation - #52469

Open
advitrocks9 wants to merge 6 commits into
apache:mainfrom
advitrocks9:fix-float-int-cast-overflow
Open

advitrocks9 wants to merge 6 commits into
apache:mainfrom
advitrocks9:fix-float-int-cast-overflow

Conversation

@advitrocks9

@advitrocks9 advitrocks9 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Rationale for this change

With allow_float_truncate=true and allow_int_overflow=false, a float to int cast never checks the range, so out of range values silently come back as garbage. Casting 1e308, 3e9 or NaN from float64 to int32 returns -2147483648.

What changes are included in this PR?

Adds a WasOutOfRange check for that case. Also exempts the half float to int cast from float-cast-overflow, like float and double, and prints half float values in cast errors instead of their raw bits. R's %/% passes allow_int_overflow = TRUE, since it casts rows it then masks.

Are these changes tested?

Cast.FloatingToIntOverflow tests casts from float16, float32 and float64 to int8 and uint8 with and without truncation, an out of range value in a null slot, the int64 and uint64 limits, and the half float error messages. 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.

Copilot AI balanced review requested due to automatic review settings October 9, 2026 01:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

⚠️ GitHub issue #46557 has been automatically assigned in GitHub to PR creator.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

⚠️ GitHub issue #46557 has been automatically assigned in GitHub to PR creator.

Copilot AI balanced review requested due to automatic review settings October 9, 2026 01:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

⚠️ GitHub issue #46557 has been automatically assigned in GitHub to PR creator.

Copilot AI balanced review requested due to automatic review settings October 9, 2026 02:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@thisisnic
thisisnic requested review from pitrou and a balanced review from Copilot October 11, 2026 17:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The safe path still performs undefined out-of-range conversions before validating them, and half-float diagnostics expose raw bit patterns.

2 open findings

🧠 Review effort: Balanced

Comment on lines +234 to +235
} else if (!options.allow_int_overflow) {
RETURN_NOT_OK(CheckFloatToInt<WasOutOfRange>(batch[0], *out));
Comment on lines +131 to +132
return Status::Invalid("Float value ", val, " out of range converting to ",
*output.type);
@thisisnic

Copy link
Copy Markdown
Member

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.

Copilot AI balanced review requested due to automatic review settings October 11, 2026 18:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@advitrocks9

Copy link
Copy Markdown
Contributor Author

Fixed copilot's concerns

@dmitry-chirkov-dremio dmitry-chirkov-dremio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions github-actions Bot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Oct 21, 2026
Copilot AI balanced review requested due to automatic review settings October 22, 2026 01:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@advitrocks9

Copy link
Copy Markdown
Contributor Author

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.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #46557 has been automatically assigned in GitHub to PR creator.

@dmitry-chirkov-dremio

dmitry-chirkov-dremio commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

1.6-2x slower

Against what baseline? "The before" state or the 4th commit?
Also please re-check if PR description drifted from the actual code changes.

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]"}) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also test 127.5 and -128.5 when truncation is disabled?

Comment on lines +415 to +416
CheckCastFails(ArrayFromJSON(from, "[256.0]"), opts);
CheckCastFails(ArrayFromJSON(from, "[-1.0]"), opts);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about 255.0 and -0.5?

auto opts = CastOptions::Safe(int64());
opts.allow_float_truncate = true;
CheckCastFails(ArrayFromJSON(from, "[9223372036854775808.0]"), opts);
CheckCast(ArrayFromJSON(from, "[-9223372036854775808.0]"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "inf out" mean?

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());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seems to assume that OutT is integral, can we add a static_assert about that?

Comment on lines +135 to +136
if constexpr (std::is_same_v<Checker<InType, OutType>,
WasOutOfRange<InType, OutType>>) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a relevant method on the various checkers? It doesn't make sense to put type specialization here.

Comment on lines +146 to +153
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];
}
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why check before while allow_float_truncate checks after the conversion?

@thisisnic

Copy link
Copy Markdown
Member

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?

Copilot AI balanced review requested due to automatic review settings October 24, 2026 13:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #46557 has been automatically assigned in GitHub to PR creator.

@advitrocks9

Copy link
Copy Markdown
Contributor Author

@pitrou Thanks! I've made the changes. I'd moved the range check first so out of range values wouldn't be converted, but the cast still converts them in null slots and when truncation isn't allowed. So I moved it back after the conversion and removed out_at.

I added the cases you listed. 9223372036854775807 and 9223372036854775807.5 round to 2^63 as float or double, so they fail, and -9223372036854775808.5 rounds to -2^63 and casts to INT64_MIN.

@dmitry-chirkov-dremio That means I've undone the reordering you asked for. I measured the 1.6-2x against main's round trip, in a standalone loop rather than the kernel. I've also fixed the description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants