Conversation
zonuexe
added a commit
to zonuexe/phpstan-src
that referenced
this pull request
Sep 23, 2026
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.
createAllSmallerThan() and createAllGreaterThanOrEqualTo() treated a float bound reaching 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) returned never although the int PHP_INT_MAX satisfies it, and createAllSmallerThan(2147483647.0) returned 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 is unchanged there.
zonuexe
force-pushed
the
int-range-float-bound-32bit
branch
from
September 25, 2026 00:56
d908441 to
6031e92
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#6541 (53cc365) changed
IntegerRangeType::createAllSmallerThan()andcreateAllGreaterThanOrEqualTo()to treat a float bound>= PHP_INT_MAXas past the int range. On 64-bit builds that is right, because comparing withPHP_INT_MAXconverts it to float and rounds it up to 2^63. On 32-bit buildsPHP_INT_MAX(2147483647) is exact as a float, so:createAllGreaterThanOrEqualTo(2147483647.0)returns*NEVER*, although the intPHP_INT_MAXsatisfies$i >= 2147483647.0. This is unsound.createAllSmallerThan(2147483647.0)returnsintinstead ofint<min, 2147483646>. This is sound but imprecise.This PR decides on the
ceil()that is about to be cast, and compares it withPHP_INT_MAX + 1.0. That is the first float abovePHP_INT_MAXand is exact on both widths (2^63 and 2^31). Using theceil()rather than$valuematters on 32-bit, where for example2147483647.5lies betweenPHP_INT_MAXandPHP_INT_MAX + 1.0. The turbo mirror inturbo-ext/src/IntegerRangeType.cppgets the same change, followed by the usual bump commit.64-bit behaviour is unchanged for int|float input. On 64-bit, every float at or above 2^52 is already integral, so
ceil($value) >= 2^63is the same test as$value >= (float) PHP_INT_MAX. The only observable difference is for out-of-contract arguments (a string or bool passed to@param int|float $value).createAllSmallerThan()now reachesceil()first and throws itsTypeErrorfor these, even where the old loose comparison returned a type. The native side does the same, andtype-family.phprecords it.IntegerRangeTypeTestreplaces the 64-bit-only expectations from 53cc365 with a data provider that branches onPHP_INT_SIZE. On 64-bit hosts it passes both before and after this change, so on 64-bit CI it only guards against regressions. I ran it on linux/386 (Dockerphp:8.5-cli, PHP 8.5.10,PHP_INT_SIZE4). Before the change, two rows fail there:createAllGreaterThanOrEqualTo(2147483647.0)gives*NEVER*instead of2147483647, andcreateAllSmallerThan(2147483647.0)givesintinstead ofint<min, 2147483646>. After the change all 18 rows pass.Whether PHPStan supports running on a 32-bit PHP at all isn't settled (phpstan/phpstan#11711, phpstan/phpstan#14948), and
composer.jsonhas nophp-64bitrequirement. This PR only makes these two factories correct on such a host and keeps 64-bit behaviour as it is. If 32-bit analyser hosts should be unsupported instead, feel free to close this.#6542 applies the same
>=guard to 2.2.x; I'll update it to this approach so the two branches agree when 2.2.x is merged up.Verified on macOS arm64, PHP 8.5.10:
smoke.php(ALL OK),side-by-side.php,signature-parity.php,walk-trace.php --shards=8(identical)