Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 47 additions & 38 deletions src/Type/IntegerRangeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -92,17 +91,7 @@ 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) {
return new IntegerType();
}

if ($value <= PHP_INT_MIN) {
return new NeverType();
}

return self::fromInterval(null, (int) ceil($value), -1);
return self::fromFloatComparison($value, '<');
}

/**
Expand All @@ -116,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, '<=');
}

/**
Expand All @@ -138,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, '>');
}

/**
Expand All @@ -160,17 +133,53 @@ public static function createAllGreaterThanOrEqualTo($value): Type
return self::fromInterval($value, null);
}

if ($value <= PHP_INT_MIN) {
return new IntegerType();
return self::fromFloatComparison($value, '>=');
}

/**
* 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
{
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();
}

// (float) PHP_INT_MAX is already past the int range, so no int
// reaches it
if ($value >= PHP_INT_MAX) {
return new NeverType();
$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($value), null);
return $holdsAtMin ? self::fromInterval(null, $low) : self::fromInterval($high, null);
}

public function getMin(): ?int
Expand Down
69 changes: 69 additions & 0 deletions tests/PHPStan/Analyser/nsrt/integer-range-float-two-to-the-63.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php declare(strict_types = 1);

namespace IntegerRangeFloatTwoToThe63;

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<min, 9223372036854775295>', $i);
} else {
assertType('int<9223372036854775296, max>', $i);
}
assertType('int', $i);
}

function greaterOrEqual(int $i): void
{
if ($i >= 9.2233720368547758E18) {
assertType('int<9223372036854775296, max>', $i);
} else {
assertType('int<min, 9223372036854775295>', $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);
}
}

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<min, 9007199254740994>', $i);
} else {
assertType('int<9007199254740995, max>', $i);
}
if ($i <= 9007199254740996.0) {
assertType('int<min, 9007199254740997>', $i);
} else {
assertType('int<9007199254740998, max>', $i);
}
}
Loading
Loading