@@ -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 {
0 commit comments