Skip to content

Treat the float 2**63 as above every int in IntegerRangeType factories - #6542

Open
zonuexe wants to merge 2 commits into
phpstan:2.2.xfrom
zonuexe:int-range-float-2-pow-63
Open

zonuexe wants to merge 2 commits into
phpstan:2.2.xfrom
zonuexe:int-range-float-2-pow-63

Conversation

@zonuexe

@zonuexe zonuexe commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

IntegerRangeType::createAllSmallerThan() and createAllGreaterThanOrEqualTo() mishandle a float argument of exactly 2**63 (9.2233720368547758E18, i.e. PHP_INT_MAX + 1).

The guard $value > PHP_INT_MAX compares against PHP_INT_MAX converted to float, which is 2**63 itself, so the guard is false for exactly 2**63. The code then reaches (int) ceil($value), which emits "The float 9.223372036854776E+18 is not representable as an int, cast occurred" and gives the wrong type:

call before after
createAllSmallerThan(2**63) *NEVER* + warning int
createAllGreaterThanOrEqualTo(2**63) int<min, max> + warning *NEVER*

As a result, if ($i < 9.2233720368547758E18) on an int narrowed $i to never in the truthy branch. On the PHP_INT_MIN side, a float -2**63 is exactly PHP_INT_MIN and can be cast safely, and all four factories already return the right types there.

The first commit changed these two guards to >=, like the other two factories and like #6541 on 2.3.x. That is right on 64-bit builds, but on 32-bit builds PHP_INT_MAX (2147483647) is exact as a float, so >= would make createAllGreaterThanOrEqualTo(2147483647.0) return *NEVER* although the int PHP_INT_MAX satisfies it. The second commit instead decides on the ceil() that is about to be cast, and compares it with PHP_INT_MAX + 1.0, the first float above PHP_INT_MAX on both widths. On 64-bit the result is the same as with >=. The code is the same as #6546 proposes for 2.3.x, so the two branches agree when 2.2.x is merged up.

This surfaced as warnings during self-analysis of code comparing an int|float with 9.2233720368547758E18, which #6539 worked around by comparing only a float.

Tests:

  • IntegerRangeTypeTest::testCreateFromFloat covers all four factories at 2**63, the largest float below it, -2**63, and the largest float below -2**63. These literals are 64-bit int bounds, so it is skipped on 32-bit hosts.
  • IntegerRangeTypeTest::testFloatBounds is the same as in Compare float bounds against PHP_INT_MAX + 1.0 in IntegerRangeType #6546: the bounds around PHP_INT_MAX and PHP_INT_MIN, with expectations that depend on PHP_INT_SIZE. On linux/386 (Docker php:8.5-cli, PHP 8.5.10), two of its rows fail with the first commit alone, and all pass with the second.
  • An nsrt test covers <, <=, > and >= between an int and 9.2233720368547758E18. It passes on linux/386 too.

@zonuexe zonuexe changed the title Treat the float 2**63 as above every int in IntegerRangeType factories Treat the float 2**63 as above every int in IntegerRangeType factories Sep 22, 2026
A float compared with PHP_INT_MAX compares against the float 2**63, so
`$value > PHP_INT_MAX` is false for exactly 2**63. createAllSmallerThan()
and createAllGreaterThanOrEqualTo() then cast 2**63 to int, which emits
"not representable as an int" and yields NeverType / int<min, max>
instead of int / NeverType. Use `>=` like the other two factories do.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit made createAllSmallerThan() and
createAllGreaterThanOrEqualTo() treat a float bound `>= PHP_INT_MAX` as
past the int range. That holds on 64-bit builds, where comparing with
PHP_INT_MAX converts it to float and rounds it up to 2**63, but on 32-bit
builds PHP_INT_MAX is exact as a float: createAllGreaterThanOrEqualTo(
2147483647.0) would return never although the int PHP_INT_MAX satisfies
it, and createAllSmallerThan(2147483647.0) would return int instead of
int<min, 2147483646>.

Decide on the ceil() that is about to be cast instead, compared against
PHP_INT_MAX + 1.0, the first float above PHP_INT_MAX, which is exact on
both widths (2**63 and 2**31). On 64-bit builds every float at or above
2**52 is integral, so the result there is the same as with `>=`. This is
the same code as proposed for 2.3.x in phpstan#6546.

testCreateFromFloat's literals are 64-bit int bounds, so it is skipped on
32-bit hosts; testFloatBounds covers both widths.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant