Skip to content

Commit b3bab80

Browse files
committed
Infer float ranges for math functions and int division
ceil() and floor() map the bounds of a range. round() is bounded by floor() and ceil() of them, since PHP versions and modes round .5 differently, and keeps only the sign for another precision. sqrt() maps the bounds and adds NAN below zero; fmod() has the sign of the dividend and a magnitude below both operands, NAN for an infinite dividend or a zero divisor. lcg_value() is float<0, 1>, microtime(true) non-negative, and the float of hrtime(true) at least 1. The float part of an int division is the range of the quotients of the bounds unless it is every float but NAN, so count($list) / 4 is float<0.0, max>|int<0, max> and its ceil() float<0.0, max>. An integer divisor next to zero is -1 or 1, not a denormal, and the infinities that stand for an unbounded int no longer make NAN. Closes phpstan/phpstan#6963 Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
1 parent a8ecd82 commit b3bab80

19 files changed

Lines changed: 805 additions & 225 deletions

‎resources/functionMap.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5273,7 +5273,7 @@
52735273
'layerObj::setWKTProjection' => ['int', 'proj_params'=>'string'],
52745274
'layerObj::updateFromString' => ['int', 'snippet'=>'string'],
52755275
'lcfirst' => ['string', 'str'=>'string'],
5276-
'lcg_value' => ['float'],
5276+
'lcg_value' => ['float<0, 1>'],
52775277
'lchgrp' => ['bool', 'filename'=>'string', 'group'=>'string|int'],
52785278
'lchown' => ['bool', 'filename'=>'string', 'user'=>'string|int'],
52795279
'ldap_8859_to_t61' => ['string|false', 'value'=>'string'],

‎src/Type/OperatorTypeHelper.php‎

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -438,11 +438,68 @@ public static function divide(Type $leftType, Type $rightType, PhpVersion $phpVe
438438
return $withoutFloat;
439439
}
440440
}
441+
442+
// the float of an int division lies between the quotients of the bounds
443+
$floatType = self::getIntegerDivisionFloatType($leftType, $rightType, $phpVersion);
444+
if ($floatType !== null) {
445+
return self::replaceFloatType($result, $floatType);
446+
}
441447
}
442448

443449
return $result;
444450
}
445451

452+
private static function getIntegerDivisionFloatType(Type $leftType, Type $rightType, PhpVersion $phpVersion): ?Type
453+
{
454+
$leftMembers = self::getFloatIntervalMembers($leftType);
455+
$rightMembers = self::getFloatIntervalMembers($rightType);
456+
if ($leftMembers === null || $rightMembers === null) {
457+
return null;
458+
}
459+
if (count($leftMembers) * count($rightMembers) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
460+
return null;
461+
}
462+
463+
$floatTypes = [];
464+
foreach ($leftMembers as [, $leftInterval]) {
465+
foreach ($rightMembers as [, $rightInterval]) {
466+
foreach (self::splitIntegerDivisor($rightInterval) as $divisorInterval) {
467+
$floatTypes[] = self::floatIntervalMath('/', $leftInterval, $divisorInterval, $phpVersion);
468+
}
469+
}
470+
}
471+
472+
// a quotient that can be any float but NAN reads better as float, like int + 1.5 does
473+
$floatType = TypeCombinator::union(...$floatTypes);
474+
if ($floatType->equals(FloatRangeType::fromInterval(-INF, INF))) {
475+
return null;
476+
}
477+
478+
return $floatType;
479+
}
480+
481+
private static function replaceFloatType(Type $type, Type $floatType): Type
482+
{
483+
if (get_class($type) === FloatType::class) {
484+
return $floatType;
485+
}
486+
if (!$type instanceof UnionType) {
487+
return $type;
488+
}
489+
490+
$types = [];
491+
foreach ($type->getTypes() as $innerType) {
492+
$types[] = get_class($innerType) === FloatType::class ? $floatType : $innerType;
493+
}
494+
495+
$union = TypeCombinator::union(...$types);
496+
if ($type instanceof BenevolentUnionType) {
497+
return TypeUtils::toBenevolentUnion($union);
498+
}
499+
500+
return $union;
501+
}
502+
446503
private static function getDivTypeFromTypes(Type $leftType, Type $rightType): Type
447504
{
448505
$leftTypes = $leftType->getConstantScalarTypes();
@@ -1672,19 +1729,48 @@ private static function floatRangeMath(string $operator, Type $leftType, Type $r
16721729
continue;
16731730
}
16741731

1675-
$resultTypes[] = self::floatIntervalMath($operator, $leftInterval, $rightInterval, $phpVersion);
1732+
foreach ($operator === '/' && $rightMember->isInteger()->yes() ? self::splitIntegerDivisor($rightInterval) : [$rightInterval] as $divisorInterval) {
1733+
$resultTypes[] = self::floatIntervalMath($operator, $leftInterval, $divisorInterval, $phpVersion);
1734+
}
16761735
}
16771736
}
16781737

16791738
return TypeCombinator::union(...$resultTypes);
16801739
}
16811740

1741+
/**
1742+
* An integer divisor next to zero is -1 or 1, not a denormal: the
1743+
* negative part, zero on its own and the positive part.
1744+
*
1745+
* @param array{float, float, bool, bool} $interval
1746+
* @return list<array{float, float, bool, bool}>
1747+
*/
1748+
private static function splitIntegerDivisor(array $interval): array
1749+
{
1750+
[$min, $max] = $interval;
1751+
if ($min > 0.0 || $max < 0.0) {
1752+
return [$interval];
1753+
}
1754+
1755+
$intervals = [];
1756+
if ($min < 0.0) {
1757+
$intervals[] = [$min, -1.0, false, false];
1758+
}
1759+
$intervals[] = [0.0, 0.0, false, false];
1760+
if ($max > 0.0) {
1761+
$intervals[] = [1.0, $max, false, false];
1762+
}
1763+
1764+
return $intervals;
1765+
}
1766+
16821767
/**
16831768
* The members of a number type with the floats each of them stands for:
1684-
* [min, max, whether NAN], an int without a bound reaching the infinity
1685-
* on that side, and NAN alone being an empty interval.
1769+
* [min, max, whether NAN, whether an infinite bound is a value]. An int
1770+
* without a bound reaches the infinity on that side, which it never is,
1771+
* and NAN alone is an empty interval.
16861772
*
1687-
* @return list<array{Type, array{float, float, bool}}>|null
1773+
* @return list<array{Type, array{float, float, bool, bool}}>|null
16881774
*/
16891775
private static function getFloatIntervalMembers(Type $type): ?array
16901776
{
@@ -1695,22 +1781,23 @@ private static function getFloatIntervalMembers(Type $type): ?array
16951781
$members = [];
16961782
foreach ($type instanceof UnionType ? $type->getTypes() : [$type] as $member) {
16971783
if ($member instanceof ConstantIntegerType) {
1698-
$interval = [(float) $member->getValue(), (float) $member->getValue(), false];
1784+
$interval = [(float) $member->getValue(), (float) $member->getValue(), false, false];
16991785
} elseif ($member instanceof IntegerRangeType) {
17001786
$interval = [
17011787
$member->getMin() === null ? -INF : (float) $member->getMin(),
17021788
$member->getMax() === null ? INF : (float) $member->getMax(),
17031789
false,
1790+
false,
17041791
];
17051792
} elseif ($member instanceof ConstantFloatType) {
17061793
$value = $member->getValue();
1707-
$interval = is_nan($value) ? [INF, -INF, true] : [$value, $value, false];
1794+
$interval = is_nan($value) ? [INF, -INF, true, true] : [$value, $value, false, true];
17081795
} elseif ($member instanceof FloatRangeType) {
1709-
$interval = [$member->getMin(), $member->getMax(), false];
1796+
$interval = [$member->getMin(), $member->getMax(), false, true];
17101797
} elseif (get_class($member) === IntegerType::class) {
1711-
$interval = [-INF, INF, false];
1798+
$interval = [-INF, INF, false, false];
17121799
} elseif (get_class($member) === FloatType::class) {
1713-
$interval = [-INF, INF, true];
1800+
$interval = [-INF, INF, true, true];
17141801
} else {
17151802
return null;
17161803
}
@@ -1723,27 +1810,32 @@ private static function getFloatIntervalMembers(Type $type): ?array
17231810

17241811
/**
17251812
* @param '+'|'-'|'*'|'/' $operator
1726-
* @param array{float, float, bool} $left
1727-
* @param array{float, float, bool} $right
1813+
* @param array{float, float, bool, bool} $left
1814+
* @param array{float, float, bool, bool} $right
17281815
*/
17291816
private static function floatIntervalMath(string $operator, array $left, array $right, PhpVersion $phpVersion): Type
17301817
{
1731-
[$leftMin, $leftMax, $nan] = $left;
1732-
[$rightMin, $rightMax, $rightNan] = $right;
1818+
[$leftMin, $leftMax, $nan, $leftInfinities] = $left;
1819+
[$rightMin, $rightMax, $rightNan, $rightInfinities] = $right;
17331820
$nan = $nan || $rightNan;
17341821
$resultTypes = [];
17351822

17361823
if ($leftMin <= $leftMax && $rightMin <= $rightMax) {
17371824
$leftHasZero = $leftMin <= 0.0 && $leftMax >= 0.0;
17381825
$rightHasZero = $rightMin <= 0.0 && $rightMax >= 0.0;
1739-
$leftHasInfinity = $leftMin === -INF || $leftMax === INF;
1740-
$rightHasInfinity = $rightMin === -INF || $rightMax === INF;
1826+
// the infinities that stand for an unbounded int are not values
1827+
$leftHasNegativeInfinity = $leftInfinities && $leftMin === -INF;
1828+
$leftHasPositiveInfinity = $leftInfinities && $leftMax === INF;
1829+
$rightHasNegativeInfinity = $rightInfinities && $rightMin === -INF;
1830+
$rightHasPositiveInfinity = $rightInfinities && $rightMax === INF;
1831+
$leftHasInfinity = $leftHasNegativeInfinity || $leftHasPositiveInfinity;
1832+
$rightHasInfinity = $rightHasNegativeInfinity || $rightHasPositiveInfinity;
17411833

17421834
$rightIntervals = [[$rightMin, $rightMax]];
17431835
if ($operator === '+') {
1744-
$nan = $nan || ($leftMax === INF && $rightMin === -INF) || ($leftMin === -INF && $rightMax === INF);
1836+
$nan = $nan || ($leftHasPositiveInfinity && $rightHasNegativeInfinity) || ($leftHasNegativeInfinity && $rightHasPositiveInfinity);
17451837
} elseif ($operator === '-') {
1746-
$nan = $nan || ($leftMax === INF && $rightMax === INF) || ($leftMin === -INF && $rightMin === -INF);
1838+
$nan = $nan || ($leftHasPositiveInfinity && $rightHasPositiveInfinity) || ($leftHasNegativeInfinity && $rightHasNegativeInfinity);
17471839
} elseif ($operator === '*') {
17481840
$nan = $nan || ($leftHasZero && $rightHasInfinity) || ($rightHasZero && $leftHasInfinity);
17491841
} else {
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\DependencyInjection\AutowiredService;
8+
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\Type\Constant\ConstantFloatType;
10+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
11+
use PHPStan\Type\FloatRangeType;
12+
use PHPStan\Type\FloatType;
13+
use PHPStan\Type\IntegerType;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeCombinator;
16+
use PHPStan\Type\UnionType;
17+
use function abs;
18+
use function fmod;
19+
use function is_infinite;
20+
use function is_nan;
21+
use function max;
22+
use function min;
23+
use const NAN;
24+
25+
#[AutowiredService]
26+
final class FmodFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
27+
{
28+
29+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
30+
{
31+
return $functionReflection->getName() === 'fmod';
32+
}
33+
34+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
35+
{
36+
$args = $functionCall->getArgs();
37+
if (!isset($args[0], $args[1])) {
38+
return null;
39+
}
40+
41+
$leftMembers = $this->getFloatMembers($scope->getType($args[0]->value));
42+
$rightMembers = $this->getFloatMembers($scope->getType($args[1]->value));
43+
if ($leftMembers === null || $rightMembers === null) {
44+
return null;
45+
}
46+
47+
$resultTypes = [];
48+
foreach ($leftMembers as $left) {
49+
foreach ($rightMembers as $right) {
50+
if ($left instanceof ConstantFloatType && $right instanceof ConstantFloatType) {
51+
$resultTypes[] = new ConstantFloatType(fmod($left->getValue(), $right->getValue()));
52+
continue;
53+
}
54+
55+
$resultTypes[] = $this->getRemainderType($this->getBounds($left), $this->getBounds($right));
56+
}
57+
}
58+
59+
return TypeCombinator::union(...$resultTypes);
60+
}
61+
62+
/**
63+
* @return list<ConstantFloatType|FloatRangeType>|null
64+
*/
65+
private function getFloatMembers(Type $type): ?array
66+
{
67+
if (!(new UnionType([new IntegerType(), new FloatType()]))->isSuperTypeOf($type)->yes()) {
68+
return null;
69+
}
70+
71+
$members = [];
72+
$floatType = $type->toFloat();
73+
foreach ($floatType instanceof UnionType ? $floatType->getTypes() : [$floatType] as $member) {
74+
if (!$member instanceof ConstantFloatType && !$member instanceof FloatRangeType) {
75+
return null;
76+
}
77+
$members[] = $member;
78+
}
79+
80+
return $members;
81+
}
82+
83+
/**
84+
* @return array{float, float}|null null for NAN
85+
*/
86+
private function getBounds(ConstantFloatType|FloatRangeType $type): ?array
87+
{
88+
if ($type instanceof FloatRangeType) {
89+
return [$type->getMin(), $type->getMax()];
90+
}
91+
92+
$value = $type->getValue();
93+
94+
return is_nan($value) ? null : [$value, $value];
95+
}
96+
97+
/**
98+
* The remainder has the sign of the dividend and a smaller magnitude
99+
* than both operands. An infinite dividend or a zero divisor is NAN.
100+
*
101+
* @param array{float, float}|null $left
102+
* @param array{float, float}|null $right
103+
*/
104+
private function getRemainderType(?array $left, ?array $right): Type
105+
{
106+
if ($left === null || $right === null) {
107+
return new ConstantFloatType(NAN);
108+
}
109+
110+
[$leftMin, $leftMax] = $left;
111+
[$rightMin, $rightMax] = $right;
112+
$resultTypes = [];
113+
if (is_infinite($leftMin) || is_infinite($leftMax) || ($rightMin <= 0.0 && $rightMax >= 0.0)) {
114+
$resultTypes[] = new ConstantFloatType(NAN);
115+
}
116+
117+
$leftHasFinite = $leftMin < $leftMax || !is_infinite($leftMin);
118+
$rightHasNonZero = $rightMin < 0.0 || $rightMax > 0.0;
119+
if ($leftHasFinite && $rightHasNonZero) {
120+
$divisorMagnitude = max(abs($rightMin), abs($rightMax));
121+
$resultTypes[] = FloatRangeType::fromInterval(
122+
$leftMin < 0.0 ? -min(-$leftMin, $divisorMagnitude) : 0.0,
123+
$leftMax > 0.0 ? min($leftMax, $divisorMagnitude) : 0.0,
124+
);
125+
}
126+
127+
return TypeCombinator::union(...$resultTypes);
128+
}
129+
130+
}

‎src/Type/Php/HrtimeFunctionReturnTypeExtension.php‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
use PHPStan\Type\Constant\ConstantArrayType;
1212
use PHPStan\Type\Constant\ConstantIntegerType;
1313
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
14-
use PHPStan\Type\FloatType;
14+
use PHPStan\Type\FloatRangeType;
1515
use PHPStan\Type\IntegerRangeType;
1616
use PHPStan\Type\Type;
1717
use PHPStan\Type\TypeCombinator;
1818
use function count;
19+
use const INF;
1920

2021
#[AutowiredService]
2122
final class HrtimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
@@ -34,7 +35,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3435
[2],
3536
isList: TrinaryLogic::createYes(),
3637
);
37-
$numberType = new BenevolentUnionType([IntegerRangeType::fromInterval(1, null), new FloatType()]);
38+
$numberType = new BenevolentUnionType([IntegerRangeType::fromInterval(1, null), FloatRangeType::fromInterval(1.0, INF)]);
3839

3940
if (count($functionCall->getArgs()) < 1) {
4041
return $arrayType;

‎src/Type/Php/MicrotimeFunctionReturnTypeExtension.php‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
1010
use PHPStan\Type\BenevolentUnionType;
1111
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12-
use PHPStan\Type\FloatType;
12+
use PHPStan\Type\FloatRangeType;
1313
use PHPStan\Type\IntersectionType;
1414
use PHPStan\Type\MixedType;
1515
use PHPStan\Type\StringType;
1616
use PHPStan\Type\Type;
1717
use PHPStan\Type\UnionType;
1818
use function count;
19+
use const INF;
1920

2021
#[AutowiredService]
2122
final class MicrotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
@@ -29,6 +30,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo
2930
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
3031
{
3132
$stringType = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]);
33+
$floatType = FloatRangeType::fromInterval(0.0, INF);
3234

3335
if (count($functionCall->getArgs()) < 1) {
3436
return $stringType;
@@ -39,17 +41,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
3941
$isFalseType = $argType->isFalse();
4042
$compareTypes = $isTrueType->compareTo($isFalseType);
4143
if ($compareTypes === $isTrueType) {
42-
return new FloatType();
44+
return $floatType;
4345
}
4446
if ($compareTypes === $isFalseType) {
4547
return $stringType;
4648
}
4749

4850
if ($argType instanceof MixedType) {
49-
return new BenevolentUnionType([$stringType, new FloatType()]);
51+
return new BenevolentUnionType([$stringType, $floatType]);
5052
}
5153

52-
return new UnionType([$stringType, new FloatType()]);
54+
return new UnionType([$stringType, $floatType]);
5355
}
5456

5557
}

0 commit comments

Comments
 (0)