Fix intDiv and intDivOrNull on the smallest positive float - #112410
Fix intDiv and intDivOrNull on the smallest positive float#112410groeneai wants to merge 2 commits into
Conversation
std::numeric_limits<Float>::min is the smallest positive normal value, not the
domain minimum, and is_signed_v is true for Float32, Float64 and BFloat16. The
INT_MIN / -1 overflow guard in throwIfDivisionLeadsToFPE and its non-throwing
twin divisionLeadsToFPE compared a floating dividend against min as if it were
lowest, so an ordinary tiny positive float divided by -1 tripped a check that
exists only for integer overflow.
Three user-visible symptoms:
- intDiv(toFloat64(2.2250738585072014e-308), toFloat64(-1)) threw
ILLEGAL_DIVISION "Division of minimal signed number by minus one"; the
correct result is 0. Same for Float32 with 1.17549435e-38.
- intDivOrNull on the same operands returned NULL instead of 0, a silent
wrong result reached through the predicate twin rather than the thrower.
- BFloat16 was affected identically, at 2^-125.
intDivOrZero already returned 0, but only because it short-circuits to 0,
which equals the real quotient here.
Adding is_integer<A> to both guards restricts them to a signed integer
dividend. Dividing a float by -1 cannot overflow, since IEEE negation is exact
and the quotient truncates to 0, so the guard has no reason to run on that
path. B is deliberately not restricted: a signed integer dividend at its
minimum with a floating -1 divisor is suppressed by intDivOrZero and
intDivOrNull today, and only the is_signed_v<B> clause keeps that working.
The sibling moduloLeadsToFPE already documents this exact trap (ClickHouse#101976), but
that change covered the modulo consumers only and left the division guard
carrying it.
The added clause is constexpr: identical code for integer operands, one
comparison fewer for floats. No setting default changes.
Internal second-model review: 2 rounds, 2 findings, both addressed (click to expand)Before opening this PR I ran an independent cold review of the resulting code plus two rounds of a Findings
The same review asked to rename the test's section labels, the source comment and the filename. I did
Verification performed independently of the fix author💡 The carrier set was re-enumerated from scratch before reading the implementation notes, and 💡 The behaviour delta was measured rather than argued: over the full native scalar type matrix the 💡 The load-bearing premise was measured too: 💡 Test liveness was traced per block rather than assumed. The float rows fail on unpatched Round 2 of the second-model pass returned no findings against the final text. |
Pre-PR validation gate (click to expand)
Session id: cron:clickhouse-impl-slot-46:20260729-063900 |
|
cc @Avogar @Algunenano could you review this? |
|
Workflow [PR], commit [3c17be0] Summary: ❌
|
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 7/7 (100.00%) · Uncovered code |
CI finish ledger - 2e20e97Every failure below has an owner: a fixing PR (mine or external), or a full-effort fix task whose fixing-PR link will be posted here when it opens. Only
This is the only failing check at this head: 174 check-runs, 173 non-failing, 0 queued or in progress, Session id: cron:our-pr-ci-monitor:20260729-180000 |
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fixed
intDivraisingILLEGAL_DIVISION("Division of minimal signed number by minus one") andintDivOrNullreturningNULLwhen the dividend is the smallest positive normalFloat32orFloat64value (or the correspondingBFloat16value,2^-125) and the divisor is-1. The correct result,0, is now returned.Description
std::numeric_limits<Float>::minis the smallest positive normal value, not the domain minimum, andis_signed_vis true forFloat32,Float64andBFloat16. TheINT_MIN / -1overflow guard inthrowIfDivisionLeadsToFPEand its non-throwing twindivisionLeadsToFPEcompared a floating dividend againstminas if it werelowest, so a tiny positive float over-1tripped a check meant for integer overflow.Two user-visible symptoms on
master:intDiv(toFloat64(2.2250738585072014e-308), toFloat64(-1))throwsILLEGAL_DIVISION; the correct result is0. Same forFloat32at1.17549435e-38, and forBFloat16at itsnumeric_limits::minof2^-125.intDivOrNullon the same operands returnsNULLinstead of0- a silent wrong result, reached through the predicate twin rather than the thrower.intDivOrZeroalready returned0, but only because it short-circuits to0, the real quotient here.The fix adds
is_integer<A>to both guards, so they apply only to a signed integer dividend; dividing a float by-1cannot overflow, since IEEE negation is exact and the quotient truncates to0.Bis deliberately not restricted: an integer dividend at its minimum with a floating-1divisor is suppressed byintDivOrZero/intDivOrNulltoday, and only theis_signed_v<B>clause keeps that working.moduloLeadsToFPEdocuments this trap (#101976), but that change covered the modulo consumers only.The new test fails on unpatched
masterand passes with the fix, in every constness shape, underNullable/LowCardinality/arrayMap, and across float widths. IntegerINT_MIN / -1, division by zero and out-of-range floats still throw; the modulo family is unchanged. A local matrix also coverscompile_expressions = 1;DivideIntegralImplis not compilable, so the guard has no JIT copy. Three mutations confirm each changed line is load-bearing; 50/50 runs are green with and without randomization.No setting default changes, and the added clause is
constexpr, so integer code generation is unchanged.