Repository navigation
Conversation
2**63 as above every int in IntegerRangeType factories
960a598 to
76aae52
Compare
06683c6 to
f91b631
Compare
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.
f91b631 to
23e9544
Compare
| function greaterOrEqual(int $i): void | ||
| { | ||
| if ($i >= 9.2233720368547758E18) { | ||
| assertType('*NEVER*', $i); |
There was a problem hiding this comment.
You're right, thanks. PHP compares an int with a float by converting the int to float, and (float) PHP_INT_MAX is 2**63, so PHP_INT_MAX >= 9.2233720368547758E18 holds. Every int from 2**63 - 512 up converts to 2**63, so the truthy branch there is int<9223372036854775296, max>, not *NEVER*.
The same rounding can shift the bound for any float of magnitude 2**53 or more, not only 2**63. For example, $i < 9007199254740996.0 kept 9007199254740995 in the truthy branch, although that int rounds up to the float itself, and $i <= -9.2233720368547758E18 gave only PHP_INT_MIN, although the ints up to -2**63 + 512 satisfy it.
943c365 makes the four createAll*() factories search for the bound with PHP's own comparison instead of deriving it from ceil() or floor(). The nsrt test now expects int<9223372036854775296, max> for your case, and the new testMatchesPhpComparison checks the ranges against PHP's comparisons around each bound, on 64-bit and on 32-bit builds.
PHP compares an int with a float by converting the int to float, which rounds every int beyond 2**53 on 64-bit builds. PHP_INT_MAX >= 2.0 ** 63 holds, so `$i >= 9.2233720368547758E18` is true for the ints from 2**63 - 512 up, and the createAll*() factories, which derived the bound from ceil() or floor() of the float, narrowed the other branch to ints that never reach it: `$i >= 2**63` became never, `$i < 2**53 + 4` kept 2**53 + 3 (which rounds to 2**53 + 4), and `$i <= -2**63` kept only PHP_INT_MIN although the ints up to -2**63 + 512 satisfy it. A NaN bound emitted "not representable as an int" and gave ranges no int satisfies. Bisect the bound with the comparison itself instead. That is exact on any int width (on 32-bit builds every int converts exactly, which also covers what FLOAT_ABOVE_INT_MAX was for), and NaN gives never, since no comparison holds for it. A value that is neither int nor float now fails on fromFloatComparison()'s float parameter in all four factories, where it used to depend on loose comparisons with the int limits first. testMatchesPhpComparison checks the returned ranges against PHP's own comparisons around the bound on both int widths; testCreateFromFloat pins the 64-bit bounds, including both directions of rounding to even. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2**63 as above every int in IntegerRangeType factoriesThe PHP 7.4 and 8.0 builds downgrade src/ and tests/ with simple-downgrade, which has no visitor for match expressions, so Lint (7.4) failed with "unexpected '=>'" on both files. Pick the comparison with an if/elseif chain in fromFloatComparison(), and pass the factory and the comparison from dataMatchesPhpComparison() as closures. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
This pull request has been marked as ready for review. |
IntegerRangeType::createAllSmallerThan(),createAllSmallerThanOrEqualTo(),createAllGreaterThan()andcreateAllGreaterThanOrEqualTo()can return ranges that disagree with PHP when the bound is a float of magnitude2**53or more, or NaN; for NaN they also emit a "not representable as an int" warning. These factories narrow$int < $floatand the other comparisons with a float constant, so a branch can lose ints that do reach it.Impact
On 64-bit builds, before (current 2.3.x) and after this PR:
$i >= 9.2233720368547758E18*NEVER*int<9223372036854775296, max>$i < 9.2233720368547758E18*NEVER*int<9223372036854775296, max>$i < 9007199254740996.0int<min, 9007199254740995>int<min, 9007199254740994>$i <= -9.2233720368547758E18-9223372036854775808int<min, -9223372036854775296>$i <= 9007199254740992.0int<min, 9007199254740992>int<min, 9007199254740993>$i < NANint<0, max>intPHP_INT_MAX >= 9.2233720368547758E18is true at runtime (https://3v4l.org/WlQ9F), so the first two rows sentPHP_INT_MAXto a branch typed*NEVER*. No comparison holds for NaN, so-1takes the falsy branch of$i < NANtoo.Cause
PHP compares an int with a float by converting the int to float. On 64-bit builds that conversion rounds every int beyond
2**53in magnitude to the nearest float, with ties to even, so9007199254740993converts to9007199254740992.0,9007199254740995to9007199254740996.0, and every int from2**63 - 512up to2**63. The factories took the bound fromceil()orfloor()of the float, which is the exact mathematical bound, not the one PHP's comparison uses. The two agree for floats below2**53in magnitude, and on 32-bit builds, where every int converts exactly; that is whyFLOAT_ABOVE_INT_MAXwas enough there. For NaN, the factories castceil(NAN)orfloor(NAN)to int.Fix
The factories pass a float bound to a new private
fromFloatComparison(), which checks the comparison atPHP_INT_MINandPHP_INT_MAXand, if the result differs, bisects the int range with the comparison itself. The result is the set of ints PHP's own comparison accepts, on either int width. For NaN the factories return*NEVER*because no comparison holds for it, so the falsy branch keeps every int. This replaces the second commit'sFLOAT_ABOVE_INT_MAX, which the new code no longer needs.A value that is neither int nor float now fails on
fromFloatComparison()'sfloatparameter in all four factories. Before,createAllGreaterThanOrEqualTo('-1e30')returnedintfrom a loose comparison withPHP_INT_MINbefore reachingceil().turbo-ext/src/IntegerRangeType.cppgets the same bisection, with the VM's int-with-float comparison (both sides as doubles), and declaresfromFloatComparison()natively, since the native class already declares the twin's private constructor.turbo-ext/tests/type-family.phpadds the2**53bounds and callsfromFloatComparison()through reflection, including an unknown operator.Other sites
ConstantStringTypepasses(float) $this->valuefor a numeric string. PHP compares an int with an integer-numeric string as two ints, so$i < '9007199254740993'should keep9007199254740992in the truthy branch, but it lands in the falsy branch. This was the same before this PR, and it is not changed here.ConstantNumericComparisonTypeTraittests(bool) $this->value, which on PHP 8.5 warns "unexpected NAN value was coerced to bool" for a NaN constant. That is why the nsrt test has no NaN case;testFloatBoundscovers NaN at the factory level.Testing
IntegerRangeTypeTest::testCreateFromFloatpins the 64-bit ranges at2**63,2**63 - 1024,-2**63, the float below-2**63,2**53and2**53 + 4, which covers rounding to even in both directions. It is skipped on 32-bit hosts.IntegerRangeTypeTest::testFloatBoundscovers NaN,INF,-INF,2.5and-2.0, which do not depend on the int width.IntegerRangeTypeTest::testMatchesPhpComparisonchecks, for the bounds that depend on the int width ((float) PHP_INT_MAX,PHP_INT_MAX ± 0.5,(float) PHP_INT_MIN,PHP_INT_MIN ± 0.5,2**31,±(2**53 + 4)), that each returned range holds exactly the ints PHP's comparison accepts within 2048 of the float and of both ends of the int range.<,<=,>and>=with9.2233720368547758E18, and both branches of<and<=with9007199254740996.0.IntegerRangeTypeTest(64-bit)IntegerRangeTypeTest(linux/386, PHP 8.5.10)integer-range-float-two-to-the-63.phpturbo-ext/tests/smoke.phpagainst the new PHP code.cppALL OKA wider check outside the repository ran the four factories at 803 floats (around
±2**52…±2**64, NaN,±INF, random values) and compared each range with PHP's comparison over 25.9 million ints near the bounds: 414,280 mismatches and 4 warnings before on 64-bit, and no mismatches after on 64-bit (with and without the turbo extension) and on linux/386. The full test suite passes with and without the extension loaded.The search costs up to 64 comparisons per call: in the PHP implementation a factory call with a float bound takes about 2 µs instead of about 0.2 µs. The factories run only when a comparison with a float bound is narrowed; with the turbo extension the search is native.