From 1d219a5bb9c3aa2684b4ee6ef09c5487b3cec342 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 23 Sep 2026 04:51:21 +0900 Subject: [PATCH 1/4] Treat the float 2**63 as above every int in IntegerRangeType factories 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 instead of int / NeverType. Use `>=` like the other two factories do. Co-authored-by: Claude Opus 5.5 --- .../integer-range-float-two-to-the-63.php | 43 +++++++++++++ tests/PHPStan/Type/IntegerRangeTypeTest.php | 62 +++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/integer-range-float-two-to-the-63.php diff --git a/tests/PHPStan/Analyser/nsrt/integer-range-float-two-to-the-63.php b/tests/PHPStan/Analyser/nsrt/integer-range-float-two-to-the-63.php new file mode 100644 index 00000000000..b914af2df34 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/integer-range-float-two-to-the-63.php @@ -0,0 +1,43 @@ += 9.2233720368547758E18) { + assertType('*NEVER*', $i); + } else { + assertType('int', $i); + } + assertType('int', $i); +} + +function smallerOrEqual(int $i): void +{ + if ($i <= 9.2233720368547758E18) { + assertType('int', $i); + } else { + assertType('*NEVER*', $i); + } +} + +function greater(int $i): void +{ + if ($i > 9.2233720368547758E18) { + assertType('*NEVER*', $i); + } else { + assertType('int', $i); + } +} diff --git a/tests/PHPStan/Type/IntegerRangeTypeTest.php b/tests/PHPStan/Type/IntegerRangeTypeTest.php index 099d945b843..d9da5021590 100644 --- a/tests/PHPStan/Type/IntegerRangeTypeTest.php +++ b/tests/PHPStan/Type/IntegerRangeTypeTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Type; use PHPStan\Testing\PHPStanTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use const PHP_INT_MAX; class IntegerRangeTypeTest extends PHPStanTestCase @@ -20,4 +21,65 @@ public function testCreateFromFloatPastTheIntRange(): void $this->assertSame('*NEVER*', IntegerRangeType::createAllGreaterThanOrEqualTo($pastMax)->describe(VerbosityLevel::precise())); } + public static function dataCreateFromFloat(): iterable + { + // 2**63 is PHP_INT_MAX + 1, the smallest float greater than every int + yield '2**63' => [ + 9.2233720368547758E18, + 'int', + 'int', + '*NEVER*', + '*NEVER*', + ]; + + // the largest float smaller than 2**63 + yield '2**63 - 1024' => [ + 9.2233720368547748E18, + 'int', + 'int', + 'int<9223372036854774785, max>', + 'int<9223372036854774784, max>', + ]; + + // -2**63 is exactly PHP_INT_MIN + yield '-2**63' => [ + -9.2233720368547758E18, + '*NEVER*', + '-9223372036854775808', + 'int<-9223372036854775807, max>', + 'int', + ]; + + // the largest float smaller than -2**63 + yield '-2**63 - 2048' => [ + -9.2233720368547779E18, + '*NEVER*', + '*NEVER*', + 'int', + 'int', + ]; + } + + #[DataProvider('dataCreateFromFloat')] + public function testCreateFromFloat( + float $value, + string $expectedSmallerThan, + string $expectedSmallerThanOrEqualTo, + string $expectedGreaterThan, + string $expectedGreaterThanOrEqualTo, + ): void + { + $this->assertSame([ + 'createAllSmallerThan' => $expectedSmallerThan, + 'createAllSmallerThanOrEqualTo' => $expectedSmallerThanOrEqualTo, + 'createAllGreaterThan' => $expectedGreaterThan, + 'createAllGreaterThanOrEqualTo' => $expectedGreaterThanOrEqualTo, + ], [ + 'createAllSmallerThan' => IntegerRangeType::createAllSmallerThan($value)->describe(VerbosityLevel::precise()), + 'createAllSmallerThanOrEqualTo' => IntegerRangeType::createAllSmallerThanOrEqualTo($value)->describe(VerbosityLevel::precise()), + 'createAllGreaterThan' => IntegerRangeType::createAllGreaterThan($value)->describe(VerbosityLevel::precise()), + 'createAllGreaterThanOrEqualTo' => IntegerRangeType::createAllGreaterThanOrEqualTo($value)->describe(VerbosityLevel::precise()), + ]); + } + } From 23e95442cb56c2a75e9d66d159231412f644207a Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 23 Sep 2026 09:28:43 +0900 Subject: [PATCH 2/4] Compare float bounds against PHP_INT_MAX + 1.0 in IntegerRangeType 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. 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/phpstan-src#6546. testCreateFromFloat's literals are 64-bit int bounds, so it is skipped on 32-bit hosts; testFloatBounds covers both widths. --- src/Type/IntegerRangeType.php | 21 ++++-- tests/PHPStan/Type/IntegerRangeTypeTest.php | 75 +++++++++++++++--- turbo-ext/src/IntegerRangeType.cpp | 84 +++++++++++++++------ turbo-ext/tests/type-family.php | 4 +- 4 files changed, 137 insertions(+), 47 deletions(-) diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index 4dd6249a6c1..518d5d00993 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -35,6 +35,9 @@ class IntegerRangeType extends IntegerType implements CompoundType { + /** The first float above PHP_INT_MAX: 2^63 on 64-bit builds, 2^31 on 32-bit ones, exact on both. */ + private const FLOAT_ABOVE_INT_MAX = PHP_INT_MAX + 1.0; + private function __construct(private ?int $min, private ?int $max) { parent::__construct(); @@ -92,9 +95,11 @@ public static function createAllSmallerThan($value): Type return self::fromInterval(null, $value, -1); } - // a float never holds PHP_INT_MAX itself, so the first float past the - // int range is (float) PHP_INT_MAX: every int is smaller than that - if ($value >= PHP_INT_MAX) { + // decided on the ceil() about to be cast: comparing $value with PHP_INT_MAX + // converts PHP_INT_MAX to float, which rounds it up to 2^63 on 64-bit builds + // but keeps it exact on 32-bit ones, where 2147483647.0 is still in range + $ceil = ceil($value); + if ($ceil >= self::FLOAT_ABOVE_INT_MAX) { return new IntegerType(); } @@ -102,7 +107,7 @@ public static function createAllSmallerThan($value): Type return new NeverType(); } - return self::fromInterval(null, (int) ceil($value), -1); + return self::fromInterval(null, (int) $ceil, -1); } /** @@ -164,13 +169,13 @@ public static function createAllGreaterThanOrEqualTo($value): Type return new IntegerType(); } - // (float) PHP_INT_MAX is already past the int range, so no int - // reaches it - if ($value >= PHP_INT_MAX) { + // decided on the ceil() about to be cast, like createAllSmallerThan() + $ceil = ceil($value); + if ($ceil >= self::FLOAT_ABOVE_INT_MAX) { return new NeverType(); } - return self::fromInterval((int) ceil($value), null); + return self::fromInterval((int) $ceil, null); } public function getMin(): ?int diff --git a/tests/PHPStan/Type/IntegerRangeTypeTest.php b/tests/PHPStan/Type/IntegerRangeTypeTest.php index d9da5021590..e00d2c5224f 100644 --- a/tests/PHPStan/Type/IntegerRangeTypeTest.php +++ b/tests/PHPStan/Type/IntegerRangeTypeTest.php @@ -4,23 +4,16 @@ use PHPStan\Testing\PHPStanTestCase; use PHPUnit\Framework\Attributes\DataProvider; +use RuntimeException; +use function restore_error_handler; +use function set_error_handler; use const PHP_INT_MAX; +use const PHP_INT_MIN; +use const PHP_INT_SIZE; class IntegerRangeTypeTest extends PHPStanTestCase { - public function testCreateFromFloatPastTheIntRange(): void - { - // PHP_INT_MAX is not representable as a float: (float) PHP_INT_MAX is - // the first float past the int range - $pastMax = (float) PHP_INT_MAX; - - $this->assertSame('int', IntegerRangeType::createAllSmallerThan($pastMax)->describe(VerbosityLevel::precise())); - $this->assertSame('int', IntegerRangeType::createAllSmallerThanOrEqualTo($pastMax)->describe(VerbosityLevel::precise())); - $this->assertSame('*NEVER*', IntegerRangeType::createAllGreaterThan($pastMax)->describe(VerbosityLevel::precise())); - $this->assertSame('*NEVER*', IntegerRangeType::createAllGreaterThanOrEqualTo($pastMax)->describe(VerbosityLevel::precise())); - } - public static function dataCreateFromFloat(): iterable { // 2**63 is PHP_INT_MAX + 1, the smallest float greater than every int @@ -69,6 +62,10 @@ public function testCreateFromFloat( string $expectedGreaterThanOrEqualTo, ): void { + if (PHP_INT_SIZE !== 8) { + $this->markTestSkipped('The bounds are those of a 64-bit int.'); + } + $this->assertSame([ 'createAllSmallerThan' => $expectedSmallerThan, 'createAllSmallerThanOrEqualTo' => $expectedSmallerThanOrEqualTo, @@ -82,4 +79,58 @@ public function testCreateFromFloat( ]); } + public static function dataFloatBounds(): iterable + { + $smallerThan = static fn (float $value): Type => IntegerRangeType::createAllSmallerThan($value); + $smallerThanOrEqualTo = static fn (float $value): Type => IntegerRangeType::createAllSmallerThanOrEqualTo($value); + $greaterThan = static fn (float $value): Type => IntegerRangeType::createAllGreaterThan($value); + $greaterThanOrEqualTo = static fn (float $value): Type => IntegerRangeType::createAllGreaterThanOrEqualTo($value); + + // PHP_INT_MAX + 1.0 is the first float above PHP_INT_MAX on any int width, and + // (float) PHP_INT_MIN is exact on any int width + $aboveMax = PHP_INT_MAX + 1.0; + yield [$smallerThan, $aboveMax, 'int']; + yield [$smallerThanOrEqualTo, $aboveMax, 'int']; + yield [$greaterThan, $aboveMax, '*NEVER*']; + yield [$greaterThanOrEqualTo, $aboveMax, '*NEVER*']; + yield [$smallerThan, (float) PHP_INT_MIN, '*NEVER*']; + yield [$smallerThanOrEqualTo, (float) PHP_INT_MIN, (string) PHP_INT_MIN]; + yield [$greaterThan, (float) PHP_INT_MIN, 'int<' . (PHP_INT_MIN + 1) . ', max>']; + yield [$greaterThanOrEqualTo, (float) PHP_INT_MIN, 'int']; + + // (float) PHP_INT_MAX is 2^63 on 64-bit builds (above every int) and exact on 32-bit + // ones; so is PHP_INT_MAX - 0.5, which is PHP_INT_MAX - 1 + 0.5 on 32-bit builds + $maxIsExact = PHP_INT_SIZE < 8; + yield [$smallerThan, (float) PHP_INT_MAX, $maxIsExact ? 'int' : 'int']; + yield [$smallerThanOrEqualTo, (float) PHP_INT_MAX, 'int']; + yield [$greaterThan, (float) PHP_INT_MAX, '*NEVER*']; + yield [$greaterThanOrEqualTo, (float) PHP_INT_MAX, $maxIsExact ? (string) PHP_INT_MAX : '*NEVER*']; + yield [$smallerThan, PHP_INT_MAX - 0.5, $maxIsExact ? 'int' : 'int']; + yield [$greaterThanOrEqualTo, PHP_INT_MAX - 0.5, $maxIsExact ? (string) PHP_INT_MAX : '*NEVER*']; + + // between PHP_INT_MAX and the next int on 32-bit builds, 2^63 on 64-bit ones: its + // ceil() is past the int range either way + yield [$smallerThan, PHP_INT_MAX + 0.5, 'int']; + yield [$greaterThanOrEqualTo, PHP_INT_MAX + 0.5, '*NEVER*']; + + yield [$smallerThan, 2.5, 'int']; + yield [$greaterThan, 2.5, 'int<3, max>']; + } + + /** + * @param callable(float): Type $factory + */ + #[DataProvider('dataFloatBounds')] + public function testFloatBounds(callable $factory, float $value, string $expected): void + { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new RuntimeException($errstr); + }); + try { + $this->assertSame($expected, $factory($value)->describe(VerbosityLevel::precise())); + } finally { + restore_error_handler(); + } + } + } diff --git a/turbo-ext/src/IntegerRangeType.cpp b/turbo-ext/src/IntegerRangeType.cpp index 1afa62f7565..5bd8042413e 100644 --- a/turbo-ext/src/IntegerRangeType.cpp +++ b/turbo-ext/src/IntegerRangeType.cpp @@ -27,6 +27,10 @@ namespace sigs = ptdecl::IntegerRangeType::sig; zend_class_entry *pt_ce_integer_range_type = nullptr; +/* the twin's private const FLOAT_ABOVE_INT_MAX = PHP_INT_MAX + 1.0: the first + * double above ZEND_LONG_MAX, exact on 64-bit (2^63) and 32-bit (2^31) builds */ +static constexpr double PT_IRT_FLOAT_ABOVE_INT_MAX = (double) ZEND_LONG_MAX + 1.0; + /* nothing is memoized per request since InitializerExprTypeResolver's * CALCULATE_SCALARS_LIMIT became a native constant */ void pt_integer_range_type_rinit() @@ -171,26 +175,26 @@ class IntegerRangeType /* the createAll*() family: the range of integers beyond an int|float * $value. An int goes straight to fromInterval(); anything else is - * first held against the int limits — a float the way the VM compares - * int with float (both as doubles), any other value loosely — and what - * passes is then rounded by ceil()/floor() and (int)-cast, which is - * where a non-number fails under strict_types. */ + * held against the int limits — a float the way the VM compares int + * with float (both as doubles), any other value loosely — and rounded + * by ceil()/floor() and (int)-cast, which is where a non-number fails + * under strict_types. Where the ceil() is held against the max instead + * of $value (createAllSmallerThan(), createAllGreaterThanOrEqualTo()), + * it is taken first, in the twin's order. */ /* fromInterval(null, $value, -1) */ static zv::Val createAllSmallerThan(zval *value) { if (Z_TYPE_P(value) == IS_LONG) return fromInterval(NullableLong::null(), NullableLong::of(Z_LVAL_P(value)), -1); - int againstMax, againstMin; - if (UNEXPECTED(!compareToLimits(value, againstMax, againstMin))) return zv::Val(); - if (againstMax >= 0) { /* $value >= PHP_INT_MAX */ + double ceiled; + if (UNEXPECTED(!roundToDouble(value, true, ceiled))) return zv::Val(); + if (ceiled >= PT_IRT_FLOAT_ABOVE_INT_MAX) { /* $ceil >= self::FLOAT_ABOVE_INT_MAX */ return integer(); } - if (againstMin <= 0) { /* $value <= PHP_INT_MIN */ + if (Z_DVAL_P(value) <= (double) ZEND_LONG_MIN) { /* $value <= PHP_INT_MIN */ return never(); } - zend_long rounded; - if (UNEXPECTED(!roundToLong(value, true, rounded))) return zv::Val(); - return fromInterval(NullableLong::null(), NullableLong::of(rounded), -1); + return fromInterval(NullableLong::null(), NullableLong::of(zend_dval_to_lval(ceiled)), -1); } /* fromInterval(null, $value) */ @@ -231,17 +235,17 @@ class IntegerRangeType static zv::Val createAllGreaterThanOrEqualTo(zval *value) { if (Z_TYPE_P(value) == IS_LONG) return fromInterval(NullableLong::of(Z_LVAL_P(value)), NullableLong::null(), 0); - int againstMax, againstMin; - if (UNEXPECTED(!compareToLimits(value, againstMax, againstMin))) return zv::Val(); + int againstMin; + if (UNEXPECTED(!compareToMin(value, againstMin))) return zv::Val(); if (againstMin <= 0) { /* $value <= PHP_INT_MIN */ return integer(); } - if (againstMax >= 0) { /* $value >= PHP_INT_MAX */ + double ceiled; + if (UNEXPECTED(!roundToDouble(value, true, ceiled))) return zv::Val(); + if (ceiled >= PT_IRT_FLOAT_ABOVE_INT_MAX) { /* $ceil >= self::FLOAT_ABOVE_INT_MAX */ return never(); } - zend_long rounded; - if (UNEXPECTED(!roundToLong(value, true, rounded))) return zv::Val(); - return fromInterval(NullableLong::of(rounded), NullableLong::null(), 0); + return fromInterval(NullableLong::of(zend_dval_to_lval(ceiled)), NullableLong::null(), 0); } /* $this->min / $this->max; false with an Error pending when the @@ -1127,34 +1131,58 @@ class IntegerRangeType * every one of the four tests false); anything else compares loosely. * false = pending exception */ [[nodiscard]] static bool compareToLimits(zval *value, int &againstMax, int &againstMin) + { + return compareToMax(value, againstMax) && compareToMin(value, againstMin); + } + + [[nodiscard]] static bool compareToMax(zval *value, int &againstMax) { if (EXPECTED(Z_TYPE_P(value) == IS_DOUBLE)) { double d = Z_DVAL_P(value); double max = (double) ZEND_LONG_MAX; - double min = (double) ZEND_LONG_MIN; againstMax = d > max ? 1 : (d < max ? -1 : (d == max ? 0 : -1)); - againstMin = d < min ? -1 : (d > min ? 1 : (d == min ? 0 : 1)); return true; } zval limit; ZVAL_LONG(&limit, ZEND_LONG_MAX); againstMax = zend_compare(value, &limit); - if (UNEXPECTED(EG(exception))) return false; + return !EG(exception); + } + + [[nodiscard]] static bool compareToMin(zval *value, int &againstMin) + { + if (EXPECTED(Z_TYPE_P(value) == IS_DOUBLE)) { + double d = Z_DVAL_P(value); + double min = (double) ZEND_LONG_MIN; + againstMin = d < min ? -1 : (d > min ? 1 : (d == min ? 0 : 1)); + return true; + } + zval limit; ZVAL_LONG(&limit, ZEND_LONG_MIN); againstMin = zend_compare(value, &limit); return !EG(exception); } - /* (int) ceil($value) / (int) floor($value) for a non-int $value: a - * float rounds and casts as the engine casts; anything else is what - * ceil()/floor() reject under strict_types. false = pending exception */ - [[nodiscard]] static bool roundToLong(zval *value, bool useCeil, zend_long &out) + /* ceil($value) / floor($value) for a non-int $value: a float rounds; + * anything else is what ceil()/floor() reject under strict_types. + * false = pending exception */ + [[nodiscard]] static bool roundToDouble(zval *value, bool useCeil, double &out) { if (UNEXPECTED(Z_TYPE_P(value) != IS_DOUBLE)) { zend_type_error("%s(): Argument #1 ($num) must be of type int|float, %s given", useCeil ? "ceil" : "floor", zend_zval_value_name(value)); return false; } - out = zend_dval_to_lval(useCeil ? ceil(Z_DVAL_P(value)) : floor(Z_DVAL_P(value))); + out = useCeil ? ceil(Z_DVAL_P(value)) : floor(Z_DVAL_P(value)); + return true; + } + + /* (int) ceil($value) / (int) floor($value) for a non-int $value, cast as + * the engine casts. false = pending exception */ + [[nodiscard]] static bool roundToLong(zval *value, bool useCeil, zend_long &out) + { + double rounded; + if (UNEXPECTED(!roundToDouble(value, useCeil, rounded))) return false; + out = zend_dval_to_lval(rounded); return true; } @@ -1334,6 +1362,11 @@ static void pt_irt_bound(INTERNAL_FUNCTION_PARAMETERS, bool (IntegerRangeType::* RETURN_LONG(bound.value); } +static void pt_irt_float_above_int_max_constant(zval *out) +{ + ZVAL_DOUBLE(out, PT_IRT_FLOAT_ABOVE_INT_MAX); +} + PT_MINIT_REGISTRATION(pt_register_integer_range_type) { reg::Class cls("PHPStan\\Type\\IntegerRangeType"); @@ -1341,6 +1374,7 @@ PT_MINIT_REGISTRATION(pt_register_integer_range_type) /* "min" and "max" must stay the first two declared properties * (slots::min, slots::max) */ ptdecl::IntegerRangeType::declareProperties(cls); + cls.privateClassConstantValue("FLOAT_ABOVE_INT_MAX", pt_irt_float_above_int_max_constant); cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) { zend_long min, max; diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index 04bf5951fa4..ee911dd64e7 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -353,13 +353,13 @@ } } } - foreach ([0, 1, -1, 42, PHP_INT_MAX, PHP_INT_MIN, PHP_INT_MAX - 1, PHP_INT_MIN + 1, 0.0, 0.5, -0.5, 2.5, -2.5, 1e30, -1e30, 9.2233720368547758E+18, -9.2233720368547758E+18, 9.2e18, -9.2e18, (float) PHP_INT_MAX, (float) PHP_INT_MIN, NAN, INF, -INF] as $value) { + foreach ([0, 1, -1, 42, PHP_INT_MAX, PHP_INT_MIN, PHP_INT_MAX - 1, PHP_INT_MIN + 1, 0.0, 0.5, -0.5, 2.5, -2.5, 1e30, -1e30, 9.2233720368547758E+18, -9.2233720368547758E+18, 9.2e18, -9.2e18, (float) PHP_INT_MAX, (float) PHP_INT_MIN, PHP_INT_MAX + 1.0, 9223372036854774784.0, 2147483647.0, 2147483646.5, 2147483647.5, -2147483648.0, -2147483648.5, NAN, INF, -INF] as $value) { $key = is_float($value) ? var_export($value, true) : (string) $value; foreach (['createAllSmallerThan', 'createAllSmallerThanOrEqualTo', 'createAllGreaterThan', 'createAllGreaterThanOrEqualTo'] as $factory) { $r["$factory $key"] = @$view($rangeClass::$factory($value)); } } - foreach (['5', true, null] as $value) { + foreach (['5', '1e30', '-1e30', true, null] as $value) { foreach (['createAllSmallerThan', 'createAllGreaterThanOrEqualTo'] as $factory) { try { $r["$factory " . var_export($value, true)] = $view($rangeClass::$factory($value)); From 943c36556e621a8e2c367ed4f2af01cb342f691d Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Mon, 2 Nov 2026 05:36:24 +0900 Subject: [PATCH 3/4] Compare ints with a float bound the way PHP does in IntegerRangeType 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 --- src/Type/IntegerRangeType.php | 88 +++---- .../integer-range-float-two-to-the-63.php | 34 ++- tests/PHPStan/Type/IntegerRangeTypeTest.php | 147 ++++++++--- turbo-ext/src/IntegerRangeType.cpp | 192 +++++--------- turbo-ext/src/generated/IntegerRangeType.h | 244 +++++++++--------- turbo-ext/tests/type-family.php | 15 +- 6 files changed, 392 insertions(+), 328 deletions(-) diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index 518d5d00993..c14aef9b17c 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -9,6 +9,7 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; @@ -18,9 +19,7 @@ use function array_filter; use function array_map; use function assert; -use function ceil; use function count; -use function floor; use function get_class; use function is_float; use function is_int; @@ -35,9 +34,6 @@ class IntegerRangeType extends IntegerType implements CompoundType { - /** The first float above PHP_INT_MAX: 2^63 on 64-bit builds, 2^31 on 32-bit ones, exact on both. */ - private const FLOAT_ABOVE_INT_MAX = PHP_INT_MAX + 1.0; - private function __construct(private ?int $min, private ?int $max) { parent::__construct(); @@ -95,19 +91,7 @@ public static function createAllSmallerThan($value): Type return self::fromInterval(null, $value, -1); } - // decided on the ceil() about to be cast: comparing $value with PHP_INT_MAX - // converts PHP_INT_MAX to float, which rounds it up to 2^63 on 64-bit builds - // but keeps it exact on 32-bit ones, where 2147483647.0 is still in range - $ceil = ceil($value); - if ($ceil >= self::FLOAT_ABOVE_INT_MAX) { - return new IntegerType(); - } - - if ($value <= PHP_INT_MIN) { - return new NeverType(); - } - - return self::fromInterval(null, (int) $ceil, -1); + return self::fromFloatComparison($value, '<'); } /** @@ -121,15 +105,7 @@ public static function createAllSmallerThanOrEqualTo($value): Type return self::fromInterval(null, $value); } - if ($value >= PHP_INT_MAX) { - return new IntegerType(); - } - - if ($value < PHP_INT_MIN) { - return new NeverType(); - } - - return self::fromInterval(null, (int) floor($value)); + return self::fromFloatComparison($value, '<='); } /** @@ -143,15 +119,7 @@ public static function createAllGreaterThan($value): Type return self::fromInterval($value, null, 1); } - if ($value < PHP_INT_MIN) { - return new IntegerType(); - } - - if ($value >= PHP_INT_MAX) { - return new NeverType(); - } - - return self::fromInterval((int) floor($value), null, 1); + return self::fromFloatComparison($value, '>'); } /** @@ -165,17 +133,49 @@ public static function createAllGreaterThanOrEqualTo($value): Type return self::fromInterval($value, null); } - if ($value <= PHP_INT_MIN) { - return new IntegerType(); - } + return self::fromFloatComparison($value, '>='); + } - // decided on the ceil() about to be cast, like createAllSmallerThan() - $ceil = ceil($value); - if ($ceil >= self::FLOAT_ABOVE_INT_MAX) { - return new NeverType(); + /** + * The ints for which `$int <$operator> $value` holds. PHP compares an int with a float by converting the + * int to float, which rounds ints beyond 2^53 on 64-bit builds (PHP_INT_MAX >= 2.0 ** 63 holds), so the + * bound cannot be derived from ceil() or floor() of $value. It is searched for with the comparison itself. + * + * @param string $operator '<', '<=', '>' or '>=' + */ + private static function fromFloatComparison(float $value, string $operator): Type + { + $holds = match ($operator) { + '<' => static fn (int $int): bool => $int < $value, + '<=' => static fn (int $int): bool => $int <= $value, + '>' => static fn (int $int): bool => $int > $value, + '>=' => static fn (int $int): bool => $int >= $value, + default => throw new ShouldNotHappenException(), + }; + + $holdsAtMin = $holds(PHP_INT_MIN); + if ($holdsAtMin === $holds(PHP_INT_MAX)) { + // also a NaN $value, which no comparison holds for + return $holdsAtMin ? new IntegerType() : new NeverType(); + } + + // $holds() gives $holdsAtMin for $low and the opposite for $high + $low = PHP_INT_MIN; + $high = PHP_INT_MAX; + while (true) { + // floor(($low + $high) / 2) without overflowing + $middle = ($low >> 1) + ($high >> 1) + ($low & $high & 1); + if ($middle === $low) { + break; + } + if ($holds($middle) === $holdsAtMin) { + $low = $middle; + } else { + $high = $middle; + } } - return self::fromInterval((int) $ceil, null); + return $holdsAtMin ? self::fromInterval(null, $low) : self::fromInterval($high, null); } public function getMin(): ?int diff --git a/tests/PHPStan/Analyser/nsrt/integer-range-float-two-to-the-63.php b/tests/PHPStan/Analyser/nsrt/integer-range-float-two-to-the-63.php index b914af2df34..2c172c37d64 100644 --- a/tests/PHPStan/Analyser/nsrt/integer-range-float-two-to-the-63.php +++ b/tests/PHPStan/Analyser/nsrt/integer-range-float-two-to-the-63.php @@ -4,12 +4,14 @@ use function PHPStan\Testing\assertType; +// PHP compares an int with a float as two floats, and the ints from 2**63 - 512 up convert to 2**63 + function smaller(int $i): void { if ($i < 9.2233720368547758E18) { - assertType('int', $i); + assertType('int', $i); } else { - assertType('*NEVER*', $i); + assertType('int<9223372036854775296, max>', $i); } assertType('int', $i); } @@ -17,9 +19,9 @@ function smaller(int $i): void function greaterOrEqual(int $i): void { if ($i >= 9.2233720368547758E18) { - assertType('*NEVER*', $i); + assertType('int<9223372036854775296, max>', $i); } else { - assertType('int', $i); + assertType('int', $i); } assertType('int', $i); } @@ -41,3 +43,27 @@ function greater(int $i): void assertType('int', $i); } } + +function intMax(): void +{ + assertType('true', 9223372036854775807 >= 9.2233720368547758E18); + assertType('false', 9223372036854775807 < 9.2233720368547758E18); + assertType('true', 9223372036854775296 >= 9.2233720368547758E18); + assertType('false', 9223372036854775295 >= 9.2233720368547758E18); +} + +// 2**53 + 3 and 2**53 + 5 both round to 2**53 + 4 + +function roundedToEven(int $i): void +{ + if ($i < 9007199254740996.0) { + assertType('int', $i); + } else { + assertType('int<9007199254740995, max>', $i); + } + if ($i <= 9007199254740996.0) { + assertType('int', $i); + } else { + assertType('int<9007199254740998, max>', $i); + } +} diff --git a/tests/PHPStan/Type/IntegerRangeTypeTest.php b/tests/PHPStan/Type/IntegerRangeTypeTest.php index e00d2c5224f..17bb4eacec0 100644 --- a/tests/PHPStan/Type/IntegerRangeTypeTest.php +++ b/tests/PHPStan/Type/IntegerRangeTypeTest.php @@ -3,10 +3,14 @@ namespace PHPStan\Type; use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPUnit\Framework\Attributes\DataProvider; use RuntimeException; use function restore_error_handler; use function set_error_handler; +use function sprintf; +use const INF; +use const NAN; use const PHP_INT_MAX; use const PHP_INT_MIN; use const PHP_INT_SIZE; @@ -14,32 +18,34 @@ class IntegerRangeTypeTest extends PHPStanTestCase { + private const PROBED_INTS_AROUND_BOUND = 2048; + public static function dataCreateFromFloat(): iterable { - // 2**63 is PHP_INT_MAX + 1, the smallest float greater than every int + // PHP compares an int with a float as two floats, and the ints from 2**63 - 512 up convert to 2**63 yield '2**63' => [ 9.2233720368547758E18, + 'int', 'int', - 'int', - '*NEVER*', '*NEVER*', + 'int<9223372036854775296, max>', ]; - // the largest float smaller than 2**63 + // the largest float smaller than 2**63, which the ints from 2**63 - 1535 to 2**63 - 513 convert to yield '2**63 - 1024' => [ 9.2233720368547748E18, - 'int', - 'int', - 'int<9223372036854774785, max>', - 'int<9223372036854774784, max>', + 'int', + 'int', + 'int<9223372036854775296, max>', + 'int<9223372036854774273, max>', ]; - // -2**63 is exactly PHP_INT_MIN + // -2**63 is exactly PHP_INT_MIN, and the ints up to -2**63 + 512 convert to it yield '-2**63' => [ -9.2233720368547758E18, '*NEVER*', - '-9223372036854775808', - 'int<-9223372036854775807, max>', + 'int', + 'int<-9223372036854775295, max>', 'int', ]; @@ -51,6 +57,24 @@ public static function dataCreateFromFloat(): iterable 'int', 'int', ]; + + // the first float whose neighbours are 2 apart: 2**53 + 1 rounds down to it (to even) + yield '2**53' => [ + 9007199254740992.0, + 'int', + 'int', + 'int<9007199254740994, max>', + 'int<9007199254740992, max>', + ]; + + // 2**53 + 3 and 2**53 + 5 both round to 2**53 + 4 (to even) + yield '2**53 + 4' => [ + 9007199254740996.0, + 'int', + 'int', + 'int<9007199254740998, max>', + 'int<9007199254740995, max>', + ]; } #[DataProvider('dataCreateFromFloat')] @@ -86,35 +110,25 @@ public static function dataFloatBounds(): iterable $greaterThan = static fn (float $value): Type => IntegerRangeType::createAllGreaterThan($value); $greaterThanOrEqualTo = static fn (float $value): Type => IntegerRangeType::createAllGreaterThanOrEqualTo($value); - // PHP_INT_MAX + 1.0 is the first float above PHP_INT_MAX on any int width, and - // (float) PHP_INT_MIN is exact on any int width - $aboveMax = PHP_INT_MAX + 1.0; - yield [$smallerThan, $aboveMax, 'int']; - yield [$smallerThanOrEqualTo, $aboveMax, 'int']; - yield [$greaterThan, $aboveMax, '*NEVER*']; - yield [$greaterThanOrEqualTo, $aboveMax, '*NEVER*']; - yield [$smallerThan, (float) PHP_INT_MIN, '*NEVER*']; - yield [$smallerThanOrEqualTo, (float) PHP_INT_MIN, (string) PHP_INT_MIN]; - yield [$greaterThan, (float) PHP_INT_MIN, 'int<' . (PHP_INT_MIN + 1) . ', max>']; - yield [$greaterThanOrEqualTo, (float) PHP_INT_MIN, 'int']; - - // (float) PHP_INT_MAX is 2^63 on 64-bit builds (above every int) and exact on 32-bit - // ones; so is PHP_INT_MAX - 0.5, which is PHP_INT_MAX - 1 + 0.5 on 32-bit builds - $maxIsExact = PHP_INT_SIZE < 8; - yield [$smallerThan, (float) PHP_INT_MAX, $maxIsExact ? 'int' : 'int']; - yield [$smallerThanOrEqualTo, (float) PHP_INT_MAX, 'int']; - yield [$greaterThan, (float) PHP_INT_MAX, '*NEVER*']; - yield [$greaterThanOrEqualTo, (float) PHP_INT_MAX, $maxIsExact ? (string) PHP_INT_MAX : '*NEVER*']; - yield [$smallerThan, PHP_INT_MAX - 0.5, $maxIsExact ? 'int' : 'int']; - yield [$greaterThanOrEqualTo, PHP_INT_MAX - 0.5, $maxIsExact ? (string) PHP_INT_MAX : '*NEVER*']; - - // between PHP_INT_MAX and the next int on 32-bit builds, 2^63 on 64-bit ones: its - // ceil() is past the int range either way - yield [$smallerThan, PHP_INT_MAX + 0.5, 'int']; - yield [$greaterThanOrEqualTo, PHP_INT_MAX + 0.5, '*NEVER*']; + // no int compares with NaN + yield [$smallerThan, NAN, '*NEVER*']; + yield [$smallerThanOrEqualTo, NAN, '*NEVER*']; + yield [$greaterThan, NAN, '*NEVER*']; + yield [$greaterThanOrEqualTo, NAN, '*NEVER*']; + + yield [$smallerThan, INF, 'int']; + yield [$smallerThanOrEqualTo, INF, 'int']; + yield [$greaterThan, INF, '*NEVER*']; + yield [$greaterThanOrEqualTo, INF, '*NEVER*']; + yield [$smallerThan, -INF, '*NEVER*']; + yield [$greaterThanOrEqualTo, -INF, 'int']; yield [$smallerThan, 2.5, 'int']; + yield [$smallerThanOrEqualTo, 2.5, 'int']; yield [$greaterThan, 2.5, 'int<3, max>']; + yield [$greaterThanOrEqualTo, 2.5, 'int<3, max>']; + yield [$smallerThan, -2.0, 'int']; + yield [$greaterThanOrEqualTo, -2.0, 'int<-2, max>']; } /** @@ -133,4 +147,63 @@ public function testFloatBounds(callable $factory, float $value, string $expecte } } + public static function dataMatchesPhpComparison(): iterable + { + // floats whose int bounds depend on the int width: (float) PHP_INT_MAX is 2**63 on 64-bit + // builds and exact on 32-bit ones, and so is PHP_INT_MAX - 0.5 on 32-bit ones + $values = [ + '(float) PHP_INT_MAX' => (float) PHP_INT_MAX, + 'PHP_INT_MAX - 0.5' => PHP_INT_MAX - 0.5, + 'PHP_INT_MAX + 0.5' => PHP_INT_MAX + 0.5, + '(float) PHP_INT_MIN' => (float) PHP_INT_MIN, + 'PHP_INT_MIN + 0.5' => PHP_INT_MIN + 0.5, + 'PHP_INT_MIN - 0.5' => PHP_INT_MIN - 0.5, + '2**31' => 2147483648.0, + '2**53 + 4' => 9007199254740996.0, + '-2**53 - 4' => -9007199254740996.0, + ]; + foreach ($values as $label => $value) { + foreach (['<', '<=', '>', '>='] as $operator) { + yield sprintf('$int %s %s', $operator, $label) => [$operator, $value]; + } + } + } + + /** + * @param '<'|'<='|'>'|'>=' $operator + */ + #[DataProvider('dataMatchesPhpComparison')] + public function testMatchesPhpComparison(string $operator, float $value): void + { + [$type, $compare] = match ($operator) { + '<' => [IntegerRangeType::createAllSmallerThan($value), static fn (int $int): bool => $int < $value], + '<=' => [IntegerRangeType::createAllSmallerThanOrEqualTo($value), static fn (int $int): bool => $int <= $value], + '>' => [IntegerRangeType::createAllGreaterThan($value), static fn (int $int): bool => $int > $value], + '>=' => [IntegerRangeType::createAllGreaterThanOrEqualTo($value), static fn (int $int): bool => $int >= $value], + }; + + // a comparison with a float flips at most once, near the float or at an end of the int range; + // the ints converting to one float near 2**63 span 1024 of them + $centers = [PHP_INT_MIN, PHP_INT_MAX]; + if ($value > PHP_INT_MIN && $value < PHP_INT_MAX) { + $centers[] = (int) $value; + } + $mismatches = []; + foreach ($centers as $center) { + // clamped to the int range without overflowing into float + $from = $center < PHP_INT_MIN + self::PROBED_INTS_AROUND_BOUND ? PHP_INT_MIN : $center - self::PROBED_INTS_AROUND_BOUND; + $to = $center > PHP_INT_MAX - self::PROBED_INTS_AROUND_BOUND ? PHP_INT_MAX : $center + self::PROBED_INTS_AROUND_BOUND; + for ($int = $from;; $int++) { + if ($type->isSuperTypeOf(new ConstantIntegerType($int))->yes() !== $compare($int)) { + $mismatches[] = $int; + } + if ($int === $to) { + break; + } + } + } + + $this->assertSame([], $mismatches, $type->describe(VerbosityLevel::precise())); + } + } diff --git a/turbo-ext/src/IntegerRangeType.cpp b/turbo-ext/src/IntegerRangeType.cpp index 5bd8042413e..f0be460d0e6 100644 --- a/turbo-ext/src/IntegerRangeType.cpp +++ b/turbo-ext/src/IntegerRangeType.cpp @@ -23,14 +23,8 @@ namespace slots = ptdecl::IntegerRangeType::slot; namespace sigs = ptdecl::IntegerRangeType::sig; -#include - zend_class_entry *pt_ce_integer_range_type = nullptr; -/* the twin's private const FLOAT_ABOVE_INT_MAX = PHP_INT_MAX + 1.0: the first - * double above ZEND_LONG_MAX, exact on 64-bit (2^63) and 32-bit (2^31) builds */ -static constexpr double PT_IRT_FLOAT_ABOVE_INT_MAX = (double) ZEND_LONG_MAX + 1.0; - /* nothing is memoized per request since InitializerExprTypeResolver's * CALCULATE_SCALARS_LIMIT became a native constant */ void pt_integer_range_type_rinit() @@ -174,78 +168,73 @@ class IntegerRangeType } /* the createAll*() family: the range of integers beyond an int|float - * $value. An int goes straight to fromInterval(); anything else is - * held against the int limits — a float the way the VM compares int - * with float (both as doubles), any other value loosely — and rounded - * by ceil()/floor() and (int)-cast, which is where a non-number fails - * under strict_types. Where the ceil() is held against the max instead - * of $value (createAllSmallerThan(), createAllGreaterThanOrEqualTo()), - * it is taken first, in the twin's order. */ + * $value. An int goes straight to fromInterval(); anything else goes to + * fromFloatComparison(), whose float parameter is where a non-float fails + * under strict_types. */ /* fromInterval(null, $value, -1) */ static zv::Val createAllSmallerThan(zval *value) { if (Z_TYPE_P(value) == IS_LONG) return fromInterval(NullableLong::null(), NullableLong::of(Z_LVAL_P(value)), -1); - double ceiled; - if (UNEXPECTED(!roundToDouble(value, true, ceiled))) return zv::Val(); - if (ceiled >= PT_IRT_FLOAT_ABOVE_INT_MAX) { /* $ceil >= self::FLOAT_ABOVE_INT_MAX */ - return integer(); - } - if (Z_DVAL_P(value) <= (double) ZEND_LONG_MIN) { /* $value <= PHP_INT_MIN */ - return never(); - } - return fromInterval(NullableLong::null(), NullableLong::of(zend_dval_to_lval(ceiled)), -1); + return fromFloatComparison(value, [](zend_long i, double f) { return (double) i < f; }); /* fromFloatComparison($value, '<') */ } /* fromInterval(null, $value) */ static zv::Val createAllSmallerThanOrEqualTo(zval *value) { if (Z_TYPE_P(value) == IS_LONG) return fromInterval(NullableLong::null(), NullableLong::of(Z_LVAL_P(value)), 0); - int againstMax, againstMin; - if (UNEXPECTED(!compareToLimits(value, againstMax, againstMin))) return zv::Val(); - if (againstMax >= 0) { /* $value >= PHP_INT_MAX */ - return integer(); - } - if (againstMin < 0) { /* $value < PHP_INT_MIN */ - return never(); - } - zend_long rounded; - if (UNEXPECTED(!roundToLong(value, false, rounded))) return zv::Val(); - return fromInterval(NullableLong::null(), NullableLong::of(rounded), 0); + return fromFloatComparison(value, [](zend_long i, double f) { return (double) i <= f; }); /* fromFloatComparison($value, '<=') */ } /* fromInterval($value, null, 1) */ static zv::Val createAllGreaterThan(zval *value) { if (Z_TYPE_P(value) == IS_LONG) return fromInterval(NullableLong::of(Z_LVAL_P(value)), NullableLong::null(), 1); - int againstMax, againstMin; - if (UNEXPECTED(!compareToLimits(value, againstMax, againstMin))) return zv::Val(); - if (againstMin < 0) { /* $value < PHP_INT_MIN */ - return integer(); - } - if (againstMax >= 0) { /* $value >= PHP_INT_MAX */ - return never(); - } - zend_long rounded; - if (UNEXPECTED(!roundToLong(value, false, rounded))) return zv::Val(); - return fromInterval(NullableLong::of(rounded), NullableLong::null(), 1); + return fromFloatComparison(value, [](zend_long i, double f) { return (double) i > f; }); /* fromFloatComparison($value, '>') */ } /* fromInterval($value, null) */ static zv::Val createAllGreaterThanOrEqualTo(zval *value) { if (Z_TYPE_P(value) == IS_LONG) return fromInterval(NullableLong::of(Z_LVAL_P(value)), NullableLong::null(), 0); - int againstMin; - if (UNEXPECTED(!compareToMin(value, againstMin))) return zv::Val(); - if (againstMin <= 0) { /* $value <= PHP_INT_MIN */ - return integer(); + return fromFloatComparison(value, [](zend_long i, double f) { return (double) i >= f; }); /* fromFloatComparison($value, '>=') */ + } + + /* fromFloatComparison($value, $operator): the ints for which + * `$int <$operator> $value` holds. holds() is the VM's int-with-float + * comparison, both sides as doubles, which rounds ints beyond 2^53 on + * 64-bit builds; the bound is bisected with it, in the twin's order. */ + template + static zv::Val fromFloatComparison(zval *value, Holds holds) + { + if (UNEXPECTED(Z_TYPE_P(value) != IS_DOUBLE)) { + zend_type_error("PHPStan\\Type\\IntegerRangeType::fromFloatComparison(): Argument #1 ($value) must be of type float, %s given", zend_zval_value_name(value)); + return zv::Val(); + } + double f = Z_DVAL_P(value); + bool holdsAtMin = holds(ZEND_LONG_MIN, f); + if (holdsAtMin == holds(ZEND_LONG_MAX, f)) { + /* also a NAN $value, which no comparison holds for */ + return holdsAtMin ? integer() : never(); } - double ceiled; - if (UNEXPECTED(!roundToDouble(value, true, ceiled))) return zv::Val(); - if (ceiled >= PT_IRT_FLOAT_ABOVE_INT_MAX) { /* $ceil >= self::FLOAT_ABOVE_INT_MAX */ - return never(); + + /* holds() gives holdsAtMin for low and the opposite for high */ + zend_long low = ZEND_LONG_MIN; + zend_long high = ZEND_LONG_MAX; + while (true) { + /* floor((low + high) / 2) without overflowing */ + zend_long middle = (low >> 1) + (high >> 1) + (low & high & 1); + if (middle == low) break; + if (holds(middle, f) == holdsAtMin) { + low = middle; + } else { + high = middle; + } } - return fromInterval(NullableLong::of(zend_dval_to_lval(ceiled)), NullableLong::null(), 0); + + return holdsAtMin + ? fromInterval(NullableLong::null(), NullableLong::of(low), 0) + : fromInterval(NullableLong::of(high), NullableLong::null(), 0); } /* $this->min / $this->max; false with an Error pending when the @@ -1123,69 +1112,6 @@ class IntegerRangeType return pt_type_call(Z_OBJ_P(result.raw()), PT_LC("toacceptsresult"), 0, NULL); } - /* the three-way comparisons of a non-int $value against PHP_INT_MAX and - * PHP_INT_MIN — the callers read `>`, `>=`, `<`, `<=` off the sign. A - * float compares as the VM compares int with float, both sides as - * doubles — so NAN is neither above nor below either limit, on either - * side (a NAN answer of -1 against the max and 1 against the min keeps - * every one of the four tests false); anything else compares loosely. - * false = pending exception */ - [[nodiscard]] static bool compareToLimits(zval *value, int &againstMax, int &againstMin) - { - return compareToMax(value, againstMax) && compareToMin(value, againstMin); - } - - [[nodiscard]] static bool compareToMax(zval *value, int &againstMax) - { - if (EXPECTED(Z_TYPE_P(value) == IS_DOUBLE)) { - double d = Z_DVAL_P(value); - double max = (double) ZEND_LONG_MAX; - againstMax = d > max ? 1 : (d < max ? -1 : (d == max ? 0 : -1)); - return true; - } - zval limit; - ZVAL_LONG(&limit, ZEND_LONG_MAX); - againstMax = zend_compare(value, &limit); - return !EG(exception); - } - - [[nodiscard]] static bool compareToMin(zval *value, int &againstMin) - { - if (EXPECTED(Z_TYPE_P(value) == IS_DOUBLE)) { - double d = Z_DVAL_P(value); - double min = (double) ZEND_LONG_MIN; - againstMin = d < min ? -1 : (d > min ? 1 : (d == min ? 0 : 1)); - return true; - } - zval limit; - ZVAL_LONG(&limit, ZEND_LONG_MIN); - againstMin = zend_compare(value, &limit); - return !EG(exception); - } - - /* ceil($value) / floor($value) for a non-int $value: a float rounds; - * anything else is what ceil()/floor() reject under strict_types. - * false = pending exception */ - [[nodiscard]] static bool roundToDouble(zval *value, bool useCeil, double &out) - { - if (UNEXPECTED(Z_TYPE_P(value) != IS_DOUBLE)) { - zend_type_error("%s(): Argument #1 ($num) must be of type int|float, %s given", useCeil ? "ceil" : "floor", zend_zval_value_name(value)); - return false; - } - out = useCeil ? ceil(Z_DVAL_P(value)) : floor(Z_DVAL_P(value)); - return true; - } - - /* (int) ceil($value) / (int) floor($value) for a non-int $value, cast as - * the engine casts. false = pending exception */ - [[nodiscard]] static bool roundToLong(zval *value, bool useCeil, zend_long &out) - { - double rounded; - if (UNEXPECTED(!roundToDouble(value, useCeil, rounded))) return false; - out = zend_dval_to_lval(rounded); - return true; - } - /* $base ** $exponent — int, or float on overflow / negative exponent; * false = pending exception */ [[nodiscard]] static bool phpPow(zend_long base, zend_long exponent, zval *out) @@ -1362,11 +1288,6 @@ static void pt_irt_bound(INTERNAL_FUNCTION_PARAMETERS, bool (IntegerRangeType::* RETURN_LONG(bound.value); } -static void pt_irt_float_above_int_max_constant(zval *out) -{ - ZVAL_DOUBLE(out, PT_IRT_FLOAT_ABOVE_INT_MAX); -} - PT_MINIT_REGISTRATION(pt_register_integer_range_type) { reg::Class cls("PHPStan\\Type\\IntegerRangeType"); @@ -1374,7 +1295,6 @@ PT_MINIT_REGISTRATION(pt_register_integer_range_type) /* "min" and "max" must stay the first two declared properties * (slots::min, slots::max) */ ptdecl::IntegerRangeType::declareProperties(cls); - cls.privateClassConstantValue("FLOAT_ABOVE_INT_MAX", pt_irt_float_above_int_max_constant); cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) { zend_long min, max; @@ -1435,6 +1355,34 @@ PT_MINIT_REGISTRATION(pt_register_integer_range_type) pt_irt_create_all(INTERNAL_FUNCTION_PARAM_PASSTHRU, IntegerRangeType::createAllGreaterThanOrEqualTo); }); + /* only reflection reaches the private twin: the factories above bisect + * natively. An unknown $operator fails at the first comparison, where the + * twin's match() reaches its default arm */ + cls.method(sigs::fromFloatComparison, [](INTERNAL_FUNCTION_PARAMETERS) { + double value; + zend_string *op; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_DOUBLE(value) + Z_PARAM_STR(op) + ZEND_PARSE_PARAMETERS_END(); + zval boxed; + ZVAL_DOUBLE(&boxed, value); + if (zend_string_equals_literal(op, "<")) { + PT_RETURN_VAL(IntegerRangeType::fromFloatComparison(&boxed, [](zend_long i, double f) { return (double) i < f; })); + } + if (zend_string_equals_literal(op, "<=")) { + PT_RETURN_VAL(IntegerRangeType::fromFloatComparison(&boxed, [](zend_long i, double f) { return (double) i <= f; })); + } + if (zend_string_equals_literal(op, ">")) { + PT_RETURN_VAL(IntegerRangeType::fromFloatComparison(&boxed, [](zend_long i, double f) { return (double) i > f; })); + } + if (zend_string_equals_literal(op, ">=")) { + PT_RETURN_VAL(IntegerRangeType::fromFloatComparison(&boxed, [](zend_long i, double f) { return (double) i >= f; })); + } + pt_throw_should_not_happen(); + RETURN_THROWS(); + }); + cls.method(sigs::getMin, [](INTERNAL_FUNCTION_PARAMETERS) { pt_irt_bound(INTERNAL_FUNCTION_PARAM_PASSTHRU, &IntegerRangeType::getMin); }); diff --git a/turbo-ext/src/generated/IntegerRangeType.h b/turbo-ext/src/generated/IntegerRangeType.h index a2d7db5f18b..108bfbb05e6 100644 --- a/turbo-ext/src/generated/IntegerRangeType.h +++ b/turbo-ext/src/generated/IntegerRangeType.h @@ -50,54 +50,56 @@ inline constexpr char strings[] = "createAllSmallerThanOrEqualTo\0" /* 142 */ "createAllGreaterThan\0" /* 172 */ "createAllGreaterThanOrEqualTo\0" /* 193 */ - "getMin\0" /* 223 */ - "getMax\0" /* 230 */ - "level\0" /* 237 */ - "PHPStan\\Type\\VerbosityLevel\0" /* 243 */ - "describe\0" /* 271 */ - "amount\0" /* 280 */ - "type\0" /* 287 */ - "strictTypes\0" /* 292 */ - "PHPStan\\Type\\AcceptsResult\0" /* 304 */ - "accepts\0" /* 331 */ - "PHPStan\\Type\\IsSuperTypeOfResult\0" /* 339 */ - "isSuperTypeOf\0" /* 372 */ - "otherType\0" /* 386 */ - "isSubTypeOf\0" /* 396 */ - "PHPStan\\Type\\UnionType\0" /* 408 */ - "isSubTypeOfUnionWithReason\0" /* 431 */ - "acceptingType\0" /* 458 */ - "isAcceptedBy\0" /* 472 */ - "equals\0" /* 485 */ - "precision\0" /* 492 */ - "PHPStan\\Type\\GeneralizePrecision\0" /* 502 */ - "generalize\0" /* 535 */ - "phpVersion\0" /* 546 */ - "PHPStan\\Php\\PhpVersion\0" /* 557 */ - "PHPStan\\TrinaryLogic\0" /* 580 */ - "isSmallerThan\0" /* 601 */ - "isSmallerThanOrEqual\0" /* 615 */ - "isGreaterThan\0" /* 636 */ - "isGreaterThanOrEqual\0" /* 650 */ - "getSmallerType\0" /* 671 */ - "getSmallerOrEqualType\0" /* 686 */ - "getGreaterType\0" /* 708 */ - "getGreaterOrEqualType\0" /* 723 */ - "PHPStan\\Type\\BooleanType\0" /* 745 */ - "toBoolean\0" /* 770 */ - "toAbsoluteNumber\0" /* 780 */ - "toBitwiseNotType\0" /* 797 */ - "toString\0" /* 814 */ - "tryUnion\0" /* 823 */ - "tryIntersect\0" /* 832 */ - "typeToRemove\0" /* 845 */ - "tryRemove\0" /* 858 */ - "exponent\0" /* 868 */ - "exponentiate\0" /* 877 */ - "getFiniteTypes\0" /* 890 */ - "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 905 */ - "toPhpDocNode\0" /* 944 */ - "looseCompare"; /* 957 */ + "operator\0" /* 223 */ + "fromFloatComparison\0" /* 232 */ + "getMin\0" /* 252 */ + "getMax\0" /* 259 */ + "level\0" /* 266 */ + "PHPStan\\Type\\VerbosityLevel\0" /* 272 */ + "describe\0" /* 300 */ + "amount\0" /* 309 */ + "type\0" /* 316 */ + "strictTypes\0" /* 321 */ + "PHPStan\\Type\\AcceptsResult\0" /* 333 */ + "accepts\0" /* 360 */ + "PHPStan\\Type\\IsSuperTypeOfResult\0" /* 368 */ + "isSuperTypeOf\0" /* 401 */ + "otherType\0" /* 415 */ + "isSubTypeOf\0" /* 425 */ + "PHPStan\\Type\\UnionType\0" /* 437 */ + "isSubTypeOfUnionWithReason\0" /* 460 */ + "acceptingType\0" /* 487 */ + "isAcceptedBy\0" /* 501 */ + "equals\0" /* 514 */ + "precision\0" /* 521 */ + "PHPStan\\Type\\GeneralizePrecision\0" /* 531 */ + "generalize\0" /* 564 */ + "phpVersion\0" /* 575 */ + "PHPStan\\Php\\PhpVersion\0" /* 586 */ + "PHPStan\\TrinaryLogic\0" /* 609 */ + "isSmallerThan\0" /* 630 */ + "isSmallerThanOrEqual\0" /* 644 */ + "isGreaterThan\0" /* 665 */ + "isGreaterThanOrEqual\0" /* 679 */ + "getSmallerType\0" /* 700 */ + "getSmallerOrEqualType\0" /* 715 */ + "getGreaterType\0" /* 737 */ + "getGreaterOrEqualType\0" /* 752 */ + "PHPStan\\Type\\BooleanType\0" /* 774 */ + "toBoolean\0" /* 799 */ + "toAbsoluteNumber\0" /* 809 */ + "toBitwiseNotType\0" /* 826 */ + "toString\0" /* 843 */ + "tryUnion\0" /* 852 */ + "tryIntersect\0" /* 861 */ + "typeToRemove\0" /* 874 */ + "tryRemove\0" /* 887 */ + "exponent\0" /* 897 */ + "exponentiate\0" /* 906 */ + "getFiniteTypes\0" /* 919 */ + "PHPStan\\PhpDocParser\\Ast\\Type\\TypeNode\0" /* 934 */ + "toPhpDocNode\0" /* 973 */ + "looseCompare"; /* 986 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_NULL | MAY_BE_LONG), /* __construct $min */ reg::packed(4, MAY_BE_NULL | MAY_BE_LONG), /* __construct $max */ @@ -119,65 +121,68 @@ inline constexpr reg::PackedArg args[] = { reg::packed(28, 0, 29), /* createAllGreaterThan return */ reg::packed(115, 0), /* createAllGreaterThanOrEqualTo $value */ reg::packed(28, 0, 29), /* createAllGreaterThanOrEqualTo return */ + reg::packed(115, MAY_BE_DOUBLE), /* fromFloatComparison $value */ + reg::packed(223, MAY_BE_STRING), /* fromFloatComparison $operator */ + reg::packed(28, 0, 29), /* fromFloatComparison return */ reg::packed(28, MAY_BE_NULL | MAY_BE_LONG), /* getMin return */ reg::packed(28, MAY_BE_NULL | MAY_BE_LONG), /* getMax return */ - reg::packed(237, 0, 243), /* describe $level */ + reg::packed(266, 0, 272), /* describe $level */ reg::packed(28, MAY_BE_STRING), /* describe return */ - reg::packed(280, MAY_BE_LONG), /* shift $amount */ + reg::packed(309, MAY_BE_LONG), /* shift $amount */ reg::packed(28, 0, 29), /* shift return */ - reg::packed(287, 0, 29), /* accepts $type */ - reg::packed(292, MAY_BE_BOOL), /* accepts $strictTypes */ - reg::packed(28, 0, 304), /* accepts return */ - reg::packed(287, 0, 29), /* isSuperTypeOf $type */ - reg::packed(28, 0, 339), /* isSuperTypeOf return */ - reg::packed(386, 0, 29), /* isSubTypeOf $otherType */ - reg::packed(28, 0, 339), /* isSubTypeOf return */ - reg::packed(386, 0, 408), /* isSubTypeOfUnionWithReason $otherType */ - reg::packed(28, 0, 339), /* isSubTypeOfUnionWithReason return */ - reg::packed(458, 0, 29), /* isAcceptedBy $acceptingType */ - reg::packed(292, MAY_BE_BOOL), /* isAcceptedBy $strictTypes */ - reg::packed(28, 0, 304), /* isAcceptedBy return */ - reg::packed(287, 0, 29), /* equals $type */ + reg::packed(316, 0, 29), /* accepts $type */ + reg::packed(321, MAY_BE_BOOL), /* accepts $strictTypes */ + reg::packed(28, 0, 333), /* accepts return */ + reg::packed(316, 0, 29), /* isSuperTypeOf $type */ + reg::packed(28, 0, 368), /* isSuperTypeOf return */ + reg::packed(415, 0, 29), /* isSubTypeOf $otherType */ + reg::packed(28, 0, 368), /* isSubTypeOf return */ + reg::packed(415, 0, 437), /* isSubTypeOfUnionWithReason $otherType */ + reg::packed(28, 0, 368), /* isSubTypeOfUnionWithReason return */ + reg::packed(487, 0, 29), /* isAcceptedBy $acceptingType */ + reg::packed(321, MAY_BE_BOOL), /* isAcceptedBy $strictTypes */ + reg::packed(28, 0, 333), /* isAcceptedBy return */ + reg::packed(316, 0, 29), /* equals $type */ reg::packed(28, MAY_BE_BOOL), /* equals return */ - reg::packed(492, 0, 502), /* generalize $precision */ + reg::packed(521, 0, 531), /* generalize $precision */ reg::packed(28, 0, 29), /* generalize return */ - reg::packed(386, 0, 29), /* isSmallerThan $otherType */ - reg::packed(546, 0, 557), /* isSmallerThan $phpVersion */ - reg::packed(28, 0, 580), /* isSmallerThan return */ - reg::packed(386, 0, 29), /* isSmallerThanOrEqual $otherType */ - reg::packed(546, 0, 557), /* isSmallerThanOrEqual $phpVersion */ - reg::packed(28, 0, 580), /* isSmallerThanOrEqual return */ - reg::packed(386, 0, 29), /* isGreaterThan $otherType */ - reg::packed(546, 0, 557), /* isGreaterThan $phpVersion */ - reg::packed(28, 0, 580), /* isGreaterThan return */ - reg::packed(386, 0, 29), /* isGreaterThanOrEqual $otherType */ - reg::packed(546, 0, 557), /* isGreaterThanOrEqual $phpVersion */ - reg::packed(28, 0, 580), /* isGreaterThanOrEqual return */ - reg::packed(546, 0, 557), /* getSmallerType $phpVersion */ + reg::packed(415, 0, 29), /* isSmallerThan $otherType */ + reg::packed(575, 0, 586), /* isSmallerThan $phpVersion */ + reg::packed(28, 0, 609), /* isSmallerThan return */ + reg::packed(415, 0, 29), /* isSmallerThanOrEqual $otherType */ + reg::packed(575, 0, 586), /* isSmallerThanOrEqual $phpVersion */ + reg::packed(28, 0, 609), /* isSmallerThanOrEqual return */ + reg::packed(415, 0, 29), /* isGreaterThan $otherType */ + reg::packed(575, 0, 586), /* isGreaterThan $phpVersion */ + reg::packed(28, 0, 609), /* isGreaterThan return */ + reg::packed(415, 0, 29), /* isGreaterThanOrEqual $otherType */ + reg::packed(575, 0, 586), /* isGreaterThanOrEqual $phpVersion */ + reg::packed(28, 0, 609), /* isGreaterThanOrEqual return */ + reg::packed(575, 0, 586), /* getSmallerType $phpVersion */ reg::packed(28, 0, 29), /* getSmallerType return */ - reg::packed(546, 0, 557), /* getSmallerOrEqualType $phpVersion */ + reg::packed(575, 0, 586), /* getSmallerOrEqualType $phpVersion */ reg::packed(28, 0, 29), /* getSmallerOrEqualType return */ - reg::packed(546, 0, 557), /* getGreaterType $phpVersion */ + reg::packed(575, 0, 586), /* getGreaterType $phpVersion */ reg::packed(28, 0, 29), /* getGreaterType return */ - reg::packed(546, 0, 557), /* getGreaterOrEqualType $phpVersion */ + reg::packed(575, 0, 586), /* getGreaterOrEqualType $phpVersion */ reg::packed(28, 0, 29), /* getGreaterOrEqualType return */ - reg::packed(28, 0, 745), /* toBoolean return */ + reg::packed(28, 0, 774), /* toBoolean return */ reg::packed(28, 0, 29), /* toAbsoluteNumber return */ reg::packed(28, 0, 29), /* toBitwiseNotType return */ reg::packed(28, 0, 29), /* toString return */ - reg::packed(386, 0, 29), /* tryUnion $otherType */ + reg::packed(415, 0, 29), /* tryUnion $otherType */ reg::packed(28, MAY_BE_NULL, 29), /* tryUnion return */ - reg::packed(386, 0, 29), /* tryIntersect $otherType */ + reg::packed(415, 0, 29), /* tryIntersect $otherType */ reg::packed(28, MAY_BE_NULL, 29), /* tryIntersect return */ - reg::packed(845, 0, 29), /* tryRemove $typeToRemove */ + reg::packed(874, 0, 29), /* tryRemove $typeToRemove */ reg::packed(28, MAY_BE_NULL, 29), /* tryRemove return */ - reg::packed(868, 0, 29), /* exponentiate $exponent */ + reg::packed(897, 0, 29), /* exponentiate $exponent */ reg::packed(28, 0, 29), /* exponentiate return */ reg::packed(28, MAY_BE_ARRAY), /* getFiniteTypes return */ - reg::packed(28, 0, 905), /* toPhpDocNode return */ - reg::packed(287, 0, 29), /* looseCompare $type */ - reg::packed(546, 0, 557), /* looseCompare $phpVersion */ - reg::packed(28, 0, 745), /* looseCompare return */ + reg::packed(28, 0, 934), /* toPhpDocNode return */ + reg::packed(316, 0, 29), /* looseCompare $type */ + reg::packed(575, 0, 586), /* looseCompare $phpVersion */ + reg::packed(28, 0, 774), /* looseCompare return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -191,36 +196,37 @@ inline constexpr sigtab::Sig createAllSmallerThan = { { 121 /* createAllSmallerT inline constexpr sigtab::Sig createAllSmallerThanOrEqualTo = { { 142 /* createAllSmallerThanOrEqualTo */, 1, 14, 1, 15, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig createAllGreaterThan = { { 172 /* createAllGreaterThan */, 1, 16, 1, 17, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig createAllGreaterThanOrEqualTo = { { 193 /* createAllGreaterThanOrEqualTo */, 1, 18, 1, 19, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getMin = { { 223 /* getMin */, 0, 20, 0, 20, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getMax = { { 230 /* getMax */, 0, 21, 0, 21, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig describe = { { 271 /* describe */, 1, 22, 1, 23, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig shift = { { 20 /* shift */, 1, 24, 1, 25, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig accepts = { { 331 /* accepts */, 2, 26, 2, 28, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isSuperTypeOf = { { 372 /* isSuperTypeOf */, 1, 29, 1, 30, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isSubTypeOf = { { 396 /* isSubTypeOf */, 1, 31, 1, 32, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isSubTypeOfUnionWithReason = { { 431 /* isSubTypeOfUnionWithReason */, 1, 33, 1, 34, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isAcceptedBy = { { 472 /* isAcceptedBy */, 2, 35, 2, 37, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig equals = { { 485 /* equals */, 1, 38, 1, 39, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig generalize = { { 535 /* generalize */, 1, 40, 1, 41, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isSmallerThan = { { 601 /* isSmallerThan */, 2, 42, 2, 44, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isSmallerThanOrEqual = { { 615 /* isSmallerThanOrEqual */, 2, 45, 2, 47, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isGreaterThan = { { 636 /* isGreaterThan */, 2, 48, 2, 50, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isGreaterThanOrEqual = { { 650 /* isGreaterThanOrEqual */, 2, 51, 2, 53, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getSmallerType = { { 671 /* getSmallerType */, 1, 54, 1, 55, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getSmallerOrEqualType = { { 686 /* getSmallerOrEqualType */, 1, 56, 1, 57, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getGreaterType = { { 708 /* getGreaterType */, 1, 58, 1, 59, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getGreaterOrEqualType = { { 723 /* getGreaterOrEqualType */, 1, 60, 1, 61, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toBoolean = { { 770 /* toBoolean */, 0, 62, 0, 62, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toAbsoluteNumber = { { 780 /* toAbsoluteNumber */, 0, 63, 0, 63, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toBitwiseNotType = { { 797 /* toBitwiseNotType */, 0, 64, 0, 64, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toString = { { 814 /* toString */, 0, 65, 0, 65, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryUnion = { { 823 /* tryUnion */, 1, 66, 1, 67, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryIntersect = { { 832 /* tryIntersect */, 1, 68, 1, 69, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig tryRemove = { { 858 /* tryRemove */, 1, 70, 1, 71, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig exponentiate = { { 877 /* exponentiate */, 1, 72, 1, 73, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getFiniteTypes = { { 890 /* getFiniteTypes */, 0, 74, 0, 74, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig toPhpDocNode = { { 944 /* toPhpDocNode */, 0, 75, 0, 75, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig looseCompare = { { 957 /* looseCompare */, 2, 76, 2, 78, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig fromFloatComparison = { { 232 /* fromFloatComparison */, 2, 20, 2, 22, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getMin = { { 252 /* getMin */, 0, 23, 0, 23, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getMax = { { 259 /* getMax */, 0, 24, 0, 24, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig describe = { { 300 /* describe */, 1, 25, 1, 26, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig shift = { { 20 /* shift */, 1, 27, 1, 28, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig accepts = { { 360 /* accepts */, 2, 29, 2, 31, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isSuperTypeOf = { { 401 /* isSuperTypeOf */, 1, 32, 1, 33, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isSubTypeOf = { { 425 /* isSubTypeOf */, 1, 34, 1, 35, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isSubTypeOfUnionWithReason = { { 460 /* isSubTypeOfUnionWithReason */, 1, 36, 1, 37, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isAcceptedBy = { { 501 /* isAcceptedBy */, 2, 38, 2, 40, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig equals = { { 514 /* equals */, 1, 41, 1, 42, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig generalize = { { 564 /* generalize */, 1, 43, 1, 44, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isSmallerThan = { { 630 /* isSmallerThan */, 2, 45, 2, 47, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isSmallerThanOrEqual = { { 644 /* isSmallerThanOrEqual */, 2, 48, 2, 50, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isGreaterThan = { { 665 /* isGreaterThan */, 2, 51, 2, 53, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isGreaterThanOrEqual = { { 679 /* isGreaterThanOrEqual */, 2, 54, 2, 56, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getSmallerType = { { 700 /* getSmallerType */, 1, 57, 1, 58, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getSmallerOrEqualType = { { 715 /* getSmallerOrEqualType */, 1, 59, 1, 60, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getGreaterType = { { 737 /* getGreaterType */, 1, 61, 1, 62, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getGreaterOrEqualType = { { 752 /* getGreaterOrEqualType */, 1, 63, 1, 64, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toBoolean = { { 799 /* toBoolean */, 0, 65, 0, 65, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toAbsoluteNumber = { { 809 /* toAbsoluteNumber */, 0, 66, 0, 66, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toBitwiseNotType = { { 826 /* toBitwiseNotType */, 0, 67, 0, 67, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toString = { { 843 /* toString */, 0, 68, 0, 68, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryUnion = { { 852 /* tryUnion */, 1, 69, 1, 70, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryIntersect = { { 861 /* tryIntersect */, 1, 71, 1, 72, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig tryRemove = { { 887 /* tryRemove */, 1, 73, 1, 74, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig exponentiate = { { 906 /* exponentiate */, 1, 75, 1, 76, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getFiniteTypes = { { 919 /* getFiniteTypes */, 0, 77, 0, 77, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toPhpDocNode = { { 973 /* toPhpDocNode */, 0, 78, 0, 78, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig looseCompare = { { 986 /* looseCompare */, 2, 79, 2, 81, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::IntegerRangeType diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index ee911dd64e7..ceab6a10a6b 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -353,14 +353,25 @@ } } } - foreach ([0, 1, -1, 42, PHP_INT_MAX, PHP_INT_MIN, PHP_INT_MAX - 1, PHP_INT_MIN + 1, 0.0, 0.5, -0.5, 2.5, -2.5, 1e30, -1e30, 9.2233720368547758E+18, -9.2233720368547758E+18, 9.2e18, -9.2e18, (float) PHP_INT_MAX, (float) PHP_INT_MIN, PHP_INT_MAX + 1.0, 9223372036854774784.0, 2147483647.0, 2147483646.5, 2147483647.5, -2147483648.0, -2147483648.5, NAN, INF, -INF] as $value) { + foreach ([0, 1, -1, 42, PHP_INT_MAX, PHP_INT_MIN, PHP_INT_MAX - 1, PHP_INT_MIN + 1, 0.0, 0.5, -0.5, 2.5, -2.5, 1e30, -1e30, 9.2233720368547758E+18, -9.2233720368547758E+18, 9.2e18, -9.2e18, (float) PHP_INT_MAX, (float) PHP_INT_MIN, PHP_INT_MAX + 1.0, 9223372036854774784.0, 2147483647.0, 2147483646.5, 2147483647.5, -2147483648.0, -2147483648.5, 9007199254740992.0, 9007199254740996.0, -9007199254740996.0, 9223372036854774784.0 - 2048.0, -9223372036854774784.0, NAN, INF, -INF] as $value) { $key = is_float($value) ? var_export($value, true) : (string) $value; foreach (['createAllSmallerThan', 'createAllSmallerThanOrEqualTo', 'createAllGreaterThan', 'createAllGreaterThanOrEqualTo'] as $factory) { $r["$factory $key"] = @$view($rangeClass::$factory($value)); } } + $fromFloatComparison = new \ReflectionMethod($rangeClass, 'fromFloatComparison'); + foreach ([2.5, -2.5, 9.2233720368547758E+18, -9.2233720368547758E+18, 9007199254740996.0, NAN] as $value) { + foreach (['<', '<=', '>', '>=', '!='] as $operator) { + $key = 'fromFloatComparison ' . var_export($value, true) . " $operator"; + try { + $r[$key] = $view($fromFloatComparison->invoke(null, $value, $operator)); + } catch (\Throwable $e) { + $r[$key] = get_class($e); + } + } + } foreach (['5', '1e30', '-1e30', true, null] as $value) { - foreach (['createAllSmallerThan', 'createAllGreaterThanOrEqualTo'] as $factory) { + foreach (['createAllSmallerThan', 'createAllSmallerThanOrEqualTo', 'createAllGreaterThan', 'createAllGreaterThanOrEqualTo'] as $factory) { try { $r["$factory " . var_export($value, true)] = $view($rangeClass::$factory($value)); } catch (\TypeError $e) { From 8ddd9c52fe22727b369fbe36a5f2f23b3f474835 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Mon, 2 Nov 2026 17:19:16 +0900 Subject: [PATCH 4/4] Do not use match in IntegerRangeType and its test The 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 --- src/Type/IntegerRangeType.php | 18 +++++++---- tests/PHPStan/Type/IntegerRangeTypeTest.php | 36 ++++++++++++++------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index c14aef9b17c..c43e741c595 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -145,13 +145,17 @@ public static function createAllGreaterThanOrEqualTo($value): Type */ private static function fromFloatComparison(float $value, string $operator): Type { - $holds = match ($operator) { - '<' => static fn (int $int): bool => $int < $value, - '<=' => static fn (int $int): bool => $int <= $value, - '>' => static fn (int $int): bool => $int > $value, - '>=' => static fn (int $int): bool => $int >= $value, - default => throw new ShouldNotHappenException(), - }; + if ($operator === '<') { + $holds = static fn (int $int): bool => $int < $value; + } elseif ($operator === '<=') { + $holds = static fn (int $int): bool => $int <= $value; + } elseif ($operator === '>') { + $holds = static fn (int $int): bool => $int > $value; + } elseif ($operator === '>=') { + $holds = static fn (int $int): bool => $int >= $value; + } else { + throw new ShouldNotHappenException(); + } $holdsAtMin = $holds(PHP_INT_MIN); if ($holdsAtMin === $holds(PHP_INT_MAX)) { diff --git a/tests/PHPStan/Type/IntegerRangeTypeTest.php b/tests/PHPStan/Type/IntegerRangeTypeTest.php index 17bb4eacec0..dc911e9d88a 100644 --- a/tests/PHPStan/Type/IntegerRangeTypeTest.php +++ b/tests/PHPStan/Type/IntegerRangeTypeTest.php @@ -162,25 +162,39 @@ public static function dataMatchesPhpComparison(): iterable '2**53 + 4' => 9007199254740996.0, '-2**53 - 4' => -9007199254740996.0, ]; + $comparisons = [ + '<' => [ + static fn (float $value): Type => IntegerRangeType::createAllSmallerThan($value), + static fn (int $int, float $value): bool => $int < $value, + ], + '<=' => [ + static fn (float $value): Type => IntegerRangeType::createAllSmallerThanOrEqualTo($value), + static fn (int $int, float $value): bool => $int <= $value, + ], + '>' => [ + static fn (float $value): Type => IntegerRangeType::createAllGreaterThan($value), + static fn (int $int, float $value): bool => $int > $value, + ], + '>=' => [ + static fn (float $value): Type => IntegerRangeType::createAllGreaterThanOrEqualTo($value), + static fn (int $int, float $value): bool => $int >= $value, + ], + ]; foreach ($values as $label => $value) { - foreach (['<', '<=', '>', '>='] as $operator) { - yield sprintf('$int %s %s', $operator, $label) => [$operator, $value]; + foreach ($comparisons as $operator => [$factory, $compare]) { + yield sprintf('$int %s %s', $operator, $label) => [$factory, $compare, $value]; } } } /** - * @param '<'|'<='|'>'|'>=' $operator + * @param callable(float): Type $factory + * @param callable(int, float): bool $compare */ #[DataProvider('dataMatchesPhpComparison')] - public function testMatchesPhpComparison(string $operator, float $value): void + public function testMatchesPhpComparison(callable $factory, callable $compare, float $value): void { - [$type, $compare] = match ($operator) { - '<' => [IntegerRangeType::createAllSmallerThan($value), static fn (int $int): bool => $int < $value], - '<=' => [IntegerRangeType::createAllSmallerThanOrEqualTo($value), static fn (int $int): bool => $int <= $value], - '>' => [IntegerRangeType::createAllGreaterThan($value), static fn (int $int): bool => $int > $value], - '>=' => [IntegerRangeType::createAllGreaterThanOrEqualTo($value), static fn (int $int): bool => $int >= $value], - }; + $type = $factory($value); // a comparison with a float flips at most once, near the float or at an end of the int range; // the ints converting to one float near 2**63 span 1024 of them @@ -194,7 +208,7 @@ public function testMatchesPhpComparison(string $operator, float $value): void $from = $center < PHP_INT_MIN + self::PROBED_INTS_AROUND_BOUND ? PHP_INT_MIN : $center - self::PROBED_INTS_AROUND_BOUND; $to = $center > PHP_INT_MAX - self::PROBED_INTS_AROUND_BOUND ? PHP_INT_MAX : $center + self::PROBED_INTS_AROUND_BOUND; for ($int = $from;; $int++) { - if ($type->isSuperTypeOf(new ConstantIntegerType($int))->yes() !== $compare($int)) { + if ($type->isSuperTypeOf(new ConstantIntegerType($int))->yes() !== $compare($int, $value)) { $mismatches[] = $int; } if ($int === $to) {