Skip to content

Commit

Permalink
Improve range computation
Browse files Browse the repository at this point in the history
  • Loading branch information
VincentLanglet committed Nov 4, 2022
1 parent 2e31cbe commit d3e52c7
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 51 deletions.
112 changes: 76 additions & 36 deletions src/Reflection/InitializerExprTypeResolver.php
Expand Up @@ -62,8 +62,10 @@
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnionType;
use function array_keys;
use function ceil;
use function count;
use function dirname;
use function floor;
use function in_array;
use function is_float;
use function is_int;
Expand Down Expand Up @@ -1551,6 +1553,14 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T
return $union->toNumber();
}

if ($operand instanceof IntegerRangeType) {
$operandMin = $operand->getMin();
$operandMax = $operand->getMax();
} else {
$operandMin = $operand->getValue();
$operandMax = $operand->getValue();
}

if ($node instanceof BinaryOp\Plus) {
if ($operand instanceof ConstantIntegerType) {
/** @var int|float|null $min */
Expand Down Expand Up @@ -1616,63 +1626,93 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T
}
}
} elseif ($node instanceof Expr\BinaryOp\Mul) {
if ($operand instanceof ConstantIntegerType) {
/** @var int|float|null $min */
$min = $rangeMin !== null ? $rangeMin * $operand->getValue() : null;
$min1 = ($rangeMin ?? -INF) * ($operandMin ?? -INF);
$min2 = ($rangeMin ?? -INF) * ($operandMax ?? INF);
$max1 = ($rangeMax ?? INF) * ($operandMin ?? -INF);
$max2 = ($rangeMax ?? INF) * ($operandMax ?? INF);

/** @var int|float|null $max */
$max = $rangeMax !== null ? $rangeMax * $operand->getValue() : null;
} else {
/** @var int|float|null $min */
$min = $rangeMin !== null && $operand->getMin() !== null ? $rangeMin * $operand->getMin() : null;
$min = min($min1, $min2, $max1, $max2);
$max = max($min1, $min2, $max1, $max2);

/** @var int|float|null $max */
$max = $rangeMax !== null && $operand->getMax() !== null ? $rangeMax * $operand->getMax() : null;
if ($min === -INF) {
$min = null;
}

if ($min !== null && $max !== null && $min > $max) {
[$min, $max] = [$max, $min];
}

// invert maximas on multiplication with negative constants
if ((($range instanceof ConstantIntegerType && $range->getValue() < 0)
|| ($operand instanceof ConstantIntegerType && $operand->getValue() < 0))
&& ($min === null || $max === null)) {
[$min, $max] = [$max, $min];
if ($max === INF) {
$max = null;
}

} else {
if ($operand instanceof ConstantIntegerType) {
$min = $rangeMin !== null && $operand->getValue() !== 0 ? $rangeMin / $operand->getValue() : null;
$max = $rangeMax !== null && $operand->getValue() !== 0 ? $rangeMax / $operand->getValue() : null;
} else {
$min = $rangeMin !== null && $operand->getMin() !== null && $operand->getMin() !== 0 ? $rangeMin / $operand->getMin() : null;
$max = $rangeMax !== null && $operand->getMax() !== null && $operand->getMax() !== 0 ? $rangeMax / $operand->getMax() : null;
}
// Avoid division by zero when looking for the min and the max by using the closest int
$operandMin = $operandMin !== 0 ? $operandMin : 1;
$operandMax = $operandMax !== 0 ? $operandMax : -1;

if (
($operandMin < 0 || $operandMin === null)
&& ($operandMax > 0 || $operandMax === null)
) {
$result = TypeCombinator::union(
$this->integerRangeMath($range, $node, IntegerRangeType::fromInterval($operandMin, 0)),
$this->integerRangeMath($range, $node, IntegerRangeType::fromInterval(0, $operandMax))
)->toNumber();

if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) {
return new BenevolentUnionType([new IntegerType(), new FloatType()]);
} else {
return $result;
}
}
if (
($rangeMin < 0 || $rangeMin === null)
&& ($rangeMax > 0 || $rangeMax === null)
) {
$result = TypeCombinator::union(
$this->integerRangeMath(IntegerRangeType::fromInterval($rangeMin, 0), $node, $operand),
$this->integerRangeMath(IntegerRangeType::fromInterval(0, $rangeMax), $node, $operand)
)->toNumber();

if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) {
return new BenevolentUnionType([new IntegerType(), new FloatType()]);
} else {
return $result;
}
}

$rangeMinSign = ($rangeMin ?? -INF) <=> 0;
$rangeMaxSign = ($rangeMax ?? INF) <=> 0;

$min1 = $operandMin !== null ? ($rangeMin ?? -INF) / $operandMin : $rangeMinSign * -0.1;
$min2 = $operandMax !== null ? ($rangeMin ?? -INF) / $operandMax : $rangeMinSign * 0.1;
$max1 = $operandMin !== null ? ($rangeMax ?? INF) / $operandMin : $rangeMaxSign * -0.1;
$max2 = $operandMax !== null ? ($rangeMax ?? INF) / $operandMax : $rangeMaxSign * 0.1;

$min = min($min1, $min2, $max1, $max2);
$max = max($min1, $min2, $max1, $max2);

if ($range instanceof IntegerRangeType && $operand instanceof IntegerRangeType) {
if ($rangeMax === null && $operand->getMax() === null) {
$min = 0;
} elseif ($rangeMin === null && $operand->getMin() === null) {
if ($min === -INF) {
$min = null;
}
if ($max === INF) {
$max = null;
}
}

if ($min !== null && $max !== null && $min > $max) {
[$min, $max] = [$max, $min];
}

if ($operand instanceof IntegerRangeType
&& ($operand->getMin() === null || $operand->getMax() === null)
|| ($rangeMin === null || $rangeMax === null)
|| is_float($min) || is_float($max)
|| is_float($min)
|| is_float($max)
) {
if (is_float($min)) {
$min = (int) $min;
$min = (int) ceil($min);
}
if (is_float($max)) {
$max = (int) $max;
}

if ($min !== null && $max !== null && $min > $max) {
[$min, $max] = [$max, $min];
$max = (int) floor($max);
}

// invert maximas on division with negative constants
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -52,6 +52,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/date.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/instanceof.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/integer-range-types.php');
<<<<<<< HEAD
if (PHP_INT_SIZE === 8) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/random-int.php');
}
Expand Down
42 changes: 27 additions & 15 deletions tests/PHPStan/Analyser/data/integer-range-types.php
Expand Up @@ -218,70 +218,70 @@ public function math($i, $j, $z, $pi, $r1, $r2, $r3, $rMin, $rMax, $x, $y) {
assertType('int<2, 13>', $r1 + $j);
assertType('int<-2, 9>', $r1 - $j);
assertType('int<1, 30>', $r1 * $j);
assertType('float|int<0, 10>', $r1 / $j);
assertType('float|int<1, 10>', $r1 / $j);
assertType('int<min, 15>', $rMin * $j);
assertType('int<5, max>', $rMax * $j);

assertType('int<2, 13>', $j + $r1);
assertType('int<-9, 2>', $j - $r1);
assertType('int<1, 30>', $j * $r1);
assertType('float|int<0, 3>', $j / $r1);
assertType('float|int<1, 3>', $j / $r1);
assertType('int<min, 15>', $j * $rMin);
assertType('int<5, max>', $j * $rMax);

assertType('int<-19, -10>|int<2, 13>', $r1 + $z);
assertType('int<-2, 9>|int<21, 30>', $r1 - $z);
assertType('int<-200, -20>|int<1, 30>', $r1 * $z);
assertType('float|int<0, 10>', $r1 / $z);
assertType('float|int<1, 10>', $r1 / $z);
assertType('int', $rMin * $z);
assertType('int<min, -100>|int<5, max>', $rMax * $z);

assertType('int<2, max>', $pi + 1);
assertType('int<-1, max>', $pi - 2);
assertType('int<2, max>', $pi * 2);
assertType('float|int<0, max>', $pi / 2);
assertType('float|int<1, max>', $pi / 2);
assertType('int<2, max>', 1 + $pi);
assertType('int<min, 2>', 2 - $pi);
assertType('int<2, max>', 2 * $pi);
assertType('float|int<2, max>', 2 / $pi);
assertType('float|int<1, 2>', 2 / $pi);

assertType('int<5, 14>', $r1 + 4);
assertType('int<-3, 6>', $r1 - 4);
assertType('int<4, 40>', $r1 * 4);
assertType('float|int<0, 2>', $r1 / 4);
assertType('float|int<1, 2>', $r1 / 4);
assertType('int<9, max>', $rMax + 4);
assertType('int<1, max>', $rMax - 4);
assertType('int<20, max>', $rMax * 4);
assertType('float|int<1, max>', $rMax / 4);
assertType('float|int<2, max>', $rMax / 4);

assertType('int<6, 20>', $r1 + $r2);
assertType('int<-9, 5>', $r1 - $r2);
assertType('int<5, 100>', $r1 * $r2);
assertType('float|int<0, 1>', $r1 / $r2);
assertType('float|int<1, 2>', $r1 / $r2);

assertType('int<-99, 19>', $r1 - $r3);

assertType('int<min, 15>', $r1 + $rMin);
assertType('int<-4, max>', $r1 - $rMin);
assertType('int<min, 50>', $r1 * $rMin);
assertType('float|int<min, 2>', $r1 / $rMin);
assertType('float|int<-10, -1>|int<1, 10>', $r1 / $rMin);
assertType('int<min, 15>', $rMin + $r1);
assertType('int<min, 4>', $rMin - $r1);
assertType('int<min, 50>', $rMin * $r1);
assertType('float|int<min, 0>', $rMin / $r1);
assertType('float|int<min, 5>', $rMin / $r1);

assertType('int<6, max>', $r1 + $rMax);
assertType('int', $r1 - $rMax);
assertType('int<5, max>', $r1 * $rMax);
assertType('float|int<0, max>', $r1 / $rMax);
assertType('float|int<1, 2>', $r1 / $rMax);
assertType('int<6, max>', $rMax + $r1);
assertType('int<-5, max>', $rMax - $r1);
assertType('int<5, max>', $rMax * $r1);
assertType('float|int<5, max>', $rMax / $r1);
assertType('float|int<1, max>', $rMax / $r1);

assertType('5|10|15|20|30', $x / $y);

assertType('float|int<0, max>', $rMax / $rMax);
assertType('float|int<1, max>', $rMax / $rMax);
assertType('(float|int)', $rMin / $rMin);
}

Expand All @@ -307,8 +307,8 @@ public function maximaInversion($rMin, $rMax) {
assertType('int<-5, max>', $rMin * -1);
assertType('int<min, -10>', $rMax * -2);

assertType('float|int<0, max>', -1 / $rMin);
assertType('float|int<min, 0>', -2 / $rMax);
assertType('-1|1|float', -1 / $rMin);
assertType('float', -2 / $rMax);

assertType('float|int<-5, max>', $rMin / -1);
assertType('float|int<min, -2>', $rMax / -2);
Expand All @@ -330,4 +330,16 @@ public function unaryMinus($r1, $r2, $rMin, $rMax, $rZero) {
assertType('int<-50, 0>', -$rZero);
}

/**
* @param int<-1, 2> $p
* @param int<-1, 2> $u
*/
public function sayHello($p, $u): void
{
assertType('int<-2, 4>', $p + $u);
assertType('int<-3, 3>', $p - $u);
assertType('int<-2, 4>', $p * $u);
assertType('float|int<-2, 2>', $p / $u);
}

}

0 comments on commit d3e52c7

Please sign in to comment.